diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000000..64a352ee2a --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,117 @@ +/*! + * 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. + */ + +module.exports = { + root: true, + parser: '@typescript-eslint/parser', + plugins: [ + '@typescript-eslint', + ], + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + ], + rules: { + // Following checks are temporarily disabled. We shall incrementally enable them in the + // future, fixing any violations as we go. + '@typescript-eslint/no-non-null-assertion': 0, + + // Disabled checks + '@typescript-eslint/no-explicit-any': 0, + '@typescript-eslint/no-use-before-define': 0, + '@typescript-eslint/no-var-requires': 0, + + // 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', + { + 'allowExpressions': true, + 'allowTypedFunctionExpressions': true, + 'allowHigherOrderFunctions': true + } + ], + 'no-unused-vars': 'off', // Must be disabled to enable the next rule + '@typescript-eslint/no-unused-vars': ['error'], + 'quotes': ['error', 'single', {'avoidEscape': true}], + '@typescript-eslint/naming-convention': [ + 'error', + { + "selector": "variable", + "format": ["camelCase", "UPPER_CASE"] + }, + { + "selector": "parameter", + "format": ["camelCase"], + "leadingUnderscore": "allow" + }, + + { + "selector": "memberLike", + "format": ["camelCase"] + }, + + { + "selector": "typeLike", + "format": ["PascalCase"] + }, + + // Ignore properties that require quotes (HTTP headers, names that include spaces or dashes etc.). + { + "selector": [ + "classProperty", + "objectLiteralProperty", + "typeProperty", + "classMethod", + "objectLiteralMethod", + "typeMethod", + "accessor", + "enumMember" + ], + "format": null, + "modifiers": ["requiresQuotes"] + }, + + // Ignore destructured property names. + { + "selector": "variable", + "modifiers": ["destructured"], + "format": null + }, + + // Following types are temporarily disabled. We shall incrementally enable them in the + // future, fixing any violations as we go. + { + "selector": [ + "classProperty", + "objectLiteralProperty", + "typeProperty", + "enumMember" + ], + "format": null + } + ], + } +}; diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000..7729d13a47 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "[FR]" +labels: 'type: feature request' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context, code samples or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/firestore-issue.md b/.github/ISSUE_TEMPLATE/firestore-issue.md new file mode 100644 index 0000000000..8b65dc5906 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/firestore-issue.md @@ -0,0 +1,53 @@ +--- +name: Firestore issue +about: Bug reports and feature requests related to Cloud Firestore +title: "[Firestore]" +labels: 'api: firestore' +assignees: schmidt-sebastian + +--- + +### [READ] Step 1: Are you in the right place? + +**Cloud Firestore support is provided by the [`@google-cloud/firestore`](https://npmjs.com/package/@google-cloud/firestore) library. Therefore the easiest and most efficient way to get Firestore issues resolved is by directly reporting them at the [nodejs-firestore](https://github.com/googleapis/nodejs-firestore) GitHub repo.** + +If you still think the problem is related to the code in this repository, then read on. + + * For issues or feature requests related to __the code in this repository__ + file a Github issue. + * For general technical questions, post a question on [StackOverflow](http://stackoverflow.com/) + with the firebase tag. + * For general Firebase discussion, use the [firebase-talk](https://groups.google.com/forum/#!forum/firebase-talk) + google group. + * For help troubleshooting your application that does not fall under one + of the above categories, reach out to the personalized + [Firebase support channel](https://firebase.google.com/support/). + +### [REQUIRED] Step 2: Describe your environment + + * Operating System version: _____ + * Firebase SDK version: _____ + * Firebase Product: Firestore + * Node.js version: _____ + * NPM version: _____ + +### [REQUIRED] Step 3: Describe the problem + +#### Steps to reproduce: + +What happened? How can we make the problem occur? +This could be a description, log/console output, etc. + +You can enable logging for Firestore by including the following line in your code: + +``` +admin.firestore.setLogFunction(console.log); +``` + +This will print Firestore logs to the console. + +#### Relevant Code: + +``` +// TODO(you): code here to reproduce the problem +``` diff --git a/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE/general-bug-report.md similarity index 72% rename from ISSUE_TEMPLATE.md rename to .github/ISSUE_TEMPLATE/general-bug-report.md index 5de83b2cc9..82308572d5 100644 --- a/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE/general-bug-report.md @@ -1,8 +1,17 @@ +--- +name: General Bug report +about: Bug reports related to any component in this repo +title: '' +labels: '' +assignees: '' + +--- + ### [READ] Step 1: Are you in the right place? - * For issues or feature requests related to __the code in this repository__ - file a Github issue. - * If this is a __feature request__ make sure the issue title starts with "FR:". + * For issues related to __the code in this repository__ file a Github issue. + * If the issue pertains to Cloud Firestore, read the instructions in the "Firestore issue" + template. * For general technical questions, post a question on [StackOverflow](http://stackoverflow.com/) with the firebase tag. * For general Firebase discussion, use the [firebase-talk](https://groups.google.com/forum/#!forum/firebase-talk) @@ -15,8 +24,9 @@ * Operating System version: _____ * Firebase SDK version: _____ - * Library version: _____ * Firebase Product: _____ (auth, database, storage, etc) + * Node.js version: _____ + * NPM version: _____ ### [REQUIRED] Step 3: Describe the problem 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..eca721b842 --- /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: 'node20' + 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..c7529cc1dd --- /dev/null +++ b/.github/actions/send-email/dist/index.js @@ -0,0 +1,28211 @@ +/******/ (() => { // webpackBootstrap +/******/ var __webpack_modules__ = ({ + +/***/ 7351: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.issue = exports.issueCommand = void 0; +const os = __importStar(__nccwpck_require__(2037)); +const utils_1 = __nccwpck_require__(5278); +/** + * 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 + +/***/ }), + +/***/ 2186: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +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()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.getIDToken = exports.getState = exports.saveState = exports.group = exports.endGroup = exports.startGroup = exports.info = exports.notice = exports.warning = exports.error = exports.debug = exports.isDebug = exports.setFailed = exports.setCommandEcho = exports.setOutput = exports.getBooleanInput = exports.getMultilineInput = exports.getInput = exports.addPath = exports.setSecret = exports.exportVariable = exports.ExitCode = void 0; +const command_1 = __nccwpck_require__(7351); +const file_command_1 = __nccwpck_require__(717); +const utils_1 = __nccwpck_require__(5278); +const os = __importStar(__nccwpck_require__(2037)); +const path = __importStar(__nccwpck_require__(1017)); +const oidc_utils_1 = __nccwpck_require__(8041); +/** + * 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) { + return file_command_1.issueFileCommand('ENV', file_command_1.prepareKeyValueMessage(name, val)); + } + 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.issueFileCommand('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. + * Unless trimWhitespace is set to false in InputOptions, the value is also trimmed. + * Returns an empty string if the value is not defined. + * + * @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}`); + } + if (options && options.trimWhitespace === false) { + return val; + } + return val.trim(); +} +exports.getInput = getInput; +/** + * Gets the values of an multiline input. Each value is also trimmed. + * + * @param name name of the input to get + * @param options optional. See InputOptions. + * @returns string[] + * + */ +function getMultilineInput(name, options) { + const inputs = getInput(name, options) + .split('\n') + .filter(x => x !== ''); + if (options && options.trimWhitespace === false) { + return inputs; + } + return inputs.map(input => input.trim()); +} +exports.getMultilineInput = getMultilineInput; +/** + * Gets the input value of the boolean type in the YAML 1.2 "core schema" specification. + * Support boolean input list: `true | True | TRUE | false | False | FALSE` . + * The return value is also in boolean type. + * ref: https://yaml.org/spec/1.2/spec.html#id2804923 + * + * @param name name of the input to get + * @param options optional. See InputOptions. + * @returns boolean + */ +function getBooleanInput(name, options) { + const trueValue = ['true', 'True', 'TRUE']; + const falseValue = ['false', 'False', 'FALSE']; + const val = getInput(name, options); + if (trueValue.includes(val)) + return true; + if (falseValue.includes(val)) + return false; + throw new TypeError(`Input does not meet YAML 1.2 "Core Schema" specification: ${name}\n` + + `Support boolean input list: \`true | True | TRUE | false | False | FALSE\``); +} +exports.getBooleanInput = getBooleanInput; +/** + * 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) { + const filePath = process.env['GITHUB_OUTPUT'] || ''; + if (filePath) { + return file_command_1.issueFileCommand('OUTPUT', file_command_1.prepareKeyValueMessage(name, value)); + } + process.stdout.write(os.EOL); + command_1.issueCommand('set-output', { name }, utils_1.toCommandValue(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() + * @param properties optional properties to add to the annotation. + */ +function error(message, properties = {}) { + command_1.issueCommand('error', utils_1.toCommandProperties(properties), message instanceof Error ? message.toString() : message); +} +exports.error = error; +/** + * Adds a warning issue + * @param message warning issue message. Errors will be converted to string via toString() + * @param properties optional properties to add to the annotation. + */ +function warning(message, properties = {}) { + command_1.issueCommand('warning', utils_1.toCommandProperties(properties), message instanceof Error ? message.toString() : message); +} +exports.warning = warning; +/** + * Adds a notice issue + * @param message notice issue message. Errors will be converted to string via toString() + * @param properties optional properties to add to the annotation. + */ +function notice(message, properties = {}) { + command_1.issueCommand('notice', utils_1.toCommandProperties(properties), message instanceof Error ? message.toString() : message); +} +exports.notice = notice; +/** + * 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) { + const filePath = process.env['GITHUB_STATE'] || ''; + if (filePath) { + return file_command_1.issueFileCommand('STATE', file_command_1.prepareKeyValueMessage(name, value)); + } + command_1.issueCommand('save-state', { name }, utils_1.toCommandValue(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; +function getIDToken(aud) { + return __awaiter(this, void 0, void 0, function* () { + return yield oidc_utils_1.OidcClient.getIDToken(aud); + }); +} +exports.getIDToken = getIDToken; +/** + * Summary exports + */ +var summary_1 = __nccwpck_require__(1327); +Object.defineProperty(exports, "summary", ({ enumerable: true, get: function () { return summary_1.summary; } })); +/** + * @deprecated use core.summary + */ +var summary_2 = __nccwpck_require__(1327); +Object.defineProperty(exports, "markdownSummary", ({ enumerable: true, get: function () { return summary_2.markdownSummary; } })); +/** + * Path exports + */ +var path_utils_1 = __nccwpck_require__(2981); +Object.defineProperty(exports, "toPosixPath", ({ enumerable: true, get: function () { return path_utils_1.toPosixPath; } })); +Object.defineProperty(exports, "toWin32Path", ({ enumerable: true, get: function () { return path_utils_1.toWin32Path; } })); +Object.defineProperty(exports, "toPlatformPath", ({ enumerable: true, get: function () { return path_utils_1.toPlatformPath; } })); +//# sourceMappingURL=core.js.map + +/***/ }), + +/***/ 717: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +// For internal use, subject to change. +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.prepareKeyValueMessage = exports.issueFileCommand = void 0; +// We use any as a valid input type +/* eslint-disable @typescript-eslint/no-explicit-any */ +const fs = __importStar(__nccwpck_require__(7147)); +const os = __importStar(__nccwpck_require__(2037)); +const uuid_1 = __nccwpck_require__(5840); +const utils_1 = __nccwpck_require__(5278); +function issueFileCommand(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.issueFileCommand = issueFileCommand; +function prepareKeyValueMessage(key, value) { + const delimiter = `ghadelimiter_${uuid_1.v4()}`; + const convertedValue = utils_1.toCommandValue(value); + // These should realistically never happen, but just in case someone finds a + // way to exploit uuid generation let's not allow keys or values that contain + // the delimiter. + if (key.includes(delimiter)) { + throw new Error(`Unexpected input: name should not contain the delimiter "${delimiter}"`); + } + if (convertedValue.includes(delimiter)) { + throw new Error(`Unexpected input: value should not contain the delimiter "${delimiter}"`); + } + return `${key}<<${delimiter}${os.EOL}${convertedValue}${os.EOL}${delimiter}`; +} +exports.prepareKeyValueMessage = prepareKeyValueMessage; +//# sourceMappingURL=file-command.js.map + +/***/ }), + +/***/ 8041: +/***/ (function(__unused_webpack_module, exports, __nccwpck_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()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.OidcClient = void 0; +const http_client_1 = __nccwpck_require__(6255); +const auth_1 = __nccwpck_require__(5526); +const core_1 = __nccwpck_require__(2186); +class OidcClient { + static createHttpClient(allowRetry = true, maxRetry = 10) { + const requestOptions = { + allowRetries: allowRetry, + maxRetries: maxRetry + }; + return new http_client_1.HttpClient('actions/oidc-client', [new auth_1.BearerCredentialHandler(OidcClient.getRequestToken())], requestOptions); + } + static getRequestToken() { + const token = process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN']; + if (!token) { + throw new Error('Unable to get ACTIONS_ID_TOKEN_REQUEST_TOKEN env variable'); + } + return token; + } + static getIDTokenUrl() { + const runtimeUrl = process.env['ACTIONS_ID_TOKEN_REQUEST_URL']; + if (!runtimeUrl) { + throw new Error('Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variable'); + } + return runtimeUrl; + } + static getCall(id_token_url) { + var _a; + return __awaiter(this, void 0, void 0, function* () { + const httpclient = OidcClient.createHttpClient(); + const res = yield httpclient + .getJson(id_token_url) + .catch(error => { + throw new Error(`Failed to get ID Token. \n + Error Code : ${error.statusCode}\n + Error Message: ${error.message}`); + }); + const id_token = (_a = res.result) === null || _a === void 0 ? void 0 : _a.value; + if (!id_token) { + throw new Error('Response json body do not have ID Token field'); + } + return id_token; + }); + } + static getIDToken(audience) { + return __awaiter(this, void 0, void 0, function* () { + try { + // New ID Token is requested from action service + let id_token_url = OidcClient.getIDTokenUrl(); + if (audience) { + const encodedAudience = encodeURIComponent(audience); + id_token_url = `${id_token_url}&audience=${encodedAudience}`; + } + core_1.debug(`ID token url is ${id_token_url}`); + const id_token = yield OidcClient.getCall(id_token_url); + core_1.setSecret(id_token); + return id_token; + } + catch (error) { + throw new Error(`Error message: ${error.message}`); + } + }); + } +} +exports.OidcClient = OidcClient; +//# sourceMappingURL=oidc-utils.js.map + +/***/ }), + +/***/ 2981: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.toPlatformPath = exports.toWin32Path = exports.toPosixPath = void 0; +const path = __importStar(__nccwpck_require__(1017)); +/** + * toPosixPath converts the given path to the posix form. On Windows, \\ will be + * replaced with /. + * + * @param pth. Path to transform. + * @return string Posix path. + */ +function toPosixPath(pth) { + return pth.replace(/[\\]/g, '/'); +} +exports.toPosixPath = toPosixPath; +/** + * toWin32Path converts the given path to the win32 form. On Linux, / will be + * replaced with \\. + * + * @param pth. Path to transform. + * @return string Win32 path. + */ +function toWin32Path(pth) { + return pth.replace(/[/]/g, '\\'); +} +exports.toWin32Path = toWin32Path; +/** + * toPlatformPath converts the given path to a platform-specific path. It does + * this by replacing instances of / and \ with the platform-specific path + * separator. + * + * @param pth The path to platformize. + * @return string The platform-specific path. + */ +function toPlatformPath(pth) { + return pth.replace(/[/\\]/g, path.sep); +} +exports.toPlatformPath = toPlatformPath; +//# sourceMappingURL=path-utils.js.map + +/***/ }), + +/***/ 1327: +/***/ (function(__unused_webpack_module, exports, __nccwpck_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()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.summary = exports.markdownSummary = exports.SUMMARY_DOCS_URL = exports.SUMMARY_ENV_VAR = void 0; +const os_1 = __nccwpck_require__(2037); +const fs_1 = __nccwpck_require__(7147); +const { access, appendFile, writeFile } = fs_1.promises; +exports.SUMMARY_ENV_VAR = 'GITHUB_STEP_SUMMARY'; +exports.SUMMARY_DOCS_URL = 'https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary'; +class Summary { + constructor() { + this._buffer = ''; + } + /** + * Finds the summary file path from the environment, rejects if env var is not found or file does not exist + * Also checks r/w permissions. + * + * @returns step summary file path + */ + filePath() { + return __awaiter(this, void 0, void 0, function* () { + if (this._filePath) { + return this._filePath; + } + const pathFromEnv = process.env[exports.SUMMARY_ENV_VAR]; + if (!pathFromEnv) { + throw new Error(`Unable to find environment variable for $${exports.SUMMARY_ENV_VAR}. Check if your runtime environment supports job summaries.`); + } + try { + yield access(pathFromEnv, fs_1.constants.R_OK | fs_1.constants.W_OK); + } + catch (_a) { + throw new Error(`Unable to access summary file: '${pathFromEnv}'. Check if the file has correct read/write permissions.`); + } + this._filePath = pathFromEnv; + return this._filePath; + }); + } + /** + * Wraps content in an HTML tag, adding any HTML attributes + * + * @param {string} tag HTML tag to wrap + * @param {string | null} content content within the tag + * @param {[attribute: string]: string} attrs key-value list of HTML attributes to add + * + * @returns {string} content wrapped in HTML element + */ + wrap(tag, content, attrs = {}) { + const htmlAttrs = Object.entries(attrs) + .map(([key, value]) => ` ${key}="${value}"`) + .join(''); + if (!content) { + return `<${tag}${htmlAttrs}>`; + } + return `<${tag}${htmlAttrs}>${content}`; + } + /** + * Writes text in the buffer to the summary buffer file and empties buffer. Will append by default. + * + * @param {SummaryWriteOptions} [options] (optional) options for write operation + * + * @returns {Promise} summary instance + */ + write(options) { + return __awaiter(this, void 0, void 0, function* () { + const overwrite = !!(options === null || options === void 0 ? void 0 : options.overwrite); + const filePath = yield this.filePath(); + const writeFunc = overwrite ? writeFile : appendFile; + yield writeFunc(filePath, this._buffer, { encoding: 'utf8' }); + return this.emptyBuffer(); + }); + } + /** + * Clears the summary buffer and wipes the summary file + * + * @returns {Summary} summary instance + */ + clear() { + return __awaiter(this, void 0, void 0, function* () { + return this.emptyBuffer().write({ overwrite: true }); + }); + } + /** + * Returns the current summary buffer as a string + * + * @returns {string} string of summary buffer + */ + stringify() { + return this._buffer; + } + /** + * If the summary buffer is empty + * + * @returns {boolen} true if the buffer is empty + */ + isEmptyBuffer() { + return this._buffer.length === 0; + } + /** + * Resets the summary buffer without writing to summary file + * + * @returns {Summary} summary instance + */ + emptyBuffer() { + this._buffer = ''; + return this; + } + /** + * Adds raw text to the summary buffer + * + * @param {string} text content to add + * @param {boolean} [addEOL=false] (optional) append an EOL to the raw text (default: false) + * + * @returns {Summary} summary instance + */ + addRaw(text, addEOL = false) { + this._buffer += text; + return addEOL ? this.addEOL() : this; + } + /** + * Adds the operating system-specific end-of-line marker to the buffer + * + * @returns {Summary} summary instance + */ + addEOL() { + return this.addRaw(os_1.EOL); + } + /** + * Adds an HTML codeblock to the summary buffer + * + * @param {string} code content to render within fenced code block + * @param {string} lang (optional) language to syntax highlight code + * + * @returns {Summary} summary instance + */ + addCodeBlock(code, lang) { + const attrs = Object.assign({}, (lang && { lang })); + const element = this.wrap('pre', this.wrap('code', code), attrs); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML list to the summary buffer + * + * @param {string[]} items list of items to render + * @param {boolean} [ordered=false] (optional) if the rendered list should be ordered or not (default: false) + * + * @returns {Summary} summary instance + */ + addList(items, ordered = false) { + const tag = ordered ? 'ol' : 'ul'; + const listItems = items.map(item => this.wrap('li', item)).join(''); + const element = this.wrap(tag, listItems); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML table to the summary buffer + * + * @param {SummaryTableCell[]} rows table rows + * + * @returns {Summary} summary instance + */ + addTable(rows) { + const tableBody = rows + .map(row => { + const cells = row + .map(cell => { + if (typeof cell === 'string') { + return this.wrap('td', cell); + } + const { header, data, colspan, rowspan } = cell; + const tag = header ? 'th' : 'td'; + const attrs = Object.assign(Object.assign({}, (colspan && { colspan })), (rowspan && { rowspan })); + return this.wrap(tag, data, attrs); + }) + .join(''); + return this.wrap('tr', cells); + }) + .join(''); + const element = this.wrap('table', tableBody); + return this.addRaw(element).addEOL(); + } + /** + * Adds a collapsable HTML details element to the summary buffer + * + * @param {string} label text for the closed state + * @param {string} content collapsable content + * + * @returns {Summary} summary instance + */ + addDetails(label, content) { + const element = this.wrap('details', this.wrap('summary', label) + content); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML image tag to the summary buffer + * + * @param {string} src path to the image you to embed + * @param {string} alt text description of the image + * @param {SummaryImageOptions} options (optional) addition image attributes + * + * @returns {Summary} summary instance + */ + addImage(src, alt, options) { + const { width, height } = options || {}; + const attrs = Object.assign(Object.assign({}, (width && { width })), (height && { height })); + const element = this.wrap('img', null, Object.assign({ src, alt }, attrs)); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML section heading element + * + * @param {string} text heading text + * @param {number | string} [level=1] (optional) the heading level, default: 1 + * + * @returns {Summary} summary instance + */ + addHeading(text, level) { + const tag = `h${level}`; + const allowedTag = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tag) + ? tag + : 'h1'; + const element = this.wrap(allowedTag, text); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML thematic break (
) to the summary buffer + * + * @returns {Summary} summary instance + */ + addSeparator() { + const element = this.wrap('hr', null); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML line break (
) to the summary buffer + * + * @returns {Summary} summary instance + */ + addBreak() { + const element = this.wrap('br', null); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML blockquote to the summary buffer + * + * @param {string} text quote text + * @param {string} cite (optional) citation url + * + * @returns {Summary} summary instance + */ + addQuote(text, cite) { + const attrs = Object.assign({}, (cite && { cite })); + const element = this.wrap('blockquote', text, attrs); + return this.addRaw(element).addEOL(); + } + /** + * Adds an HTML anchor tag to the summary buffer + * + * @param {string} text link text/content + * @param {string} href hyperlink + * + * @returns {Summary} summary instance + */ + addLink(text, href) { + const element = this.wrap('a', text, { href }); + return this.addRaw(element).addEOL(); + } +} +const _summary = new Summary(); +/** + * @deprecated use `core.summary` + */ +exports.markdownSummary = _summary; +exports.summary = _summary; +//# sourceMappingURL=summary.js.map + +/***/ }), + +/***/ 5278: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +// We use any as a valid input type +/* eslint-disable @typescript-eslint/no-explicit-any */ +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.toCommandProperties = exports.toCommandValue = void 0; +/** + * 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; +/** + * + * @param annotationProperties + * @returns The command properties to send with the actual annotation command + * See IssueCommandProperties: https://github.com/actions/runner/blob/main/src/Runner.Worker/ActionCommandManager.cs#L646 + */ +function toCommandProperties(annotationProperties) { + if (!Object.keys(annotationProperties).length) { + return {}; + } + return { + title: annotationProperties.title, + file: annotationProperties.file, + line: annotationProperties.startLine, + endLine: annotationProperties.endLine, + col: annotationProperties.startColumn, + endColumn: annotationProperties.endColumn + }; +} +exports.toCommandProperties = toCommandProperties; +//# sourceMappingURL=utils.js.map + +/***/ }), + +/***/ 5526: +/***/ (function(__unused_webpack_module, exports) { + +"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()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.PersonalAccessTokenCredentialHandler = exports.BearerCredentialHandler = exports.BasicCredentialHandler = void 0; +class BasicCredentialHandler { + constructor(username, password) { + this.username = username; + this.password = password; + } + prepareRequest(options) { + if (!options.headers) { + throw Error('The request has no headers'); + } + options.headers['Authorization'] = `Basic ${Buffer.from(`${this.username}:${this.password}`).toString('base64')}`; + } + // This handler cannot handle 401 + canHandleAuthentication() { + return false; + } + handleAuthentication() { + return __awaiter(this, void 0, void 0, function* () { + throw new Error('not implemented'); + }); + } +} +exports.BasicCredentialHandler = BasicCredentialHandler; +class BearerCredentialHandler { + constructor(token) { + this.token = token; + } + // currently implements pre-authorization + // TODO: support preAuth = false where it hooks on 401 + prepareRequest(options) { + if (!options.headers) { + throw Error('The request has no headers'); + } + options.headers['Authorization'] = `Bearer ${this.token}`; + } + // This handler cannot handle 401 + canHandleAuthentication() { + return false; + } + handleAuthentication() { + return __awaiter(this, void 0, void 0, function* () { + throw new Error('not implemented'); + }); + } +} +exports.BearerCredentialHandler = BearerCredentialHandler; +class PersonalAccessTokenCredentialHandler { + constructor(token) { + this.token = token; + } + // currently implements pre-authorization + // TODO: support preAuth = false where it hooks on 401 + prepareRequest(options) { + if (!options.headers) { + throw Error('The request has no headers'); + } + options.headers['Authorization'] = `Basic ${Buffer.from(`PAT:${this.token}`).toString('base64')}`; + } + // This handler cannot handle 401 + canHandleAuthentication() { + return false; + } + handleAuthentication() { + return __awaiter(this, void 0, void 0, function* () { + throw new Error('not implemented'); + }); + } +} +exports.PersonalAccessTokenCredentialHandler = PersonalAccessTokenCredentialHandler; +//# sourceMappingURL=auth.js.map + +/***/ }), + +/***/ 6255: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +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()); + }); +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.HttpClient = exports.isHttps = exports.HttpClientResponse = exports.HttpClientError = exports.getProxyUrl = exports.MediaTypes = exports.Headers = exports.HttpCodes = void 0; +const http = __importStar(__nccwpck_require__(3685)); +const https = __importStar(__nccwpck_require__(5687)); +const pm = __importStar(__nccwpck_require__(9835)); +const tunnel = __importStar(__nccwpck_require__(4294)); +const undici_1 = __nccwpck_require__(1773); +var HttpCodes; +(function (HttpCodes) { + HttpCodes[HttpCodes["OK"] = 200] = "OK"; + HttpCodes[HttpCodes["MultipleChoices"] = 300] = "MultipleChoices"; + HttpCodes[HttpCodes["MovedPermanently"] = 301] = "MovedPermanently"; + HttpCodes[HttpCodes["ResourceMoved"] = 302] = "ResourceMoved"; + HttpCodes[HttpCodes["SeeOther"] = 303] = "SeeOther"; + HttpCodes[HttpCodes["NotModified"] = 304] = "NotModified"; + HttpCodes[HttpCodes["UseProxy"] = 305] = "UseProxy"; + HttpCodes[HttpCodes["SwitchProxy"] = 306] = "SwitchProxy"; + HttpCodes[HttpCodes["TemporaryRedirect"] = 307] = "TemporaryRedirect"; + HttpCodes[HttpCodes["PermanentRedirect"] = 308] = "PermanentRedirect"; + HttpCodes[HttpCodes["BadRequest"] = 400] = "BadRequest"; + HttpCodes[HttpCodes["Unauthorized"] = 401] = "Unauthorized"; + HttpCodes[HttpCodes["PaymentRequired"] = 402] = "PaymentRequired"; + HttpCodes[HttpCodes["Forbidden"] = 403] = "Forbidden"; + HttpCodes[HttpCodes["NotFound"] = 404] = "NotFound"; + HttpCodes[HttpCodes["MethodNotAllowed"] = 405] = "MethodNotAllowed"; + HttpCodes[HttpCodes["NotAcceptable"] = 406] = "NotAcceptable"; + HttpCodes[HttpCodes["ProxyAuthenticationRequired"] = 407] = "ProxyAuthenticationRequired"; + HttpCodes[HttpCodes["RequestTimeout"] = 408] = "RequestTimeout"; + HttpCodes[HttpCodes["Conflict"] = 409] = "Conflict"; + HttpCodes[HttpCodes["Gone"] = 410] = "Gone"; + HttpCodes[HttpCodes["TooManyRequests"] = 429] = "TooManyRequests"; + HttpCodes[HttpCodes["InternalServerError"] = 500] = "InternalServerError"; + HttpCodes[HttpCodes["NotImplemented"] = 501] = "NotImplemented"; + HttpCodes[HttpCodes["BadGateway"] = 502] = "BadGateway"; + HttpCodes[HttpCodes["ServiceUnavailable"] = 503] = "ServiceUnavailable"; + HttpCodes[HttpCodes["GatewayTimeout"] = 504] = "GatewayTimeout"; +})(HttpCodes || (exports.HttpCodes = HttpCodes = {})); +var Headers; +(function (Headers) { + Headers["Accept"] = "accept"; + Headers["ContentType"] = "content-type"; +})(Headers || (exports.Headers = Headers = {})); +var MediaTypes; +(function (MediaTypes) { + MediaTypes["ApplicationJson"] = "application/json"; +})(MediaTypes || (exports.MediaTypes = MediaTypes = {})); +/** + * Returns the proxy URL, depending upon the supplied url and proxy environment variables. + * @param serverUrl The server URL where the request will be sent. For example, https://api.github.com + */ +function getProxyUrl(serverUrl) { + const proxyUrl = pm.getProxyUrl(new URL(serverUrl)); + return proxyUrl ? proxyUrl.href : ''; +} +exports.getProxyUrl = getProxyUrl; +const HttpRedirectCodes = [ + HttpCodes.MovedPermanently, + HttpCodes.ResourceMoved, + HttpCodes.SeeOther, + HttpCodes.TemporaryRedirect, + HttpCodes.PermanentRedirect +]; +const HttpResponseRetryCodes = [ + HttpCodes.BadGateway, + HttpCodes.ServiceUnavailable, + HttpCodes.GatewayTimeout +]; +const RetryableHttpVerbs = ['OPTIONS', 'GET', 'DELETE', 'HEAD']; +const ExponentialBackoffCeiling = 10; +const ExponentialBackoffTimeSlice = 5; +class HttpClientError extends Error { + constructor(message, statusCode) { + super(message); + this.name = 'HttpClientError'; + this.statusCode = statusCode; + Object.setPrototypeOf(this, HttpClientError.prototype); + } +} +exports.HttpClientError = HttpClientError; +class HttpClientResponse { + constructor(message) { + this.message = message; + } + readBody() { + return __awaiter(this, void 0, void 0, function* () { + return new Promise((resolve) => __awaiter(this, void 0, void 0, function* () { + let output = Buffer.alloc(0); + this.message.on('data', (chunk) => { + output = Buffer.concat([output, chunk]); + }); + this.message.on('end', () => { + resolve(output.toString()); + }); + })); + }); + } + readBodyBuffer() { + return __awaiter(this, void 0, void 0, function* () { + return new Promise((resolve) => __awaiter(this, void 0, void 0, function* () { + const chunks = []; + this.message.on('data', (chunk) => { + chunks.push(chunk); + }); + this.message.on('end', () => { + resolve(Buffer.concat(chunks)); + }); + })); + }); + } +} +exports.HttpClientResponse = HttpClientResponse; +function isHttps(requestUrl) { + const parsedUrl = new URL(requestUrl); + return parsedUrl.protocol === 'https:'; +} +exports.isHttps = isHttps; +class HttpClient { + constructor(userAgent, handlers, requestOptions) { + this._ignoreSslError = false; + this._allowRedirects = true; + this._allowRedirectDowngrade = false; + this._maxRedirects = 50; + this._allowRetries = false; + this._maxRetries = 1; + this._keepAlive = false; + this._disposed = false; + this.userAgent = userAgent; + this.handlers = handlers || []; + this.requestOptions = requestOptions; + if (requestOptions) { + if (requestOptions.ignoreSslError != null) { + this._ignoreSslError = requestOptions.ignoreSslError; + } + this._socketTimeout = requestOptions.socketTimeout; + if (requestOptions.allowRedirects != null) { + this._allowRedirects = requestOptions.allowRedirects; + } + if (requestOptions.allowRedirectDowngrade != null) { + this._allowRedirectDowngrade = requestOptions.allowRedirectDowngrade; + } + if (requestOptions.maxRedirects != null) { + this._maxRedirects = Math.max(requestOptions.maxRedirects, 0); + } + if (requestOptions.keepAlive != null) { + this._keepAlive = requestOptions.keepAlive; + } + if (requestOptions.allowRetries != null) { + this._allowRetries = requestOptions.allowRetries; + } + if (requestOptions.maxRetries != null) { + this._maxRetries = requestOptions.maxRetries; + } + } + } + options(requestUrl, additionalHeaders) { + return __awaiter(this, void 0, void 0, function* () { + return this.request('OPTIONS', requestUrl, null, additionalHeaders || {}); + }); + } + get(requestUrl, additionalHeaders) { + return __awaiter(this, void 0, void 0, function* () { + return this.request('GET', requestUrl, null, additionalHeaders || {}); + }); + } + del(requestUrl, additionalHeaders) { + return __awaiter(this, void 0, void 0, function* () { + return this.request('DELETE', requestUrl, null, additionalHeaders || {}); + }); + } + post(requestUrl, data, additionalHeaders) { + return __awaiter(this, void 0, void 0, function* () { + return this.request('POST', requestUrl, data, additionalHeaders || {}); + }); + } + patch(requestUrl, data, additionalHeaders) { + return __awaiter(this, void 0, void 0, function* () { + return this.request('PATCH', requestUrl, data, additionalHeaders || {}); + }); + } + put(requestUrl, data, additionalHeaders) { + return __awaiter(this, void 0, void 0, function* () { + return this.request('PUT', requestUrl, data, additionalHeaders || {}); + }); + } + head(requestUrl, additionalHeaders) { + return __awaiter(this, void 0, void 0, function* () { + return this.request('HEAD', requestUrl, null, additionalHeaders || {}); + }); + } + sendStream(verb, requestUrl, stream, additionalHeaders) { + return __awaiter(this, void 0, void 0, function* () { + return this.request(verb, requestUrl, stream, additionalHeaders); + }); + } + /** + * Gets a typed object from an endpoint + * Be aware that not found returns a null. Other errors (4xx, 5xx) reject the promise + */ + getJson(requestUrl, additionalHeaders = {}) { + return __awaiter(this, void 0, void 0, function* () { + additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson); + const res = yield this.get(requestUrl, additionalHeaders); + return this._processResponse(res, this.requestOptions); + }); + } + postJson(requestUrl, obj, additionalHeaders = {}) { + return __awaiter(this, void 0, void 0, function* () { + const data = JSON.stringify(obj, null, 2); + additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson); + additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.ContentType, MediaTypes.ApplicationJson); + const res = yield this.post(requestUrl, data, additionalHeaders); + return this._processResponse(res, this.requestOptions); + }); + } + putJson(requestUrl, obj, additionalHeaders = {}) { + return __awaiter(this, void 0, void 0, function* () { + const data = JSON.stringify(obj, null, 2); + additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson); + additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.ContentType, MediaTypes.ApplicationJson); + const res = yield this.put(requestUrl, data, additionalHeaders); + return this._processResponse(res, this.requestOptions); + }); + } + patchJson(requestUrl, obj, additionalHeaders = {}) { + return __awaiter(this, void 0, void 0, function* () { + const data = JSON.stringify(obj, null, 2); + additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.Accept, MediaTypes.ApplicationJson); + additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader(additionalHeaders, Headers.ContentType, MediaTypes.ApplicationJson); + const res = yield this.patch(requestUrl, data, additionalHeaders); + return this._processResponse(res, this.requestOptions); + }); + } + /** + * Makes a raw http request. + * All other methods such as get, post, patch, and request ultimately call this. + * Prefer get, del, post and patch + */ + request(verb, requestUrl, data, headers) { + return __awaiter(this, void 0, void 0, function* () { + if (this._disposed) { + throw new Error('Client has already been disposed.'); + } + const parsedUrl = new URL(requestUrl); + let info = this._prepareRequest(verb, parsedUrl, headers); + // Only perform retries on reads since writes may not be idempotent. + const maxTries = this._allowRetries && RetryableHttpVerbs.includes(verb) + ? this._maxRetries + 1 + : 1; + let numTries = 0; + let response; + do { + response = yield this.requestRaw(info, data); + // Check if it's an authentication challenge + if (response && + response.message && + response.message.statusCode === HttpCodes.Unauthorized) { + let authenticationHandler; + for (const handler of this.handlers) { + if (handler.canHandleAuthentication(response)) { + authenticationHandler = handler; + break; + } + } + if (authenticationHandler) { + return authenticationHandler.handleAuthentication(this, info, data); + } + else { + // We have received an unauthorized response but have no handlers to handle it. + // Let the response return to the caller. + return response; + } + } + let redirectsRemaining = this._maxRedirects; + while (response.message.statusCode && + HttpRedirectCodes.includes(response.message.statusCode) && + this._allowRedirects && + redirectsRemaining > 0) { + const redirectUrl = response.message.headers['location']; + if (!redirectUrl) { + // if there's no location to redirect to, we won't + break; + } + const parsedRedirectUrl = new URL(redirectUrl); + if (parsedUrl.protocol === 'https:' && + parsedUrl.protocol !== parsedRedirectUrl.protocol && + !this._allowRedirectDowngrade) { + throw new Error('Redirect from HTTPS to HTTP protocol. This downgrade is not allowed for security reasons. If you want to allow this behavior, set the allowRedirectDowngrade option to true.'); + } + // we need to finish reading the response before reassigning response + // which will leak the open socket. + yield response.readBody(); + // strip authorization header if redirected to a different hostname + if (parsedRedirectUrl.hostname !== parsedUrl.hostname) { + for (const header in headers) { + // header names are case insensitive + if (header.toLowerCase() === 'authorization') { + delete headers[header]; + } + } + } + // let's make the request with the new redirectUrl + info = this._prepareRequest(verb, parsedRedirectUrl, headers); + response = yield this.requestRaw(info, data); + redirectsRemaining--; + } + if (!response.message.statusCode || + !HttpResponseRetryCodes.includes(response.message.statusCode)) { + // If not a retry code, return immediately instead of retrying + return response; + } + numTries += 1; + if (numTries < maxTries) { + yield response.readBody(); + yield this._performExponentialBackoff(numTries); + } + } while (numTries < maxTries); + return response; + }); + } + /** + * Needs to be called if keepAlive is set to true in request options. + */ + dispose() { + if (this._agent) { + this._agent.destroy(); + } + this._disposed = true; + } + /** + * Raw request. + * @param info + * @param data + */ + requestRaw(info, data) { + return __awaiter(this, void 0, void 0, function* () { + return new Promise((resolve, reject) => { + function callbackForResult(err, res) { + if (err) { + reject(err); + } + else if (!res) { + // If `err` is not passed, then `res` must be passed. + reject(new Error('Unknown error')); + } + else { + resolve(res); + } + } + this.requestRawWithCallback(info, data, callbackForResult); + }); + }); + } + /** + * Raw request with callback. + * @param info + * @param data + * @param onResult + */ + requestRawWithCallback(info, data, onResult) { + if (typeof data === 'string') { + if (!info.options.headers) { + info.options.headers = {}; + } + info.options.headers['Content-Length'] = Buffer.byteLength(data, 'utf8'); + } + let callbackCalled = false; + function handleResult(err, res) { + if (!callbackCalled) { + callbackCalled = true; + onResult(err, res); + } + } + const req = info.httpModule.request(info.options, (msg) => { + const res = new HttpClientResponse(msg); + handleResult(undefined, res); + }); + let socket; + req.on('socket', sock => { + socket = sock; + }); + // If we ever get disconnected, we want the socket to timeout eventually + req.setTimeout(this._socketTimeout || 3 * 60000, () => { + if (socket) { + socket.end(); + } + handleResult(new Error(`Request timeout: ${info.options.path}`)); + }); + req.on('error', function (err) { + // err has statusCode property + // res should have headers + handleResult(err); + }); + if (data && typeof data === 'string') { + req.write(data, 'utf8'); + } + if (data && typeof data !== 'string') { + data.on('close', function () { + req.end(); + }); + data.pipe(req); + } + else { + req.end(); + } + } + /** + * Gets an http agent. This function is useful when you need an http agent that handles + * routing through a proxy server - depending upon the url and proxy environment variables. + * @param serverUrl The server URL where the request will be sent. For example, https://api.github.com + */ + getAgent(serverUrl) { + const parsedUrl = new URL(serverUrl); + return this._getAgent(parsedUrl); + } + getAgentDispatcher(serverUrl) { + const parsedUrl = new URL(serverUrl); + const proxyUrl = pm.getProxyUrl(parsedUrl); + const useProxy = proxyUrl && proxyUrl.hostname; + if (!useProxy) { + return; + } + return this._getProxyAgentDispatcher(parsedUrl, proxyUrl); + } + _prepareRequest(method, requestUrl, headers) { + const info = {}; + info.parsedUrl = requestUrl; + const usingSsl = info.parsedUrl.protocol === 'https:'; + info.httpModule = usingSsl ? https : http; + const defaultPort = usingSsl ? 443 : 80; + info.options = {}; + info.options.host = info.parsedUrl.hostname; + info.options.port = info.parsedUrl.port + ? parseInt(info.parsedUrl.port) + : defaultPort; + info.options.path = + (info.parsedUrl.pathname || '') + (info.parsedUrl.search || ''); + info.options.method = method; + info.options.headers = this._mergeHeaders(headers); + if (this.userAgent != null) { + info.options.headers['user-agent'] = this.userAgent; + } + info.options.agent = this._getAgent(info.parsedUrl); + // gives handlers an opportunity to participate + if (this.handlers) { + for (const handler of this.handlers) { + handler.prepareRequest(info.options); + } + } + return info; + } + _mergeHeaders(headers) { + if (this.requestOptions && this.requestOptions.headers) { + return Object.assign({}, lowercaseKeys(this.requestOptions.headers), lowercaseKeys(headers || {})); + } + return lowercaseKeys(headers || {}); + } + _getExistingOrDefaultHeader(additionalHeaders, header, _default) { + let clientHeader; + if (this.requestOptions && this.requestOptions.headers) { + clientHeader = lowercaseKeys(this.requestOptions.headers)[header]; + } + return additionalHeaders[header] || clientHeader || _default; + } + _getAgent(parsedUrl) { + let agent; + const proxyUrl = pm.getProxyUrl(parsedUrl); + const useProxy = proxyUrl && proxyUrl.hostname; + if (this._keepAlive && useProxy) { + agent = this._proxyAgent; + } + if (this._keepAlive && !useProxy) { + agent = this._agent; + } + // if agent is already assigned use that agent. + if (agent) { + return agent; + } + const usingSsl = parsedUrl.protocol === 'https:'; + let maxSockets = 100; + if (this.requestOptions) { + maxSockets = this.requestOptions.maxSockets || http.globalAgent.maxSockets; + } + // This is `useProxy` again, but we need to check `proxyURl` directly for TypeScripts's flow analysis. + if (proxyUrl && proxyUrl.hostname) { + const agentOptions = { + maxSockets, + keepAlive: this._keepAlive, + proxy: Object.assign(Object.assign({}, ((proxyUrl.username || proxyUrl.password) && { + proxyAuth: `${proxyUrl.username}:${proxyUrl.password}` + })), { host: proxyUrl.hostname, port: proxyUrl.port }) + }; + let tunnelAgent; + const overHttps = proxyUrl.protocol === 'https:'; + if (usingSsl) { + tunnelAgent = overHttps ? tunnel.httpsOverHttps : tunnel.httpsOverHttp; + } + else { + tunnelAgent = overHttps ? tunnel.httpOverHttps : tunnel.httpOverHttp; + } + agent = tunnelAgent(agentOptions); + this._proxyAgent = agent; + } + // if reusing agent across request and tunneling agent isn't assigned create a new agent + if (this._keepAlive && !agent) { + const options = { keepAlive: this._keepAlive, maxSockets }; + agent = usingSsl ? new https.Agent(options) : new http.Agent(options); + this._agent = agent; + } + // if not using private agent and tunnel agent isn't setup then use global agent + if (!agent) { + agent = usingSsl ? https.globalAgent : http.globalAgent; + } + if (usingSsl && this._ignoreSslError) { + // we don't want to set NODE_TLS_REJECT_UNAUTHORIZED=0 since that will affect request for entire process + // http.RequestOptions doesn't expose a way to modify RequestOptions.agent.options + // we have to cast it to any and change it directly + agent.options = Object.assign(agent.options || {}, { + rejectUnauthorized: false + }); + } + return agent; + } + _getProxyAgentDispatcher(parsedUrl, proxyUrl) { + let proxyAgent; + if (this._keepAlive) { + proxyAgent = this._proxyAgentDispatcher; + } + // if agent is already assigned use that agent. + if (proxyAgent) { + return proxyAgent; + } + const usingSsl = parsedUrl.protocol === 'https:'; + proxyAgent = new undici_1.ProxyAgent(Object.assign({ uri: proxyUrl.href, pipelining: !this._keepAlive ? 0 : 1 }, ((proxyUrl.username || proxyUrl.password) && { + token: `${proxyUrl.username}:${proxyUrl.password}` + }))); + this._proxyAgentDispatcher = proxyAgent; + if (usingSsl && this._ignoreSslError) { + // we don't want to set NODE_TLS_REJECT_UNAUTHORIZED=0 since that will affect request for entire process + // http.RequestOptions doesn't expose a way to modify RequestOptions.agent.options + // we have to cast it to any and change it directly + proxyAgent.options = Object.assign(proxyAgent.options.requestTls || {}, { + rejectUnauthorized: false + }); + } + return proxyAgent; + } + _performExponentialBackoff(retryNumber) { + return __awaiter(this, void 0, void 0, function* () { + retryNumber = Math.min(ExponentialBackoffCeiling, retryNumber); + const ms = ExponentialBackoffTimeSlice * Math.pow(2, retryNumber); + return new Promise(resolve => setTimeout(() => resolve(), ms)); + }); + } + _processResponse(res, options) { + return __awaiter(this, void 0, void 0, function* () { + return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () { + const statusCode = res.message.statusCode || 0; + const response = { + statusCode, + result: null, + headers: {} + }; + // not found leads to null obj returned + if (statusCode === HttpCodes.NotFound) { + resolve(response); + } + // get the result from the body + function dateTimeDeserializer(key, value) { + if (typeof value === 'string') { + const a = new Date(value); + if (!isNaN(a.valueOf())) { + return a; + } + } + return value; + } + let obj; + let contents; + try { + contents = yield res.readBody(); + if (contents && contents.length > 0) { + if (options && options.deserializeDates) { + obj = JSON.parse(contents, dateTimeDeserializer); + } + else { + obj = JSON.parse(contents); + } + response.result = obj; + } + response.headers = res.message.headers; + } + catch (err) { + // Invalid resource (contents not json); leaving result obj null + } + // note that 3xx redirects are handled by the http layer. + if (statusCode > 299) { + let msg; + // if exception/error in body, attempt to get better error + if (obj && obj.message) { + msg = obj.message; + } + else if (contents && contents.length > 0) { + // it may be the case that the exception is in the body message as string + msg = contents; + } + else { + msg = `Failed request: (${statusCode})`; + } + const err = new HttpClientError(msg, statusCode); + err.result = response.result; + reject(err); + } + else { + resolve(response); + } + })); + }); + } +} +exports.HttpClient = HttpClient; +const lowercaseKeys = (obj) => Object.keys(obj).reduce((c, k) => ((c[k.toLowerCase()] = obj[k]), c), {}); +//# sourceMappingURL=index.js.map + +/***/ }), + +/***/ 9835: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.checkBypass = exports.getProxyUrl = void 0; +function getProxyUrl(reqUrl) { + const usingSsl = reqUrl.protocol === 'https:'; + if (checkBypass(reqUrl)) { + return undefined; + } + const proxyVar = (() => { + if (usingSsl) { + return process.env['https_proxy'] || process.env['HTTPS_PROXY']; + } + else { + return process.env['http_proxy'] || process.env['HTTP_PROXY']; + } + })(); + if (proxyVar) { + try { + return new URL(proxyVar); + } + catch (_a) { + if (!proxyVar.startsWith('http://') && !proxyVar.startsWith('https://')) + return new URL(`http://${proxyVar}`); + } + } + else { + return undefined; + } +} +exports.getProxyUrl = getProxyUrl; +function checkBypass(reqUrl) { + if (!reqUrl.hostname) { + return false; + } + const reqHost = reqUrl.hostname; + if (isLoopbackAddress(reqHost)) { + return true; + } + const noProxy = process.env['no_proxy'] || process.env['NO_PROXY'] || ''; + if (!noProxy) { + return false; + } + // Determine the request port + let reqPort; + if (reqUrl.port) { + reqPort = Number(reqUrl.port); + } + else if (reqUrl.protocol === 'http:') { + reqPort = 80; + } + else if (reqUrl.protocol === 'https:') { + reqPort = 443; + } + // Format the request hostname and hostname with port + const upperReqHosts = [reqUrl.hostname.toUpperCase()]; + if (typeof reqPort === 'number') { + upperReqHosts.push(`${upperReqHosts[0]}:${reqPort}`); + } + // Compare request host against noproxy + for (const upperNoProxyItem of noProxy + .split(',') + .map(x => x.trim().toUpperCase()) + .filter(x => x)) { + if (upperNoProxyItem === '*' || + upperReqHosts.some(x => x === upperNoProxyItem || + x.endsWith(`.${upperNoProxyItem}`) || + (upperNoProxyItem.startsWith('.') && + x.endsWith(`${upperNoProxyItem}`)))) { + return true; + } + } + return false; +} +exports.checkBypass = checkBypass; +function isLoopbackAddress(host) { + const hostLower = host.toLowerCase(); + return (hostLower === 'localhost' || + hostLower.startsWith('127.') || + hostLower.startsWith('[::1]') || + hostLower.startsWith('[0:0:0:0:0:0:0:1]')); +} +//# sourceMappingURL=proxy.js.map + +/***/ }), + +/***/ 4812: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +module.exports = +{ + parallel : __nccwpck_require__(8210), + serial : __nccwpck_require__(445), + serialOrdered : __nccwpck_require__(3578) +}; + + +/***/ }), + +/***/ 1700: +/***/ ((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](); + } +} + + +/***/ }), + +/***/ 2794: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var defer = __nccwpck_require__(5295); + +// 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); + }); + } + }; +} + + +/***/ }), + +/***/ 5295: +/***/ ((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); + } +} + + +/***/ }), + +/***/ 9023: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var async = __nccwpck_require__(2794) + , abort = __nccwpck_require__(1700) + ; + +// 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; +} + + +/***/ }), + +/***/ 2474: +/***/ ((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; +} + + +/***/ }), + +/***/ 7942: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var abort = __nccwpck_require__(1700) + , async = __nccwpck_require__(2794) + ; + +// 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); +} + + +/***/ }), + +/***/ 8210: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var iterate = __nccwpck_require__(9023) + , initState = __nccwpck_require__(2474) + , terminator = __nccwpck_require__(7942) + ; + +// 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); +} + + +/***/ }), + +/***/ 445: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var serialOrdered = __nccwpck_require__(3578); + +// 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); +} + + +/***/ }), + +/***/ 3578: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var iterate = __nccwpck_require__(9023) + , initState = __nccwpck_require__(2474) + , terminator = __nccwpck_require__(7942) + ; + +// 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); +} + + +/***/ }), + +/***/ 5443: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var util = __nccwpck_require__(3837); +var Stream = (__nccwpck_require__(2781).Stream); +var DelayedStream = __nccwpck_require__(8611); + +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); +}; + + +/***/ }), + +/***/ 8611: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var Stream = (__nccwpck_require__(2781).Stream); +var util = __nccwpck_require__(3837); + +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)); +}; + + +/***/ }), + +/***/ 4334: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +var CombinedStream = __nccwpck_require__(5443); +var util = __nccwpck_require__(3837); +var path = __nccwpck_require__(1017); +var http = __nccwpck_require__(3685); +var https = __nccwpck_require__(5687); +var parseUrl = (__nccwpck_require__(7310).parse); +var fs = __nccwpck_require__(7147); +var Stream = (__nccwpck_require__(2781).Stream); +var mime = __nccwpck_require__(3583); +var asynckit = __nccwpck_require__(4812); +var populate = __nccwpck_require__(7142); + +// 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(options); + } + + 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 or not a stream + if (!value || ( !value.path && !(value.readable && value.hasOwnProperty('httpVersion')) && !(value instanceof Stream))) { + 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.setBoundary = function(boundary) { + this._boundary = boundary; +}; + +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 && err !== 'Unknown stream') { + this._error(err); + return; + } + + // add content length + if (length) { + request.setHeader('Content-Length', length); + } + + this.pipe(request); + if (cb) { + var onResponse; + + var callback = function (error, responce) { + request.removeListener('error', callback); + request.removeListener('response', onResponse); + + return cb.call(this, error, responce); + }; + + onResponse = callback.bind(this, null); + + request.on('error', callback); + request.on('response', onResponse); + } + }.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]'; +}; + + +/***/ }), + +/***/ 7142: +/***/ ((module) => { + +// populates missing values +module.exports = function(dst, src) { + + Object.keys(src).forEach(function(prop) + { + dst[prop] = dst[prop] || src[prop]; + }); + + return dst; +}; + + +/***/ }), + +/***/ 4274: +/***/ (function(module, __unused_webpack_exports, __nccwpck_require__) { + +/*! For license information please see mailgun.node.js.LICENSE.txt */ +!function(e,t){ true?module.exports=t():0}(this,(()=>(()=>{var e={9118:(e,t,n)=>{e.exports={parallel:n(9162),serial:n(1357),serialOrdered:n(9087)}},7651:e=>{function t(e){"function"==typeof this.jobs[e]&&this.jobs[e]()}e.exports=function(e){Object.keys(e.jobs).forEach(t.bind(e)),e.jobs={}}},5912:(e,t,n)=>{var a=n(9265);e.exports=function(e){var t=!1;return a((function(){t=!0})),function(n,i){t?e(n,i):a((function(){e(n,i)}))}}},9265:e=>{e.exports=function(e){var t="function"==typeof setImmediate?setImmediate:"object"==typeof process&&"function"==typeof process.nextTick?process.nextTick:null;t?t(e):setTimeout(e,0)}},7594:(e,t,n)=>{var a=n(5912),i=n(7651);e.exports=function(e,t,n,o){var s=n.keyedList?n.keyedList[n.index]:n.index;n.jobs[s]=function(e,t,n,i){var o;o=2==e.length?e(n,a(i)):e(n,t,a(i));return o}(t,s,e[s],(function(e,t){s in n.jobs&&(delete n.jobs[s],e?i(n):n.results[s]=t,o(e,n.results))}))}},4528:e=>{e.exports=function(e,t){var n=!Array.isArray(e),a={index:0,keyedList:n||t?Object.keys(e):null,jobs:{},results:n?{}:[],size:n?Object.keys(e).length:e.length};t&&a.keyedList.sort(n?t:function(n,a){return t(e[n],e[a])});return a}},5353:(e,t,n)=>{var a=n(7651),i=n(5912);e.exports=function(e){if(!Object.keys(this.jobs).length)return;this.index=this.size,a(this),i(e)(null,this.results)}},9162:(e,t,n)=>{var a=n(7594),i=n(4528),o=n(5353);e.exports=function(e,t,n){var s=i(e);for(;s.index<(s.keyedList||e).length;)a(e,t,s,(function(e,t){e?n(e,t):0!==Object.keys(s.jobs).length||n(null,s.results)})),s.index++;return o.bind(s,n)}},1357:(e,t,n)=>{var a=n(9087);e.exports=function(e,t,n){return a(e,t,null,n)}},9087:(e,t,n)=>{var a=n(7594),i=n(4528),o=n(5353);function s(e,t){return et?1:0}e.exports=function(e,t,n,s){var r=i(e,n);return a(e,t,r,(function n(i,o){i?s(i,o):(r.index++,r.index<(r.keyedList||e).length?a(e,t,r,n):s(null,r.results))})),o.bind(r,s)},e.exports.ascending=s,e.exports.descending=function(e,t){return-1*s(e,t)}},4106:(e,t,n)=>{var a=n(9779),i=n(3837),o=n(1017),s=n(3685),r=n(5687),c=n(7310).parse,p=n(7147),u=n(2781).Stream,l=n(983),d=n(9118),m=n(5469);function f(e){if(!(this instanceof f))return new f(e);for(var t in this._overheadLength=0,this._valueLength=0,this._valuesToMeasure=[],a.call(this),e=e||{})this[t]=e[t]}e.exports=f,i.inherits(f,a),f.LINE_BREAK="\r\n",f.DEFAULT_CONTENT_TYPE="application/octet-stream",f.prototype.append=function(e,t,n){"string"==typeof(n=n||{})&&(n={filename:n});var o=a.prototype.append.bind(this);if("number"==typeof t&&(t=""+t),i.isArray(t))this._error(new Error("Arrays are not supported."));else{var s=this._multiPartHeader(e,t,n),r=this._multiPartFooter();o(s),o(t),o(r),this._trackLength(s,t,n)}},f.prototype._trackLength=function(e,t,n){var a=0;null!=n.knownLength?a+=+n.knownLength:Buffer.isBuffer(t)?a=t.length:"string"==typeof t&&(a=Buffer.byteLength(t)),this._valueLength+=a,this._overheadLength+=Buffer.byteLength(e)+f.LINE_BREAK.length,t&&(t.path||t.readable&&t.hasOwnProperty("httpVersion")||t instanceof u)&&(n.knownLength||this._valuesToMeasure.push(t))},f.prototype._lengthRetriever=function(e,t){e.hasOwnProperty("fd")?null!=e.end&&e.end!=1/0&&null!=e.start?t(null,e.end+1-(e.start?e.start:0)):p.stat(e.path,(function(n,a){var i;n?t(n):(i=a.size-(e.start?e.start:0),t(null,i))})):e.hasOwnProperty("httpVersion")?t(null,+e.headers["content-length"]):e.hasOwnProperty("httpModule")?(e.on("response",(function(n){e.pause(),t(null,+n.headers["content-length"])})),e.resume()):t("Unknown stream")},f.prototype._multiPartHeader=function(e,t,n){if("string"==typeof n.header)return n.header;var a,i=this._getContentDisposition(t,n),o=this._getContentType(t,n),s="",r={"Content-Disposition":["form-data",'name="'+e+'"'].concat(i||[]),"Content-Type":[].concat(o||[])};for(var c in"object"==typeof n.header&&m(r,n.header),r)r.hasOwnProperty(c)&&null!=(a=r[c])&&(Array.isArray(a)||(a=[a]),a.length&&(s+=c+": "+a.join("; ")+f.LINE_BREAK));return"--"+this.getBoundary()+f.LINE_BREAK+s+f.LINE_BREAK},f.prototype._getContentDisposition=function(e,t){var n,a;return"string"==typeof t.filepath?n=o.normalize(t.filepath).replace(/\\/g,"/"):t.filename||e.name||e.path?n=o.basename(t.filename||e.name||e.path):e.readable&&e.hasOwnProperty("httpVersion")&&(n=o.basename(e.client._httpMessage.path||"")),n&&(a='filename="'+n+'"'),a},f.prototype._getContentType=function(e,t){var n=t.contentType;return!n&&e.name&&(n=l.lookup(e.name)),!n&&e.path&&(n=l.lookup(e.path)),!n&&e.readable&&e.hasOwnProperty("httpVersion")&&(n=e.headers["content-type"]),n||!t.filepath&&!t.filename||(n=l.lookup(t.filepath||t.filename)),n||"object"!=typeof e||(n=f.DEFAULT_CONTENT_TYPE),n},f.prototype._multiPartFooter=function(){return function(e){var t=f.LINE_BREAK;0===this._streams.length&&(t+=this._lastBoundary()),e(t)}.bind(this)},f.prototype._lastBoundary=function(){return"--"+this.getBoundary()+"--"+f.LINE_BREAK},f.prototype.getHeaders=function(e){var t,n={"content-type":"multipart/form-data; boundary="+this.getBoundary()};for(t in e)e.hasOwnProperty(t)&&(n[t.toLowerCase()]=e[t]);return n},f.prototype.setBoundary=function(e){this._boundary=e},f.prototype.getBoundary=function(){return this._boundary||this._generateBoundary(),this._boundary},f.prototype.getBuffer=function(){for(var e=new Buffer.alloc(0),t=this.getBoundary(),n=0,a=this._streams.length;n{e.exports=function(e,t){return Object.keys(t).forEach((function(n){e[n]=e[n]||t[n]})),e}},5205:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=function(e,t,n){this.name=e.name,this.require_tls=e.require_tls,this.skip_verification=e.skip_verification,this.state=e.state,this.wildcard=e.wildcard,this.spam_action=e.spam_action,this.created_at=e.created_at,this.smtp_password=e.smtp_password,this.smtp_login=e.smtp_login,this.type=e.type,this.receiving_dns_records=t||null,this.sending_dns_records=n||null;var a=["id","is_disabled","web_prefix","web_scheme"].reduce((function(t,n){return n in e&&(t[n]=e[n]),t}),{});Object.assign(this,a)};t.default=n},8127:function(e,t,n){"use strict";var a=this&&this.__assign||function(){return a=Object.assign||function(e){for(var t,n=1,a=arguments.length;n0&&i[i.length-1])||6!==r[0]&&2!==r[0])){s=0;continue}if(3===r[0]&&(!i||r[1]>i[0]&&r[1]0&&i[i.length-1])||6!==r[0]&&2!==r[0])){s=0;continue}if(3===r[0]&&(!i||r[1]>i[0]&&r[1]0&&i[i.length-1])||6!==r[0]&&2!==r[0])){s=0;continue}if(3===r[0]&&(!i||r[1]>i[0]&&r[1]0&&i[i.length-1])||6!==r[0]&&2!==r[0])){s=0;continue}if(3===r[0]&&(!i||r[1]>i[0]&&r[1]0&&i[i.length-1])||6!==r[0]&&2!==r[0])){s=0;continue}if(3===r[0]&&(!i||r[1]>i[0]&&r[1]0&&i[i.length-1])||6!==r[0]&&2!==r[0])){s=0;continue}if(3===r[0]&&(!i||r[1]>i[0]&&r[1]0&&i[i.length-1])||6!==r[0]&&2!==r[0])){s=0;continue}if(3===r[0]&&(!i||r[1]>i[0]&&r[1]{"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=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/".concat(e)).then((function(e){return e.body.route}))},e.prototype.create=function(e){return this.request.postWithFD("/v3/routes",e).then((function(e){return e.body.route}))},e.prototype.update=function(e,t){return this.request.putWithFD("/v3/routes/".concat(e),t).then((function(e){return e.body}))},e.prototype.destroy=function(e){return this.request.delete("/v3/routes/".concat(e)).then((function(e){return e.body}))},e}();t.default=n},8165:function(e,t,n){"use strict";var a=this&&this.__spreadArray||function(e,t,n){if(n||2===arguments.length)for(var a,i=0,o=t.length;i{"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=function(){function e(e){this.request=e}return e.prototype.list=function(e){return this.request.get("/v5/accounts/subaccounts",e).then((function(e){return e.body}))},e.prototype.get=function(e){return this.request.get("/v5/accounts/subaccounts/".concat(e)).then((function(e){return e.body}))},e.prototype.create=function(e){return this.request.postWithFD("/v5/accounts/subaccounts",{name:e}).then((function(e){return e.body}))},e.prototype.enable=function(e){return this.request.post("/v5/accounts/subaccounts/".concat(e,"/enable")).then((function(e){return e.body}))},e.prototype.disable=function(e){return this.request.post("/v5/accounts/subaccounts/".concat(e,"/disable")).then((function(e){return e.body}))},e.SUBACCOUNT_HEADER="X-Mailgun-On-Behalf-Of",e}();t.default=n},7002:function(e,t,n){"use strict";var a,i=this&&this.__extends||(a=function(e,t){return a=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},a(e,t)},function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function n(){this.constructor=e}a(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),o=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});var s=n(8089),r=function(e){function t(t){var n=e.call(this,s.SuppressionModels.BOUNCES)||this;return n.address=t.address,n.code=+t.code,n.error=t.error,n.created_at=new Date(t.created_at),n}return i(t,e),t}(o(n(9013)).default);t.default=r},9601:function(e,t,n){"use strict";var a,i=this&&this.__extends||(a=function(e,t){return a=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},a(e,t)},function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function n(){this.constructor=e}a(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),o=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});var s=n(8089),r=function(e){function t(t){var n=e.call(this,s.SuppressionModels.COMPLAINTS)||this;return n.address=t.address,n.created_at=new Date(t.created_at),n}return i(t,e),t}(o(n(9013)).default);t.default=r},9013:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0});var n=function(e){this.type=e};t.default=n},1481:function(e,t,n){"use strict";var a,i=this&&this.__extends||(a=function(e,t){return a=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])},a(e,t)},function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function n(){this.constructor=e}a(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}),o=this&&this.__awaiter||function(e,t,n,a){return new(n||(n=Promise))((function(i,o){function s(e){try{c(a.next(e))}catch(e){o(e)}}function r(e){try{c(a.throw(e))}catch(e){o(e)}}function c(e){var t;e.done?i(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(s,r)}c((a=a.apply(e,t||[])).next())}))},s=this&&this.__generator||function(e,t){var n,a,i,o,s={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return o={next:r(0),throw:r(1),return:r(2)},"function"==typeof Symbol&&(o[Symbol.iterator]=function(){return this}),o;function r(r){return function(c){return function(r){if(n)throw new TypeError("Generator is already executing.");for(;o&&(o=0,r[0]&&(s=0)),s;)try{if(n=1,a&&(i=2&r[0]?a.return:r[0]?a.throw||((i=a.return)&&i.call(a),0):a.next)&&!(i=i.call(a,r[1])).done)return i;switch(a=0,i&&(r=[2&r[0],i.value]),r[0]){case 0:case 1:i=r;break;case 4:return s.label++,{value:r[1],done:!1};case 5:s.label++,a=r[1],r=[0];continue;case 7:r=s.ops.pop(),s.trys.pop();continue;default:if(!(i=s.trys,(i=i.length>0&&i[i.length-1])||6!==r[0]&&2!==r[0])){s=0;continue}if(3===r[0]&&(!i||r[1]>i[0]&&r[1]0&&i[i.length-1])||6!==r[0]&&2!==r[0])){s=0;continue}if(3===r[0]&&(!i||r[1]>i[0]&&r[1]0&&i[i.length-1])||6!==r[0]&&2!==r[0])){s=0;continue}if(3===r[0]&&(!i||r[1]>i[0]&&r[1]0&&i[i.length-1])||6!==r[0]&&2!==r[0])){s=0;continue}if(3===r[0]&&(!i||r[1]>i[0]&&r[1]0&&i[i.length-1])||6!==r[0]&&2!==r[0])){s=0;continue}if(3===r[0]&&(!i||r[1]>i[0]&&r[1]0&&(u.params=new URLSearchParams(r.query),delete u.query),(null==r?void 0:r.body)&&(f=null==r?void 0:r.body,u.data=f,delete u.body),v=(0,l.default)(this.url,t),c.label=1;case 1:return c.trys.push([1,3,,4]),[4,d.default.request(a(a({method:e.toLocaleUpperCase(),timeout:this.timeout,url:v,headers:p},u),{maxBodyLength:this.maxBodyLength,proxy:this.proxy}))];case 2:return h=c.sent(),[3,4];case 3:throw x=c.sent(),b=x,new m.default({status:(null===(i=null==b?void 0:b.response)||void 0===i?void 0:i.status)||400,statusText:(null===(o=null==b?void 0:b.response)||void 0===o?void 0:o.statusText)||b.code,body:(null===(s=null==b?void 0:b.response)||void 0===s?void 0:s.data)||b.message});case 4:return[4,this.getResponseBody(h)];case 5:return[2,c.sent()]}}))}))},e.prototype.getResponseBody=function(e){return r(this,void 0,void 0,(function(){var t;return c(this,(function(n){if(t={body:{},status:null==e?void 0:e.status},"string"==typeof e.data){if("Mailgun Magnificent API"===e.data)throw new m.default({status:400,statusText:"Incorrect url",body:e.data});t.body={message:e.data}}else t.body=e.data;return[2,t]}))}))},e.prototype.joinAndTransformHeaders=function(e){var t=new d.AxiosHeaders,n=u.encode("".concat(this.username,":").concat(this.key));t.setAuthorization("Basic ".concat(n)),t.set(this.headers);var a=e&&e.headers,i=this.makeHeadersFromObject(a);return t.set(i),t},e.prototype.makeHeadersFromObject=function(e){void 0===e&&(e={});var t=new d.AxiosHeaders;return t=Object.entries(e).reduce((function(e,t){var n=t[0],a=t[1];return e.set(n,a),e}),t)},e.prototype.setSubaccountHeader=function(e){var t,n=this.makeHeadersFromObject(a(a({},this.headers),((t={})[h.default.SUBACCOUNT_HEADER]=e,t)));this.headers.set(n)},e.prototype.resetSubaccountHeader=function(){this.headers.delete(h.default.SUBACCOUNT_HEADER)},e.prototype.query=function(e,t,n,i){return this.request(e,t,a({query:n},i))},e.prototype.command=function(e,t,n,i,o){void 0===o&&(o=!0);var s={};o&&(s={"Content-Type":"application/x-www-form-urlencoded"});var r=a(a(a({},s),{body:n}),i);return this.request(e,t,r)},e.prototype.get=function(e,t,n){return this.query("get",e,t,n)},e.prototype.post=function(e,t,n){return this.command("post",e,t,n)},e.prototype.postWithFD=function(e,t){var n=this.formDataBuilder.createFormData(t);return this.command("post",e,n,{headers:{"Content-Type":"multipart/form-data"}},!1)},e.prototype.putWithFD=function(e,t){var n=this.formDataBuilder.createFormData(t);return this.command("put",e,n,{headers:{"Content-Type":"multipart/form-data"}},!1)},e.prototype.patchWithFD=function(e,t){var n=this.formDataBuilder.createFormData(t);return this.command("patch",e,n,{headers:{"Content-Type":"multipart/form-data"}},!1)},e.prototype.put=function(e,t,n){return this.command("put",e,t,n)},e.prototype.delete=function(e,t){return this.command("delete",e,t)},e}();t.default=v},8089:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.YesNo=t.WebhooksIds=t.SuppressionModels=t.Resolution=void 0,function(e){e.HOUR="hour",e.DAY="day",e.MONTH="month"}(t.Resolution||(t.Resolution={})),function(e){e.BOUNCES="bounces",e.COMPLAINTS="complaints",e.UNSUBSCRIBES="unsubscribes",e.WHITELISTS="whitelists"}(t.SuppressionModels||(t.SuppressionModels={})),function(e){e.CLICKED="clicked",e.COMPLAINED="complained",e.DELIVERED="delivered",e.OPENED="opened",e.PERMANENT_FAIL="permanent_fail",e.TEMPORARY_FAIL="temporary_fail",e.UNSUBSCRIBED="unsubscribe"}(t.WebhooksIds||(t.WebhooksIds={})),function(e){e.YES="yes",e.NO="no"}(t.YesNo||(t.YesNo={}))},7471:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},466:function(e,t,n){"use strict";var a=this&&this.__createBinding||(Object.create?function(e,t,n,a){void 0===a&&(a=n);var i=Object.getOwnPropertyDescriptor(t,n);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,a,i)}:function(e,t,n,a){void 0===a&&(a=n),e[a]=t[n]}),i=this&&this.__exportStar||function(e,t){for(var n in e)"default"===n||Object.prototype.hasOwnProperty.call(t,n)||a(t,e,n)};Object.defineProperty(t,"__esModule",{value:!0}),i(n(7471),t)},7647:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},7546:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},1358:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},2236:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},9483:function(e,t,n){"use strict";var a=this&&this.__createBinding||(Object.create?function(e,t,n,a){void 0===a&&(a=n);var i=Object.getOwnPropertyDescriptor(t,n);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,a,i)}:function(e,t,n,a){void 0===a&&(a=n),e[a]=t[n]}),i=this&&this.__exportStar||function(e,t){for(var n in e)"default"===n||Object.prototype.hasOwnProperty.call(t,n)||a(t,e,n)};Object.defineProperty(t,"__esModule",{value:!0}),i(n(7647),t),i(n(7546),t),i(n(1358),t),i(n(2236),t)},4251:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},896:function(e,t,n){"use strict";var a=this&&this.__createBinding||(Object.create?function(e,t,n,a){void 0===a&&(a=n);var i=Object.getOwnPropertyDescriptor(t,n);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,a,i)}:function(e,t,n,a){void 0===a&&(a=n),e[a]=t[n]}),i=this&&this.__exportStar||function(e,t){for(var n in e)"default"===n||Object.prototype.hasOwnProperty.call(t,n)||a(t,e,n)};Object.defineProperty(t,"__esModule",{value:!0}),i(n(4251),t)},9798:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},188:function(e,t,n){"use strict";var a=this&&this.__createBinding||(Object.create?function(e,t,n,a){void 0===a&&(a=n);var i=Object.getOwnPropertyDescriptor(t,n);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,a,i)}:function(e,t,n,a){void 0===a&&(a=n),e[a]=t[n]}),i=this&&this.__exportStar||function(e,t){for(var n in e)"default"===n||Object.prototype.hasOwnProperty.call(t,n)||a(t,e,n)};Object.defineProperty(t,"__esModule",{value:!0}),i(n(9798),t)},7677:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},2685:function(e,t,n){"use strict";var a=this&&this.__createBinding||(Object.create?function(e,t,n,a){void 0===a&&(a=n);var i=Object.getOwnPropertyDescriptor(t,n);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,a,i)}:function(e,t,n,a){void 0===a&&(a=n),e[a]=t[n]}),i=this&&this.__exportStar||function(e,t){for(var n in e)"default"===n||Object.prototype.hasOwnProperty.call(t,n)||a(t,e,n)};Object.defineProperty(t,"__esModule",{value:!0}),i(n(7677),t)},7913:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},1094:function(e,t,n){"use strict";var a=this&&this.__createBinding||(Object.create?function(e,t,n,a){void 0===a&&(a=n);var i=Object.getOwnPropertyDescriptor(t,n);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,a,i)}:function(e,t,n,a){void 0===a&&(a=n),e[a]=t[n]}),i=this&&this.__exportStar||function(e,t){for(var n in e)"default"===n||Object.prototype.hasOwnProperty.call(t,n)||a(t,e,n)};Object.defineProperty(t,"__esModule",{value:!0}),i(n(7913),t)},3446:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},1225:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},2570:function(e,t,n){"use strict";var a=this&&this.__createBinding||(Object.create?function(e,t,n,a){void 0===a&&(a=n);var i=Object.getOwnPropertyDescriptor(t,n);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,a,i)}:function(e,t,n,a){void 0===a&&(a=n),e[a]=t[n]}),i=this&&this.__exportStar||function(e,t){for(var n in e)"default"===n||Object.prototype.hasOwnProperty.call(t,n)||a(t,e,n)};Object.defineProperty(t,"__esModule",{value:!0}),i(n(3446),t),i(n(1225),t)},7104:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},4005:function(e,t,n){"use strict";var a=this&&this.__createBinding||(Object.create?function(e,t,n,a){void 0===a&&(a=n);var i=Object.getOwnPropertyDescriptor(t,n);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,a,i)}:function(e,t,n,a){void 0===a&&(a=n),e[a]=t[n]}),i=this&&this.__exportStar||function(e,t){for(var n in e)"default"===n||Object.prototype.hasOwnProperty.call(t,n)||a(t,e,n)};Object.defineProperty(t,"__esModule",{value:!0}),i(n(7104),t)},6115:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},848:function(e,t,n){"use strict";var a=this&&this.__createBinding||(Object.create?function(e,t,n,a){void 0===a&&(a=n);var i=Object.getOwnPropertyDescriptor(t,n);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,a,i)}:function(e,t,n,a){void 0===a&&(a=n),e[a]=t[n]}),i=this&&this.__exportStar||function(e,t){for(var n in e)"default"===n||Object.prototype.hasOwnProperty.call(t,n)||a(t,e,n)};Object.defineProperty(t,"__esModule",{value:!0}),i(n(6115),t)},4012:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},1574:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},9923:function(e,t,n){"use strict";var a=this&&this.__createBinding||(Object.create?function(e,t,n,a){void 0===a&&(a=n);var i=Object.getOwnPropertyDescriptor(t,n);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,a,i)}:function(e,t,n,a){void 0===a&&(a=n),e[a]=t[n]}),i=this&&this.__exportStar||function(e,t){for(var n in e)"default"===n||Object.prototype.hasOwnProperty.call(t,n)||a(t,e,n)};Object.defineProperty(t,"__esModule",{value:!0}),i(n(4012),t),i(n(1574),t)},3748:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},2220:function(e,t,n){"use strict";var a=this&&this.__createBinding||(Object.create?function(e,t,n,a){void 0===a&&(a=n);var i=Object.getOwnPropertyDescriptor(t,n);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,a,i)}:function(e,t,n,a){void 0===a&&(a=n),e[a]=t[n]}),i=this&&this.__exportStar||function(e,t){for(var n in e)"default"===n||Object.prototype.hasOwnProperty.call(t,n)||a(t,e,n)};Object.defineProperty(t,"__esModule",{value:!0}),i(n(3748),t)},5129:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},157:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},2818:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},504:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},3740:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},2043:function(e,t,n){"use strict";var a=this&&this.__createBinding||(Object.create?function(e,t,n,a){void 0===a&&(a=n);var i=Object.getOwnPropertyDescriptor(t,n);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,a,i)}:function(e,t,n,a){void 0===a&&(a=n),e[a]=t[n]}),i=this&&this.__exportStar||function(e,t){for(var n in e)"default"===n||Object.prototype.hasOwnProperty.call(t,n)||a(t,e,n)};Object.defineProperty(t,"__esModule",{value:!0}),i(n(5129),t),i(n(157),t),i(n(504),t),i(n(3740),t),i(n(2818),t)},6233:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},4826:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},7272:function(e,t,n){"use strict";var a=this&&this.__createBinding||(Object.create?function(e,t,n,a){void 0===a&&(a=n);var i=Object.getOwnPropertyDescriptor(t,n);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,a,i)}:function(e,t,n,a){void 0===a&&(a=n),e[a]=t[n]}),i=this&&this.__exportStar||function(e,t){for(var n in e)"default"===n||Object.prototype.hasOwnProperty.call(t,n)||a(t,e,n)};Object.defineProperty(t,"__esModule",{value:!0}),i(n(6233),t),i(n(4826),t)},1034:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},2955:function(e,t,n){"use strict";var a=this&&this.__createBinding||(Object.create?function(e,t,n,a){void 0===a&&(a=n);var i=Object.getOwnPropertyDescriptor(t,n);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,a,i)}:function(e,t,n,a){void 0===a&&(a=n),e[a]=t[n]}),i=this&&this.__exportStar||function(e,t){for(var n in e)"default"===n||Object.prototype.hasOwnProperty.call(t,n)||a(t,e,n)};Object.defineProperty(t,"__esModule",{value:!0}),i(n(1034),t)},799:function(e,t,n){"use strict";var a=this&&this.__createBinding||(Object.create?function(e,t,n,a){void 0===a&&(a=n);var i=Object.getOwnPropertyDescriptor(t,n);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,a,i)}:function(e,t,n,a){void 0===a&&(a=n),e[a]=t[n]}),i=this&&this.__exportStar||function(e,t){for(var n in e)"default"===n||Object.prototype.hasOwnProperty.call(t,n)||a(t,e,n)};Object.defineProperty(t,"__esModule",{value:!0}),i(n(466),t),i(n(9483),t),i(n(1094),t),i(n(2570),t),i(n(9923),t),i(n(2043),t),i(n(7272),t),i(n(896),t),i(n(2955),t),i(n(4005),t),i(n(848),t),i(n(2685),t),i(n(188),t),i(n(2220),t)},4859:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},7843:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},2755:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},4994:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},643:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},4886:function(e,t,n){"use strict";var a=this&&this.__createBinding||(Object.create?function(e,t,n,a){void 0===a&&(a=n);var i=Object.getOwnPropertyDescriptor(t,n);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,a,i)}:function(e,t,n,a){void 0===a&&(a=n),e[a]=t[n]}),i=this&&this.__exportStar||function(e,t){for(var n in e)"default"===n||Object.prototype.hasOwnProperty.call(t,n)||a(t,e,n)};Object.defineProperty(t,"__esModule",{value:!0}),i(n(7843),t),i(n(4859),t),i(n(2755),t),i(n(4994),t),i(n(643),t)},8011:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},1409:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},3627:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},970:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},2179:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},9543:function(e,t,n){"use strict";var a=this&&this.__createBinding||(Object.create?function(e,t,n,a){void 0===a&&(a=n);var i=Object.getOwnPropertyDescriptor(t,n);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,a,i)}:function(e,t,n,a){void 0===a&&(a=n),e[a]=t[n]}),i=this&&this.__exportStar||function(e,t){for(var n in e)"default"===n||Object.prototype.hasOwnProperty.call(t,n)||a(t,e,n)};Object.defineProperty(t,"__esModule",{value:!0}),i(n(8011),t),i(n(2179),t),i(n(1409),t),i(n(3627),t),i(n(970),t)},8483:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},4385:function(e,t,n){"use strict";var a=this&&this.__createBinding||(Object.create?function(e,t,n,a){void 0===a&&(a=n);var i=Object.getOwnPropertyDescriptor(t,n);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,a,i)}:function(e,t,n,a){void 0===a&&(a=n),e[a]=t[n]}),i=this&&this.__exportStar||function(e,t){for(var n in e)"default"===n||Object.prototype.hasOwnProperty.call(t,n)||a(t,e,n)};Object.defineProperty(t,"__esModule",{value:!0}),i(n(8483),t)},3097:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},720:function(e,t,n){"use strict";var a=this&&this.__createBinding||(Object.create?function(e,t,n,a){void 0===a&&(a=n);var i=Object.getOwnPropertyDescriptor(t,n);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,a,i)}:function(e,t,n,a){void 0===a&&(a=n),e[a]=t[n]}),i=this&&this.__exportStar||function(e,t){for(var n in e)"default"===n||Object.prototype.hasOwnProperty.call(t,n)||a(t,e,n)};Object.defineProperty(t,"__esModule",{value:!0}),i(n(3097),t)},2409:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},5986:function(e,t,n){"use strict";var a=this&&this.__createBinding||(Object.create?function(e,t,n,a){void 0===a&&(a=n);var i=Object.getOwnPropertyDescriptor(t,n);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,a,i)}:function(e,t,n,a){void 0===a&&(a=n),e[a]=t[n]}),i=this&&this.__exportStar||function(e,t){for(var n in e)"default"===n||Object.prototype.hasOwnProperty.call(t,n)||a(t,e,n)};Object.defineProperty(t,"__esModule",{value:!0}),i(n(2409),t)},7666:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},4553:function(e,t,n){"use strict";var a=this&&this.__createBinding||(Object.create?function(e,t,n,a){void 0===a&&(a=n);var i=Object.getOwnPropertyDescriptor(t,n);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,a,i)}:function(e,t,n,a){void 0===a&&(a=n),e[a]=t[n]}),i=this&&this.__exportStar||function(e,t){for(var n in e)"default"===n||Object.prototype.hasOwnProperty.call(t,n)||a(t,e,n)};Object.defineProperty(t,"__esModule",{value:!0}),i(n(7666),t)},5560:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},5810:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},9977:function(e,t,n){"use strict";var a=this&&this.__createBinding||(Object.create?function(e,t,n,a){void 0===a&&(a=n);var i=Object.getOwnPropertyDescriptor(t,n);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,a,i)}:function(e,t,n,a){void 0===a&&(a=n),e[a]=t[n]}),i=this&&this.__exportStar||function(e,t){for(var n in e)"default"===n||Object.prototype.hasOwnProperty.call(t,n)||a(t,e,n)};Object.defineProperty(t,"__esModule",{value:!0}),i(n(5560),t),i(n(5810),t)},9348:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},7313:function(e,t,n){"use strict";var a=this&&this.__createBinding||(Object.create?function(e,t,n,a){void 0===a&&(a=n);var i=Object.getOwnPropertyDescriptor(t,n);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,a,i)}:function(e,t,n,a){void 0===a&&(a=n),e[a]=t[n]}),i=this&&this.__exportStar||function(e,t){for(var n in e)"default"===n||Object.prototype.hasOwnProperty.call(t,n)||a(t,e,n)};Object.defineProperty(t,"__esModule",{value:!0}),i(n(9348),t)},9006:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},5006:function(e,t,n){"use strict";var a=this&&this.__createBinding||(Object.create?function(e,t,n,a){void 0===a&&(a=n);var i=Object.getOwnPropertyDescriptor(t,n);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,a,i)}:function(e,t,n,a){void 0===a&&(a=n),e[a]=t[n]}),i=this&&this.__exportStar||function(e,t){for(var n in e)"default"===n||Object.prototype.hasOwnProperty.call(t,n)||a(t,e,n)};Object.defineProperty(t,"__esModule",{value:!0}),i(n(9006),t)},2144:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},4744:function(e,t,n){"use strict";var a=this&&this.__createBinding||(Object.create?function(e,t,n,a){void 0===a&&(a=n);var i=Object.getOwnPropertyDescriptor(t,n);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,a,i)}:function(e,t,n,a){void 0===a&&(a=n),e[a]=t[n]}),i=this&&this.__exportStar||function(e,t){for(var n in e)"default"===n||Object.prototype.hasOwnProperty.call(t,n)||a(t,e,n)};Object.defineProperty(t,"__esModule",{value:!0}),i(n(2144),t)},9040:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},9700:function(e,t,n){"use strict";var a=this&&this.__createBinding||(Object.create?function(e,t,n,a){void 0===a&&(a=n);var i=Object.getOwnPropertyDescriptor(t,n);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,a,i)}:function(e,t,n,a){void 0===a&&(a=n),e[a]=t[n]}),i=this&&this.__exportStar||function(e,t){for(var n in e)"default"===n||Object.prototype.hasOwnProperty.call(t,n)||a(t,e,n)};Object.defineProperty(t,"__esModule",{value:!0}),i(n(9040),t)},8275:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},5451:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},7935:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},4205:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},4312:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},2267:function(e,t,n){"use strict";var a=this&&this.__createBinding||(Object.create?function(e,t,n,a){void 0===a&&(a=n);var i=Object.getOwnPropertyDescriptor(t,n);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,a,i)}:function(e,t,n,a){void 0===a&&(a=n),e[a]=t[n]}),i=this&&this.__exportStar||function(e,t){for(var n in e)"default"===n||Object.prototype.hasOwnProperty.call(t,n)||a(t,e,n)};Object.defineProperty(t,"__esModule",{value:!0}),i(n(8275),t),i(n(5451),t),i(n(7935),t),i(n(4205),t),i(n(4312),t)},4090:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},202:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},7587:function(e,t,n){"use strict";var a=this&&this.__createBinding||(Object.create?function(e,t,n,a){void 0===a&&(a=n);var i=Object.getOwnPropertyDescriptor(t,n);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,a,i)}:function(e,t,n,a){void 0===a&&(a=n),e[a]=t[n]}),i=this&&this.__exportStar||function(e,t){for(var n in e)"default"===n||Object.prototype.hasOwnProperty.call(t,n)||a(t,e,n)};Object.defineProperty(t,"__esModule",{value:!0}),i(n(4090),t),i(n(202),t)},771:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0})},8042:function(e,t,n){"use strict";var a=this&&this.__createBinding||(Object.create?function(e,t,n,a){void 0===a&&(a=n);var i=Object.getOwnPropertyDescriptor(t,n);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,a,i)}:function(e,t,n,a){void 0===a&&(a=n),e[a]=t[n]}),i=this&&this.__exportStar||function(e,t){for(var n in e)"default"===n||Object.prototype.hasOwnProperty.call(t,n)||a(t,e,n)};Object.defineProperty(t,"__esModule",{value:!0}),i(n(771),t)},8615:function(e,t,n){"use strict";var a=this&&this.__createBinding||(Object.create?function(e,t,n,a){void 0===a&&(a=n);var i=Object.getOwnPropertyDescriptor(t,n);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,a,i)}:function(e,t,n,a){void 0===a&&(a=n),e[a]=t[n]}),i=this&&this.__exportStar||function(e,t){for(var n in e)"default"===n||Object.prototype.hasOwnProperty.call(t,n)||a(t,e,n)};Object.defineProperty(t,"__esModule",{value:!0}),i(n(4886),t),i(n(9543),t),i(n(4385),t),i(n(720),t),i(n(5986),t),i(n(4553),t),i(n(9977),t),i(n(7313),t),i(n(5006),t),i(n(4744),t),i(n(9700),t),i(n(2267),t),i(n(7587),t),i(n(8042),t)},7530:function(e,t,n){"use strict";var a=this&&this.__createBinding||(Object.create?function(e,t,n,a){void 0===a&&(a=n);var i=Object.getOwnPropertyDescriptor(t,n);i&&!("get"in i?!t.__esModule:i.writable||i.configurable)||(i={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,a,i)}:function(e,t,n,a){void 0===a&&(a=n),e[a]=t[n]}),i=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),o=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)"default"!==n&&Object.prototype.hasOwnProperty.call(e,n)&&a(t,e,n);return i(t,e),t},s=this&&this.__exportStar||function(e,t){for(var n in e)"default"===n||Object.prototype.hasOwnProperty.call(t,n)||a(t,e,n)},r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.Interfaces=t.Enums=void 0;var c=r(n(5558));t.Enums=o(n(8089)),s(n(8615),t),t.Interfaces=o(n(799));var p=function(){function e(e){this.formData=e}return Object.defineProperty(e,"default",{get:function(){return this},enumerable:!1,configurable:!0}),e.prototype.client=function(e){return new c.default(e,this.formData)},e}();t.default=p},7501:function(e,t,n){var a;e=n.nmd(e),function(i){var o=t,s=(e&&e.exports,"object"==typeof global&&global);s.global!==s&&s.window;var r=function(e){this.message=e};(r.prototype=new Error).name="InvalidCharacterError";var c=function(e){throw new r(e)},p="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",u=/[\t\n\f\r ]/g,l={encode:function(e){e=String(e),/[^\0-\xFF]/.test(e)&&c("The string to be encoded contains characters outside of the Latin1 range.");for(var t,n,a,i,o=e.length%3,s="",r=-1,u=e.length-o;++r>18&63)+p.charAt(i>>12&63)+p.charAt(i>>6&63)+p.charAt(63&i);return 2==o?(t=e.charCodeAt(r)<<8,n=e.charCodeAt(++r),s+=p.charAt((i=t+n)>>10)+p.charAt(i>>4&63)+p.charAt(i<<2&63)+"="):1==o&&(i=e.charCodeAt(r),s+=p.charAt(i>>2)+p.charAt(i<<4&63)+"=="),s},decode:function(e){var t=(e=String(e).replace(u,"")).length;t%4==0&&(t=(e=e.replace(/==?$/,"")).length),(t%4==1||/[^+a-zA-Z0-9/]/.test(e))&&c("Invalid character: the string to be decoded is not correctly encoded.");for(var n,a,i=0,o="",s=-1;++s>(-2*i&6)));return o},version:"1.0.0"};void 0===(a=function(){return l}.call(t,n,t,e))||(e.exports=a)}()},9779:(e,t,n)=>{var a=n(3837),i=n(2781).Stream,o=n(3463);function s(){this.writable=!1,this.readable=!0,this.dataSize=0,this.maxDataSize=2097152,this.pauseStreams=!0,this._released=!1,this._streams=[],this._currentStream=null,this._insideLoop=!1,this._pendingNext=!1}e.exports=s,a.inherits(s,i),s.create=function(e){var t=new this;for(var n in e=e||{})t[n]=e[n];return t},s.isStreamLike=function(e){return"function"!=typeof e&&"string"!=typeof e&&"boolean"!=typeof e&&"number"!=typeof e&&!Buffer.isBuffer(e)},s.prototype.append=function(e){if(s.isStreamLike(e)){if(!(e instanceof o)){var t=o.create(e,{maxDataSize:1/0,pauseStream:this.pauseStreams});e.on("data",this._checkDataSize.bind(this)),e=t}this._handleErrors(e),this.pauseStreams&&e.pause()}return this._streams.push(e),this},s.prototype.pipe=function(e,t){return i.prototype.pipe.call(this,e,t),this.resume(),e},s.prototype._getNext=function(){if(this._currentStream=null,this._insideLoop)this._pendingNext=!0;else{this._insideLoop=!0;try{do{this._pendingNext=!1,this._realGetNext()}while(this._pendingNext)}finally{this._insideLoop=!1}}},s.prototype._realGetNext=function(){var e=this._streams.shift();void 0!==e?"function"==typeof e?e(function(e){s.isStreamLike(e)&&(e.on("data",this._checkDataSize.bind(this)),this._handleErrors(e)),this._pipeNext(e)}.bind(this)):this._pipeNext(e):this.end()},s.prototype._pipeNext=function(e){if(this._currentStream=e,s.isStreamLike(e))return e.on("end",this._getNext.bind(this)),void e.pipe(this,{end:!1});var t=e;this.write(t),this._getNext()},s.prototype._handleErrors=function(e){var t=this;e.on("error",(function(e){t._emitError(e)}))},s.prototype.write=function(e){this.emit("data",e)},s.prototype.pause=function(){this.pauseStreams&&(this.pauseStreams&&this._currentStream&&"function"==typeof this._currentStream.pause&&this._currentStream.pause(),this.emit("pause"))},s.prototype.resume=function(){this._released||(this._released=!0,this.writable=!0,this._getNext()),this.pauseStreams&&this._currentStream&&"function"==typeof this._currentStream.resume&&this._currentStream.resume(),this.emit("resume")},s.prototype.end=function(){this._reset(),this.emit("end")},s.prototype.destroy=function(){this._reset(),this.emit("close")},s.prototype._reset=function(){this.writable=!1,this._streams=[],this._currentStream=null},s.prototype._checkDataSize=function(){if(this._updateDataSize(),!(this.dataSize<=this.maxDataSize)){var e="DelayedStream#maxDataSize of "+this.maxDataSize+" bytes exceeded.";this._emitError(new Error(e))}},s.prototype._updateDataSize=function(){this.dataSize=0;var e=this;this._streams.forEach((function(t){t.dataSize&&(e.dataSize+=t.dataSize)})),this._currentStream&&this._currentStream.dataSize&&(this.dataSize+=this._currentStream.dataSize)},s.prototype._emitError=function(e){this._reset(),this.emit("error",e)}},1227:(e,t,n)=>{t.formatArgs=function(t){if(t[0]=(this.useColors?"%c":"")+this.namespace+(this.useColors?" %c":" ")+t[0]+(this.useColors?"%c ":" ")+"+"+e.exports.humanize(this.diff),!this.useColors)return;const n="color: "+this.color;t.splice(1,0,n,"color: inherit");let a=0,i=0;t[0].replace(/%[a-zA-Z%]/g,(e=>{"%%"!==e&&(a++,"%c"===e&&(i=a))})),t.splice(i,0,n)},t.save=function(e){try{e?t.storage.setItem("debug",e):t.storage.removeItem("debug")}catch(e){}},t.load=function(){let e;try{e=t.storage.getItem("debug")}catch(e){}!e&&"undefined"!=typeof process&&"env"in process&&(e=process.env.DEBUG);return e},t.useColors=function(){if("undefined"!=typeof window&&window.process&&("renderer"===window.process.type||window.process.__nwjs))return!0;if("undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/))return!1;return"undefined"!=typeof document&&document.documentElement&&document.documentElement.style&&document.documentElement.style.WebkitAppearance||"undefined"!=typeof window&&window.console&&(window.console.firebug||window.console.exception&&window.console.table)||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/)&&parseInt(RegExp.$1,10)>=31||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/)},t.storage=function(){try{return localStorage}catch(e){}}(),t.destroy=(()=>{let e=!1;return()=>{e||(e=!0,console.warn("Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`."))}})(),t.colors=["#0000CC","#0000FF","#0033CC","#0033FF","#0066CC","#0066FF","#0099CC","#0099FF","#00CC00","#00CC33","#00CC66","#00CC99","#00CCCC","#00CCFF","#3300CC","#3300FF","#3333CC","#3333FF","#3366CC","#3366FF","#3399CC","#3399FF","#33CC00","#33CC33","#33CC66","#33CC99","#33CCCC","#33CCFF","#6600CC","#6600FF","#6633CC","#6633FF","#66CC00","#66CC33","#9900CC","#9900FF","#9933CC","#9933FF","#99CC00","#99CC33","#CC0000","#CC0033","#CC0066","#CC0099","#CC00CC","#CC00FF","#CC3300","#CC3333","#CC3366","#CC3399","#CC33CC","#CC33FF","#CC6600","#CC6633","#CC9900","#CC9933","#CCCC00","#CCCC33","#FF0000","#FF0033","#FF0066","#FF0099","#FF00CC","#FF00FF","#FF3300","#FF3333","#FF3366","#FF3399","#FF33CC","#FF33FF","#FF6600","#FF6633","#FF9900","#FF9933","#FFCC00","#FFCC33"],t.log=console.debug||console.log||(()=>{}),e.exports=n(2447)(t);const{formatters:a}=e.exports;a.j=function(e){try{return JSON.stringify(e)}catch(e){return"[UnexpectedJSONParseError]: "+e.message}}},2447:(e,t,n)=>{e.exports=function(e){function t(e){let n,i,o,s=null;function r(...e){if(!r.enabled)return;const a=r,i=Number(new Date),o=i-(n||i);a.diff=o,a.prev=n,a.curr=i,n=i,e[0]=t.coerce(e[0]),"string"!=typeof e[0]&&e.unshift("%O");let s=0;e[0]=e[0].replace(/%([a-zA-Z%])/g,((n,i)=>{if("%%"===n)return"%";s++;const o=t.formatters[i];if("function"==typeof o){const t=e[s];n=o.call(a,t),e.splice(s,1),s--}return n})),t.formatArgs.call(a,e);(a.log||t.log).apply(a,e)}return r.namespace=e,r.useColors=t.useColors(),r.color=t.selectColor(e),r.extend=a,r.destroy=t.destroy,Object.defineProperty(r,"enabled",{enumerable:!0,configurable:!1,get:()=>null!==s?s:(i!==t.namespaces&&(i=t.namespaces,o=t.enabled(e)),o),set:e=>{s=e}}),"function"==typeof t.init&&t.init(r),r}function a(e,n){const a=t(this.namespace+(void 0===n?":":n)+e);return a.log=this.log,a}function i(e){return e.toString().substring(2,e.toString().length-2).replace(/\.\*\?$/,"*")}return t.debug=t,t.default=t,t.coerce=function(e){if(e instanceof Error)return e.stack||e.message;return e},t.disable=function(){const e=[...t.names.map(i),...t.skips.map(i).map((e=>"-"+e))].join(",");return t.enable(""),e},t.enable=function(e){let n;t.save(e),t.namespaces=e,t.names=[],t.skips=[];const a=("string"==typeof e?e:"").split(/[\s,]+/),i=a.length;for(n=0;n{t[n]=e[n]})),t.names=[],t.skips=[],t.formatters={},t.selectColor=function(e){let n=0;for(let t=0;t{"undefined"==typeof process||"renderer"===process.type||!0===process.browser||process.__nwjs?e.exports=n(1227):e.exports=n(39)},39:(e,t,n)=>{const a=n(6224),i=n(3837);t.init=function(e){e.inspectOpts={};const n=Object.keys(t.inspectOpts);for(let a=0;a{}),"Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`."),t.colors=[6,2,3,4,5,1];try{const e=n(2130);e&&(e.stderr||e).level>=2&&(t.colors=[20,21,26,27,32,33,38,39,40,41,42,43,44,45,56,57,62,63,68,69,74,75,76,77,78,79,80,81,92,93,98,99,112,113,128,129,134,135,148,149,160,161,162,163,164,165,166,167,168,169,170,171,172,173,178,179,184,185,196,197,198,199,200,201,202,203,204,205,206,207,208,209,214,215,220,221])}catch(e){}t.inspectOpts=Object.keys(process.env).filter((e=>/^debug_/i.test(e))).reduce(((e,t)=>{const n=t.substring(6).toLowerCase().replace(/_([a-z])/g,((e,t)=>t.toUpperCase()));let a=process.env[t];return a=!!/^(yes|on|true|enabled)$/i.test(a)||!/^(no|off|false|disabled)$/i.test(a)&&("null"===a?null:Number(a)),e[n]=a,e}),{}),e.exports=n(2447)(t);const{formatters:o}=e.exports;o.o=function(e){return this.inspectOpts.colors=this.useColors,i.inspect(e,this.inspectOpts).split("\n").map((e=>e.trim())).join(" ")},o.O=function(e){return this.inspectOpts.colors=this.useColors,i.inspect(e,this.inspectOpts)}},3463:(e,t,n)=>{var a=n(2781).Stream,i=n(3837);function o(){this.source=null,this.dataSize=0,this.maxDataSize=1048576,this.pauseStream=!0,this._maxDataSizeExceeded=!1,this._released=!1,this._bufferedEvents=[]}e.exports=o,i.inherits(o,a),o.create=function(e,t){var n=new this;for(var a in t=t||{})n[a]=t[a];n.source=e;var i=e.emit;return e.emit=function(){return n._handleEmit(arguments),i.apply(e,arguments)},e.on("error",(function(){})),n.pauseStream&&e.pause(),n},Object.defineProperty(o.prototype,"readable",{configurable:!0,enumerable:!0,get:function(){return this.source.readable}}),o.prototype.setEncoding=function(){return this.source.setEncoding.apply(this.source,arguments)},o.prototype.resume=function(){this._released||this.release(),this.source.resume()},o.prototype.pause=function(){this.source.pause()},o.prototype.release=function(){this._released=!0,this._bufferedEvents.forEach(function(e){this.emit.apply(this,e)}.bind(this)),this._bufferedEvents=[]},o.prototype.pipe=function(){var e=a.prototype.pipe.apply(this,arguments);return this.resume(),e},o.prototype._handleEmit=function(e){this._released?this.emit.apply(this,e):("data"===e[0]&&(this.dataSize+=e[1].length,this._checkIfMaxDataSizeExceeded()),this._bufferedEvents.push(e))},o.prototype._checkIfMaxDataSizeExceeded=function(){if(!(this._maxDataSizeExceeded||this.dataSize<=this.maxDataSize)){this._maxDataSizeExceeded=!0;var e="DelayedStream#maxDataSize of "+this.maxDataSize+" bytes exceeded.";this.emit("error",new Error(e))}}},2261:(e,t,n)=>{var a;e.exports=function(){if(!a){try{a=n(5158)("follow-redirects")}catch(e){}"function"!=typeof a&&(a=function(){})}a.apply(null,arguments)}},938:(e,t,n)=>{var a=n(7310),i=a.URL,o=n(3685),s=n(5687),r=n(2781).Writable,c=n(9491),p=n(2261),u=!1;try{c(new i)}catch(e){u="ERR_INVALID_URL"===e.code}var l=["auth","host","hostname","href","path","pathname","port","protocol","query","search","hash"],d=["abort","aborted","connect","error","socket","timeout"],m=Object.create(null);d.forEach((function(e){m[e]=function(t,n,a){this._redirectable.emit(e,t,n,a)}}));var f=R("ERR_INVALID_URL","Invalid URL",TypeError),h=R("ERR_FR_REDIRECTION_FAILURE","Redirected request failed"),v=R("ERR_FR_TOO_MANY_REDIRECTS","Maximum number of redirects exceeded",h),x=R("ERR_FR_MAX_BODY_LENGTH_EXCEEDED","Request body larger than maxBodyLength limit"),b=R("ERR_STREAM_WRITE_AFTER_END","write after end"),g=r.prototype.destroy||w;function y(e,t){r.call(this),this._sanitizeOptions(e),this._options=e,this._ended=!1,this._ending=!1,this._redirectCount=0,this._redirects=[],this._requestBodyLength=0,this._requestBodyBuffers=[],t&&this.on("response",t);var n=this;this._onNativeResponse=function(e){try{n._processResponse(e)}catch(e){n.emit("error",e instanceof h?e:new h({cause:e}))}},this._performRequest()}function _(e){var t={maxRedirects:21,maxBodyLength:10485760},n={};return Object.keys(e).forEach((function(a){var o=a+":",s=n[o]=e[a],r=t[a]=Object.create(s);Object.defineProperties(r,{request:{value:function(e,a,s){var r;return r=e,i&&r instanceof i?e=k(e):E(e)?e=k(j(e)):(s=a,a=O(e),e={protocol:o}),C(a)&&(s=a,a=null),(a=Object.assign({maxRedirects:t.maxRedirects,maxBodyLength:t.maxBodyLength},e,a)).nativeProtocols=n,E(a.host)||E(a.hostname)||(a.hostname="::1"),c.equal(a.protocol,o,"protocol mismatch"),p("options",a),new y(a,s)},configurable:!0,enumerable:!0,writable:!0},get:{value:function(e,t,n){var a=r.request(e,t,n);return a.end(),a},configurable:!0,enumerable:!0,writable:!0}})})),t}function w(){}function j(e){var t;if(u)t=new i(e);else if(!E((t=O(a.parse(e))).protocol))throw new f({input:e});return t}function O(e){if(/^\[/.test(e.hostname)&&!/^\[[:0-9a-f]+\]$/i.test(e.hostname))throw new f({input:e.href||e});if(/^\[/.test(e.host)&&!/^\[[:0-9a-f]+\](:\d+)?$/i.test(e.host))throw new f({input:e.href||e});return e}function k(e,t){var n=t||{};for(var a of l)n[a]=e[a];return n.hostname.startsWith("[")&&(n.hostname=n.hostname.slice(1,-1)),""!==n.port&&(n.port=Number(n.port)),n.path=n.search?n.pathname+n.search:n.pathname,n}function P(e,t){var n;for(var a in t)e.test(a)&&(n=t[a],delete t[a]);return null==n?void 0:String(n).trim()}function R(e,t,n){function a(n){Error.captureStackTrace(this,this.constructor),Object.assign(this,n||{}),this.code=e,this.message=this.cause?t+": "+this.cause.message:t}return a.prototype=new(n||Error),Object.defineProperties(a.prototype,{constructor:{value:a,enumerable:!1},name:{value:"Error ["+e+"]",enumerable:!1}}),a}function S(e,t){for(var n of d)e.removeListener(n,m[n]);e.on("error",w),e.destroy(t)}function E(e){return"string"==typeof e||e instanceof String}function C(e){return"function"==typeof e}y.prototype=Object.create(r.prototype),y.prototype.abort=function(){S(this._currentRequest),this._currentRequest.abort(),this.emit("abort")},y.prototype.destroy=function(e){return S(this._currentRequest,e),g.call(this,e),this},y.prototype.write=function(e,t,n){if(this._ending)throw new b;if(!E(e)&&("object"!=typeof(a=e)||!("length"in a)))throw new TypeError("data should be a string, Buffer or Uint8Array");var a;C(t)&&(n=t,t=null),0!==e.length?this._requestBodyLength+e.length<=this._options.maxBodyLength?(this._requestBodyLength+=e.length,this._requestBodyBuffers.push({data:e,encoding:t}),this._currentRequest.write(e,t,n)):(this.emit("error",new x),this.abort()):n&&n()},y.prototype.end=function(e,t,n){if(C(e)?(n=e,e=t=null):C(t)&&(n=t,t=null),e){var a=this,i=this._currentRequest;this.write(e,t,(function(){a._ended=!0,i.end(null,null,n)})),this._ending=!0}else this._ended=this._ending=!0,this._currentRequest.end(null,null,n)},y.prototype.setHeader=function(e,t){this._options.headers[e]=t,this._currentRequest.setHeader(e,t)},y.prototype.removeHeader=function(e){delete this._options.headers[e],this._currentRequest.removeHeader(e)},y.prototype.setTimeout=function(e,t){var n=this;function a(t){t.setTimeout(e),t.removeListener("timeout",t.destroy),t.addListener("timeout",t.destroy)}function i(t){n._timeout&&clearTimeout(n._timeout),n._timeout=setTimeout((function(){n.emit("timeout"),o()}),e),a(t)}function o(){n._timeout&&(clearTimeout(n._timeout),n._timeout=null),n.removeListener("abort",o),n.removeListener("error",o),n.removeListener("response",o),n.removeListener("close",o),t&&n.removeListener("timeout",t),n.socket||n._currentRequest.removeListener("socket",i)}return t&&this.on("timeout",t),this.socket?i(this.socket):this._currentRequest.once("socket",i),this.on("socket",a),this.on("abort",o),this.on("error",o),this.on("response",o),this.on("close",o),this},["flushHeaders","getHeader","setNoDelay","setSocketKeepAlive"].forEach((function(e){y.prototype[e]=function(t,n){return this._currentRequest[e](t,n)}})),["aborted","connection","socket"].forEach((function(e){Object.defineProperty(y.prototype,e,{get:function(){return this._currentRequest[e]}})})),y.prototype._sanitizeOptions=function(e){if(e.headers||(e.headers={}),e.host&&(e.hostname||(e.hostname=e.host),delete e.host),!e.pathname&&e.path){var t=e.path.indexOf("?");t<0?e.pathname=e.path:(e.pathname=e.path.substring(0,t),e.search=e.path.substring(t))}},y.prototype._performRequest=function(){var e=this._options.protocol,t=this._options.nativeProtocols[e];if(!t)throw new TypeError("Unsupported protocol "+e);if(this._options.agents){var n=e.slice(0,-1);this._options.agent=this._options.agents[n]}var i=this._currentRequest=t.request(this._options,this._onNativeResponse);for(var o of(i._redirectable=this,d))i.on(o,m[o]);if(this._currentUrl=/^\//.test(this._options.path)?a.format(this._options):this._options.path,this._isRedirect){var s=0,r=this,c=this._requestBodyBuffers;!function e(t){if(i===r._currentRequest)if(t)r.emit("error",t);else if(s=400)return e.responseUrl=this._currentUrl,e.redirects=this._redirects,this.emit("response",e),void(this._requestBodyBuffers=[]);if(S(this._currentRequest),e.destroy(),++this._redirectCount>this._options.maxRedirects)throw new v;var s=this._options.beforeRedirect;s&&(n=Object.assign({Host:e.req.getHeader("host")},this._options.headers));var r=this._options.method;((301===t||302===t)&&"POST"===this._options.method||303===t&&!/^(?:GET|HEAD)$/.test(this._options.method))&&(this._options.method="GET",this._requestBodyBuffers=[],P(/^content-/i,this._options.headers));var l,d,m=P(/^host$/i,this._options.headers),f=j(this._currentUrl),h=m||f.host,x=/^\w+:/.test(o)?this._currentUrl:a.format(Object.assign(f,{host:h})),b=(l=o,d=x,u?new i(l,d):j(a.resolve(d,l)));if(p("redirecting to",b.href),this._isRedirect=!0,k(b,this._options),(b.protocol!==f.protocol&&"https:"!==b.protocol||b.host!==h&&!function(e,t){c(E(e)&&E(t));var n=e.length-t.length-1;return n>0&&"."===e[n]&&e.endsWith(t)}(b.host,h))&&P(/^(?:authorization|cookie)$/i,this._options.headers),C(s)){var g={headers:e.headers,statusCode:t},y={url:x,method:r,headers:n};s(this._options,g,y),this._sanitizeOptions(this._options)}this._performRequest()},e.exports=_({http:o,https:s}),e.exports.wrap=_},6560:e=>{"use strict";e.exports=(e,t)=>{t=t||process.argv;const n=e.startsWith("-")?"":1===e.length?"-":"--",a=t.indexOf(n+e),i=t.indexOf("--");return-1!==a&&(-1===i||a{e.exports=n(3765)},983:(e,t,n)=>{"use strict";var a,i,o,s=n(5234),r=n(1017).extname,c=/^\s*([^;\s]*)(?:;|\s|$)/,p=/^text\//i;function u(e){if(!e||"string"!=typeof e)return!1;var t=c.exec(e),n=t&&s[t[1].toLowerCase()];return n&&n.charset?n.charset:!(!t||!p.test(t[1]))&&"UTF-8"}t.charset=u,t.charsets={lookup:u},t.contentType=function(e){if(!e||"string"!=typeof e)return!1;var n=-1===e.indexOf("/")?t.lookup(e):e;if(!n)return!1;if(-1===n.indexOf("charset")){var a=t.charset(n);a&&(n+="; charset="+a.toLowerCase())}return n},t.extension=function(e){if(!e||"string"!=typeof e)return!1;var n=c.exec(e),a=n&&t.extensions[n[1].toLowerCase()];if(!a||!a.length)return!1;return a[0]},t.extensions=Object.create(null),t.lookup=function(e){if(!e||"string"!=typeof e)return!1;var n=r("x."+e).toLowerCase().substr(1);if(!n)return!1;return t.types[n]||!1},t.types=Object.create(null),a=t.extensions,i=t.types,o=["nginx","apache",void 0,"iana"],Object.keys(s).forEach((function(e){var t=s[e],n=t.extensions;if(n&&n.length){a[e]=n;for(var r=0;ru||p===u&&"application/"===i[c].substr(0,12)))continue}i[c]=e}}}))},7824:e=>{var t=1e3,n=60*t,a=60*n,i=24*a,o=7*i,s=365.25*i;function r(e,t,n,a){var i=t>=1.5*n;return Math.round(e/n)+" "+a+(i?"s":"")}e.exports=function(e,c){c=c||{};var p=typeof e;if("string"===p&&e.length>0)return function(e){if((e=String(e)).length>100)return;var r=/^(-?(?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec(e);if(!r)return;var c=parseFloat(r[1]);switch((r[2]||"ms").toLowerCase()){case"years":case"year":case"yrs":case"yr":case"y":return c*s;case"weeks":case"week":case"w":return c*o;case"days":case"day":case"d":return c*i;case"hours":case"hour":case"hrs":case"hr":case"h":return c*a;case"minutes":case"minute":case"mins":case"min":case"m":return c*n;case"seconds":case"second":case"secs":case"sec":case"s":return c*t;case"milliseconds":case"millisecond":case"msecs":case"msec":case"ms":return c;default:return}}(e);if("number"===p&&isFinite(e))return c.long?function(e){var o=Math.abs(e);if(o>=i)return r(e,o,i,"day");if(o>=a)return r(e,o,a,"hour");if(o>=n)return r(e,o,n,"minute");if(o>=t)return r(e,o,t,"second");return e+" ms"}(e):function(e){var o=Math.abs(e);if(o>=i)return Math.round(e/i)+"d";if(o>=a)return Math.round(e/a)+"h";if(o>=n)return Math.round(e/n)+"m";if(o>=t)return Math.round(e/t)+"s";return e+"ms"}(e);throw new Error("val is not a non-empty string or a valid number. val="+JSON.stringify(e))}},1394:(e,t,n)=>{"use strict";var a=n(7310).parse,i={ftp:21,gopher:70,http:80,https:443,ws:80,wss:443},o=String.prototype.endsWith||function(e){return e.length<=this.length&&-1!==this.indexOf(e,this.length-e.length)};function s(e){return process.env[e.toLowerCase()]||process.env[e.toUpperCase()]||""}t.getProxyForUrl=function(e){var t="string"==typeof e?a(e):e||{},n=t.protocol,r=t.host,c=t.port;if("string"!=typeof r||!r||"string"!=typeof n)return"";if(n=n.split(":",1)[0],!function(e,t){var n=(s("npm_config_no_proxy")||s("no_proxy")).toLowerCase();if(!n)return!0;if("*"===n)return!1;return n.split(/[,\s]/).every((function(n){if(!n)return!0;var a=n.match(/^(.+):(\d+)$/),i=a?a[1]:n,s=a?parseInt(a[2]):0;return!(!s||s===t)||(/^[.*]/.test(i)?("*"===i.charAt(0)&&(i=i.slice(1)),!o.call(e,i)):e!==i)}))}(r=r.replace(/:\d*$/,""),c=parseInt(c)||i[n]||0))return"";var p=s("npm_config_"+n+"_proxy")||s(n+"_proxy")||s("npm_config_proxy")||s("all_proxy");return p&&-1===p.indexOf("://")&&(p=n+"://"+p),p}},2130:(e,t,n)=>{"use strict";const a=n(2037),i=n(6560),o=process.env;let s;function r(e){const t=function(e){if(!1===s)return 0;if(i("color=16m")||i("color=full")||i("color=truecolor"))return 3;if(i("color=256"))return 2;if(e&&!e.isTTY&&!0!==s)return 0;const t=s?1:0;if("win32"===process.platform){const e=a.release().split(".");return Number(process.versions.node.split(".")[0])>=8&&Number(e[0])>=10&&Number(e[2])>=10586?Number(e[2])>=14931?3:2:1}if("CI"in o)return["TRAVIS","CIRCLECI","APPVEYOR","GITLAB_CI"].some((e=>e in o))||"codeship"===o.CI_NAME?1:t;if("TEAMCITY_VERSION"in o)return/^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(o.TEAMCITY_VERSION)?1:0;if("truecolor"===o.COLORTERM)return 3;if("TERM_PROGRAM"in o){const e=parseInt((o.TERM_PROGRAM_VERSION||"").split(".")[0],10);switch(o.TERM_PROGRAM){case"iTerm.app":return e>=3?3:2;case"Apple_Terminal":return 2}}return/-256(color)?$/i.test(o.TERM)?2:/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(o.TERM)||"COLORTERM"in o?1:(o.TERM,t)}(e);return function(e){return 0!==e&&{level:e,hasBasic:!0,has256:e>=2,has16m:e>=3}}(t)}i("no-color")||i("no-colors")||i("color=false")?s=!1:(i("color")||i("colors")||i("color=true")||i("color=always"))&&(s=!0),"FORCE_COLOR"in o&&(s=0===o.FORCE_COLOR.length||0!==parseInt(o.FORCE_COLOR,10)),e.exports={supportsColor:r,stdout:r(process.stdout),stderr:r(process.stderr)}},4078:function(e,t,n){var a,i,o;o=function(){function e(e){var t=[];if(0===e.length)return"";if("string"!=typeof e[0])throw new TypeError("Url must be a string. Received "+e[0]);if(e[0].match(/^[^/:]+:\/*$/)&&e.length>1){var n=e.shift();e[0]=n+e[0]}e[0].match(/^file:\/\/\//)?e[0]=e[0].replace(/^([^/:]+):\/*/,"$1:///"):e[0]=e[0].replace(/^([^/:]+):\/*/,"$1://");for(var a=0;a0&&(i=i.replace(/^[\/]+/,"")),i=a0?"?":"")+s.join("&")}return function(){return e("object"==typeof arguments[0]?arguments[0]:[].slice.call(arguments))}},e.exports?e.exports=o():void 0===(i="function"==typeof(a=o)?a.call(t,n,t,e):a)||(e.exports=i)},9491:e=>{"use strict";e.exports=__nccwpck_require__(9491)},2361:e=>{"use strict";e.exports=__nccwpck_require__(2361)},7147:e=>{"use strict";e.exports=__nccwpck_require__(7147)},3685:e=>{"use strict";e.exports=__nccwpck_require__(3685)},5687:e=>{"use strict";e.exports=__nccwpck_require__(5687)},2037:e=>{"use strict";e.exports=__nccwpck_require__(2037)},1017:e=>{"use strict";e.exports=__nccwpck_require__(1017)},2781:e=>{"use strict";e.exports=__nccwpck_require__(2781)},6224:e=>{"use strict";e.exports=__nccwpck_require__(6224)},7310:e=>{"use strict";e.exports=__nccwpck_require__(7310)},3837:e=>{"use strict";e.exports=__nccwpck_require__(3837)},9796:e=>{"use strict";e.exports=__nccwpck_require__(9796)},3306:(e,t,n)=>{"use strict";const a=n(4106),i=n(7310),o=n(1394),s=n(3685),r=n(5687),c=n(3837),p=n(938),u=n(9796),l=n(2781),d=n(2361);function m(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}const f=m(a),h=m(i),v=m(s),x=m(r),b=m(c),g=m(p),y=m(u),_=m(l),w=m(d);function j(e,t){return function(){return e.apply(t,arguments)}}const{toString:O}=Object.prototype,{getPrototypeOf:k}=Object,P=(R=Object.create(null),e=>{const t=O.call(e);return R[t]||(R[t]=t.slice(8,-1).toLowerCase())});var R;const S=e=>(e=e.toLowerCase(),t=>P(t)===e),E=e=>t=>typeof t===e,{isArray:C}=Array,T=E("undefined");const q=S("ArrayBuffer");const A=E("string"),D=E("function"),M=E("number"),F=e=>null!==e&&"object"==typeof e,L=e=>{if("object"!==P(e))return!1;const t=k(e);return!(null!==t&&t!==Object.prototype&&null!==Object.getPrototypeOf(t)||Symbol.toStringTag in e||Symbol.iterator in e)},B=S("Date"),z=S("File"),U=S("Blob"),N=S("FileList"),I=S("URLSearchParams");function W(e,t,{allOwnKeys:n=!1}={}){if(null==e)return;let a,i;if("object"!=typeof e&&(e=[e]),C(e))for(a=0,i=e.length;a0;)if(a=n[i],t===a.toLowerCase())return a;return null}const V="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:global,$=e=>!T(e)&&e!==V;const G=(J="undefined"!=typeof Uint8Array&&k(Uint8Array),e=>J&&e instanceof J);var J;const K=S("HTMLFormElement"),Y=(({hasOwnProperty:e})=>(t,n)=>e.call(t,n))(Object.prototype),Q=S("RegExp"),Z=(e,t)=>{const n=Object.getOwnPropertyDescriptors(e),a={};W(n,((n,i)=>{let o;!1!==(o=t(n,i,e))&&(a[i]=o||n)})),Object.defineProperties(e,a)},X="abcdefghijklmnopqrstuvwxyz",ee="0123456789",te={DIGIT:ee,ALPHA:X,ALPHA_DIGIT:X+X.toUpperCase()+ee};const ne=S("AsyncFunction"),ae={isArray:C,isArrayBuffer:q,isBuffer:function(e){return null!==e&&!T(e)&&null!==e.constructor&&!T(e.constructor)&&D(e.constructor.isBuffer)&&e.constructor.isBuffer(e)},isFormData:e=>{let t;return e&&("function"==typeof FormData&&e instanceof FormData||D(e.append)&&("formdata"===(t=P(e))||"object"===t&&D(e.toString)&&"[object FormData]"===e.toString()))},isArrayBufferView:function(e){let t;return t="undefined"!=typeof ArrayBuffer&&ArrayBuffer.isView?ArrayBuffer.isView(e):e&&e.buffer&&q(e.buffer),t},isString:A,isNumber:M,isBoolean:e=>!0===e||!1===e,isObject:F,isPlainObject:L,isUndefined:T,isDate:B,isFile:z,isBlob:U,isRegExp:Q,isFunction:D,isStream:e=>F(e)&&D(e.pipe),isURLSearchParams:I,isTypedArray:G,isFileList:N,forEach:W,merge:function e(){const{caseless:t}=$(this)&&this||{},n={},a=(a,i)=>{const o=t&&H(n,i)||i;L(n[o])&&L(a)?n[o]=e(n[o],a):L(a)?n[o]=e({},a):C(a)?n[o]=a.slice():n[o]=a};for(let e=0,t=arguments.length;e(W(t,((t,a)=>{n&&D(t)?e[a]=j(t,n):e[a]=t}),{allOwnKeys:a}),e),trim:e=>e.trim?e.trim():e.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,""),stripBOM:e=>(65279===e.charCodeAt(0)&&(e=e.slice(1)),e),inherits:(e,t,n,a)=>{e.prototype=Object.create(t.prototype,a),e.prototype.constructor=e,Object.defineProperty(e,"super",{value:t.prototype}),n&&Object.assign(e.prototype,n)},toFlatObject:(e,t,n,a)=>{let i,o,s;const r={};if(t=t||{},null==e)return t;do{for(i=Object.getOwnPropertyNames(e),o=i.length;o-- >0;)s=i[o],a&&!a(s,e,t)||r[s]||(t[s]=e[s],r[s]=!0);e=!1!==n&&k(e)}while(e&&(!n||n(e,t))&&e!==Object.prototype);return t},kindOf:P,kindOfTest:S,endsWith:(e,t,n)=>{e=String(e),(void 0===n||n>e.length)&&(n=e.length),n-=t.length;const a=e.indexOf(t,n);return-1!==a&&a===n},toArray:e=>{if(!e)return null;if(C(e))return e;let t=e.length;if(!M(t))return null;const n=new Array(t);for(;t-- >0;)n[t]=e[t];return n},forEachEntry:(e,t)=>{const n=(e&&e[Symbol.iterator]).call(e);let a;for(;(a=n.next())&&!a.done;){const n=a.value;t.call(e,n[0],n[1])}},matchAll:(e,t)=>{let n;const a=[];for(;null!==(n=e.exec(t));)a.push(n);return a},isHTMLForm:K,hasOwnProperty:Y,hasOwnProp:Y,reduceDescriptors:Z,freezeMethods:e=>{Z(e,((t,n)=>{if(D(e)&&-1!==["arguments","caller","callee"].indexOf(n))return!1;const a=e[n];D(a)&&(t.enumerable=!1,"writable"in t?t.writable=!1:t.set||(t.set=()=>{throw Error("Can not rewrite read-only method '"+n+"'")}))}))},toObjectSet:(e,t)=>{const n={},a=e=>{e.forEach((e=>{n[e]=!0}))};return C(e)?a(e):a(String(e).split(t)),n},toCamelCase:e=>e.toLowerCase().replace(/[-_\s]([a-z\d])(\w*)/g,(function(e,t,n){return t.toUpperCase()+n})),noop:()=>{},toFiniteNumber:(e,t)=>(e=+e,Number.isFinite(e)?e:t),findKey:H,global:V,isContextDefined:$,ALPHABET:te,generateString:(e=16,t=te.ALPHA_DIGIT)=>{let n="";const{length:a}=t;for(;e--;)n+=t[Math.random()*a|0];return n},isSpecCompliantForm:function(e){return!!(e&&D(e.append)&&"FormData"===e[Symbol.toStringTag]&&e[Symbol.iterator])},toJSONObject:e=>{const t=new Array(10),n=(e,a)=>{if(F(e)){if(t.indexOf(e)>=0)return;if(!("toJSON"in e)){t[a]=e;const i=C(e)?[]:{};return W(e,((e,t)=>{const o=n(e,a+1);!T(o)&&(i[t]=o)})),t[a]=void 0,i}}return e};return n(e,0)},isAsyncFn:ne,isThenable:e=>e&&(F(e)||D(e))&&D(e.then)&&D(e.catch)};function ie(e,t,n,a,i){Error.call(this),Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=(new Error).stack,this.message=e,this.name="AxiosError",t&&(this.code=t),n&&(this.config=n),a&&(this.request=a),i&&(this.response=i)}ae.inherits(ie,Error,{toJSON:function(){return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:ae.toJSONObject(this.config),code:this.code,status:this.response&&this.response.status?this.response.status:null}}});const oe=ie.prototype,se={};function re(e){return ae.isPlainObject(e)||ae.isArray(e)}function ce(e){return ae.endsWith(e,"[]")?e.slice(0,-2):e}function pe(e,t,n){return e?e.concat(t).map((function(e,t){return e=ce(e),!n&&t?"["+e+"]":e})).join(n?".":""):t}["ERR_BAD_OPTION_VALUE","ERR_BAD_OPTION","ECONNABORTED","ETIMEDOUT","ERR_NETWORK","ERR_FR_TOO_MANY_REDIRECTS","ERR_DEPRECATED","ERR_BAD_RESPONSE","ERR_BAD_REQUEST","ERR_CANCELED","ERR_NOT_SUPPORT","ERR_INVALID_URL"].forEach((e=>{se[e]={value:e}})),Object.defineProperties(ie,se),Object.defineProperty(oe,"isAxiosError",{value:!0}),ie.from=(e,t,n,a,i,o)=>{const s=Object.create(oe);return ae.toFlatObject(e,s,(function(e){return e!==Error.prototype}),(e=>"isAxiosError"!==e)),ie.call(s,e.message,t,n,a,i),s.cause=e,s.name=e.name,o&&Object.assign(s,o),s};const ue=ae.toFlatObject(ae,{},null,(function(e){return/^is[A-Z]/.test(e)}));function le(e,t,n){if(!ae.isObject(e))throw new TypeError("target must be an object");t=t||new(f.default||FormData);const a=(n=ae.toFlatObject(n,{metaTokens:!0,dots:!1,indexes:!1},!1,(function(e,t){return!ae.isUndefined(t[e])}))).metaTokens,i=n.visitor||p,o=n.dots,s=n.indexes,r=(n.Blob||"undefined"!=typeof Blob&&Blob)&&ae.isSpecCompliantForm(t);if(!ae.isFunction(i))throw new TypeError("visitor must be a function");function c(e){if(null===e)return"";if(ae.isDate(e))return e.toISOString();if(!r&&ae.isBlob(e))throw new ie("Blob is not supported. Use a Buffer instead.");return ae.isArrayBuffer(e)||ae.isTypedArray(e)?r&&"function"==typeof Blob?new Blob([e]):Buffer.from(e):e}function p(e,n,i){let r=e;if(e&&!i&&"object"==typeof e)if(ae.endsWith(n,"{}"))n=a?n:n.slice(0,-2),e=JSON.stringify(e);else if(ae.isArray(e)&&function(e){return ae.isArray(e)&&!e.some(re)}(e)||(ae.isFileList(e)||ae.endsWith(n,"[]"))&&(r=ae.toArray(e)))return n=ce(n),r.forEach((function(e,a){!ae.isUndefined(e)&&null!==e&&t.append(!0===s?pe([n],a,o):null===s?n:n+"[]",c(e))})),!1;return!!re(e)||(t.append(pe(i,n,o),c(e)),!1)}const u=[],l=Object.assign(ue,{defaultVisitor:p,convertValue:c,isVisitable:re});if(!ae.isObject(e))throw new TypeError("data must be an object");return function e(n,a){if(!ae.isUndefined(n)){if(-1!==u.indexOf(n))throw Error("Circular reference detected in "+a.join("."));u.push(n),ae.forEach(n,(function(n,o){!0===(!(ae.isUndefined(n)||null===n)&&i.call(t,n,ae.isString(o)?o.trim():o,a,l))&&e(n,a?a.concat(o):[o])})),u.pop()}}(e),t}function de(e){const t={"!":"%21","'":"%27","(":"%28",")":"%29","~":"%7E","%20":"+","%00":"\0"};return encodeURIComponent(e).replace(/[!'()~]|%20|%00/g,(function(e){return t[e]}))}function me(e,t){this._pairs=[],e&&le(e,this,t)}const fe=me.prototype;function he(e){return encodeURIComponent(e).replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,"+").replace(/%5B/gi,"[").replace(/%5D/gi,"]")}function ve(e,t,n){if(!t)return e;const a=n&&n.encode||he,i=n&&n.serialize;let o;if(o=i?i(t,n):ae.isURLSearchParams(t)?t.toString():new me(t,n).toString(a),o){const t=e.indexOf("#");-1!==t&&(e=e.slice(0,t)),e+=(-1===e.indexOf("?")?"?":"&")+o}return e}fe.append=function(e,t){this._pairs.push([e,t])},fe.toString=function(e){const t=e?function(t){return e.call(this,t,de)}:de;return this._pairs.map((function(e){return t(e[0])+"="+t(e[1])}),"").join("&")};const xe=class InterceptorManager{constructor(){this.handlers=[]}use(e,t,n){return this.handlers.push({fulfilled:e,rejected:t,synchronous:!!n&&n.synchronous,runWhen:n?n.runWhen:null}),this.handlers.length-1}eject(e){this.handlers[e]&&(this.handlers[e]=null)}clear(){this.handlers&&(this.handlers=[])}forEach(e){ae.forEach(this.handlers,(function(t){null!==t&&e(t)}))}},be={silentJSONParsing:!0,forcedJSONParsing:!0,clarifyTimeoutError:!1},ge={isNode:!0,classes:{URLSearchParams:h.default.URLSearchParams,FormData:f.default,Blob:"undefined"!=typeof Blob&&Blob||null},protocols:["http","https","file","data"]};function ye(e){function t(e,n,a,i){let o=e[i++];const s=Number.isFinite(+o),r=i>=e.length;if(o=!o&&ae.isArray(a)?a.length:o,r)return ae.hasOwnProp(a,o)?a[o]=[a[o],n]:a[o]=n,!s;a[o]&&ae.isObject(a[o])||(a[o]=[]);return t(e,n,a[o],i)&&ae.isArray(a[o])&&(a[o]=function(e){const t={},n=Object.keys(e);let a;const i=n.length;let o;for(a=0;a{t(function(e){return ae.matchAll(/\w+|\[(\w*)]/g,e).map((e=>"[]"===e[0]?"":e[1]||e[0]))}(e),a,n,0)})),n}return null}const _e={transitional:be,adapter:["xhr","http"],transformRequest:[function(e,t){const n=t.getContentType()||"",a=n.indexOf("application/json")>-1,i=ae.isObject(e);i&&ae.isHTMLForm(e)&&(e=new FormData(e));if(ae.isFormData(e))return a&&a?JSON.stringify(ye(e)):e;if(ae.isArrayBuffer(e)||ae.isBuffer(e)||ae.isStream(e)||ae.isFile(e)||ae.isBlob(e))return e;if(ae.isArrayBufferView(e))return e.buffer;if(ae.isURLSearchParams(e))return t.setContentType("application/x-www-form-urlencoded;charset=utf-8",!1),e.toString();let o;if(i){if(n.indexOf("application/x-www-form-urlencoded")>-1)return function(e,t){return le(e,new ge.classes.URLSearchParams,Object.assign({visitor:function(e,t,n,a){return ae.isBuffer(e)?(this.append(t,e.toString("base64")),!1):a.defaultVisitor.apply(this,arguments)}},t))}(e,this.formSerializer).toString();if((o=ae.isFileList(e))||n.indexOf("multipart/form-data")>-1){const t=this.env&&this.env.FormData;return le(o?{"files[]":e}:e,t&&new t,this.formSerializer)}}return i||a?(t.setContentType("application/json",!1),function(e,t,n){if(ae.isString(e))try{return(t||JSON.parse)(e),ae.trim(e)}catch(e){if("SyntaxError"!==e.name)throw e}return(n||JSON.stringify)(e)}(e)):e}],transformResponse:[function(e){const t=this.transitional||_e.transitional,n=t&&t.forcedJSONParsing,a="json"===this.responseType;if(e&&ae.isString(e)&&(n&&!this.responseType||a)){const n=!(t&&t.silentJSONParsing)&&a;try{return JSON.parse(e)}catch(e){if(n){if("SyntaxError"===e.name)throw ie.from(e,ie.ERR_BAD_RESPONSE,this,null,this.response);throw e}}}return e}],timeout:0,xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",maxContentLength:-1,maxBodyLength:-1,env:{FormData:ge.classes.FormData,Blob:ge.classes.Blob},validateStatus:function(e){return e>=200&&e<300},headers:{common:{Accept:"application/json, text/plain, */*","Content-Type":void 0}}};ae.forEach(["delete","get","head","post","put","patch"],(e=>{_e.headers[e]={}}));const we=_e,je=ae.toObjectSet(["age","authorization","content-length","content-type","etag","expires","from","host","if-modified-since","if-unmodified-since","last-modified","location","max-forwards","proxy-authorization","referer","retry-after","user-agent"]),Oe=Symbol("internals");function ke(e){return e&&String(e).trim().toLowerCase()}function Pe(e){return!1===e||null==e?e:ae.isArray(e)?e.map(Pe):String(e)}function Re(e,t,n,a,i){return ae.isFunction(a)?a.call(this,t,n):(i&&(t=n),ae.isString(t)?ae.isString(a)?-1!==t.indexOf(a):ae.isRegExp(a)?a.test(t):void 0:void 0)}class AxiosHeaders{constructor(e){e&&this.set(e)}set(e,t,n){const a=this;function i(e,t,n){const i=ke(t);if(!i)throw new Error("header name must be a non-empty string");const o=ae.findKey(a,i);(!o||void 0===a[o]||!0===n||void 0===n&&!1!==a[o])&&(a[o||t]=Pe(e))}const o=(e,t)=>ae.forEach(e,((e,n)=>i(e,n,t)));return ae.isPlainObject(e)||e instanceof this.constructor?o(e,t):ae.isString(e)&&(e=e.trim())&&!/^[-_a-zA-Z0-9^`|~,!#$%&'*+.]+$/.test(e.trim())?o((e=>{const t={};let n,a,i;return e&&e.split("\n").forEach((function(e){i=e.indexOf(":"),n=e.substring(0,i).trim().toLowerCase(),a=e.substring(i+1).trim(),!n||t[n]&&je[n]||("set-cookie"===n?t[n]?t[n].push(a):t[n]=[a]:t[n]=t[n]?t[n]+", "+a:a)})),t})(e),t):null!=e&&i(t,e,n),this}get(e,t){if(e=ke(e)){const n=ae.findKey(this,e);if(n){const e=this[n];if(!t)return e;if(!0===t)return function(e){const t=Object.create(null),n=/([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g;let a;for(;a=n.exec(e);)t[a[1]]=a[2];return t}(e);if(ae.isFunction(t))return t.call(this,e,n);if(ae.isRegExp(t))return t.exec(e);throw new TypeError("parser must be boolean|regexp|function")}}}has(e,t){if(e=ke(e)){const n=ae.findKey(this,e);return!(!n||void 0===this[n]||t&&!Re(0,this[n],n,t))}return!1}delete(e,t){const n=this;let a=!1;function i(e){if(e=ke(e)){const i=ae.findKey(n,e);!i||t&&!Re(0,n[i],i,t)||(delete n[i],a=!0)}}return ae.isArray(e)?e.forEach(i):i(e),a}clear(e){const t=Object.keys(this);let n=t.length,a=!1;for(;n--;){const i=t[n];e&&!Re(0,this[i],i,e,!0)||(delete this[i],a=!0)}return a}normalize(e){const t=this,n={};return ae.forEach(this,((a,i)=>{const o=ae.findKey(n,i);if(o)return t[o]=Pe(a),void delete t[i];const s=e?function(e){return e.trim().toLowerCase().replace(/([a-z\d])(\w*)/g,((e,t,n)=>t.toUpperCase()+n))}(i):String(i).trim();s!==i&&delete t[i],t[s]=Pe(a),n[s]=!0})),this}concat(...e){return this.constructor.concat(this,...e)}toJSON(e){const t=Object.create(null);return ae.forEach(this,((n,a)=>{null!=n&&!1!==n&&(t[a]=e&&ae.isArray(n)?n.join(", "):n)})),t}[Symbol.iterator](){return Object.entries(this.toJSON())[Symbol.iterator]()}toString(){return Object.entries(this.toJSON()).map((([e,t])=>e+": "+t)).join("\n")}get[Symbol.toStringTag](){return"AxiosHeaders"}static from(e){return e instanceof this?e:new this(e)}static concat(e,...t){const n=new this(e);return t.forEach((e=>n.set(e))),n}static accessor(e){const t=(this[Oe]=this[Oe]={accessors:{}}).accessors,n=this.prototype;function a(e){const a=ke(e);t[a]||(!function(e,t){const n=ae.toCamelCase(" "+t);["get","set","has"].forEach((a=>{Object.defineProperty(e,a+n,{value:function(e,n,i){return this[a].call(this,t,e,n,i)},configurable:!0})}))}(n,e),t[a]=!0)}return ae.isArray(e)?e.forEach(a):a(e),this}}AxiosHeaders.accessor(["Content-Type","Content-Length","Accept","Accept-Encoding","User-Agent","Authorization"]),ae.reduceDescriptors(AxiosHeaders.prototype,(({value:e},t)=>{let n=t[0].toUpperCase()+t.slice(1);return{get:()=>e,set(e){this[n]=e}}})),ae.freezeMethods(AxiosHeaders);const Se=AxiosHeaders;function Ee(e,t){const n=this||we,a=t||n,i=Se.from(a.headers);let o=a.data;return ae.forEach(e,(function(e){o=e.call(n,o,i.normalize(),t?t.status:void 0)})),i.normalize(),o}function Ce(e){return!(!e||!e.__CANCEL__)}function Te(e,t,n){ie.call(this,null==e?"canceled":e,ie.ERR_CANCELED,t,n),this.name="CanceledError"}function qe(e,t,n){const a=n.config.validateStatus;n.status&&a&&!a(n.status)?t(new ie("Request failed with status code "+n.status,[ie.ERR_BAD_REQUEST,ie.ERR_BAD_RESPONSE][Math.floor(n.status/100)-4],n.config,n.request,n)):e(n)}function Ae(e,t){return e&&!function(e){return/^([a-z][a-z\d+\-.]*:)?\/\//i.test(e)}(t)?function(e,t){return t?e.replace(/\/+$/,"")+"/"+t.replace(/^\/+/,""):e}(e,t):t}ae.inherits(Te,ie,{__CANCEL__:!0});const De="1.6.0";function Me(e){const t=/^([-+\w]{1,25})(:?\/\/|:)/.exec(e);return t&&t[1]||""}const Fe=/^(?:([^;]+);)?(?:[^;]+;)?(base64|),([\s\S]*)$/;function Le(e,t){e=e||10;const n=new Array(e),a=new Array(e);let i,o=0,s=0;return t=void 0!==t?t:1e3,function(r){const c=Date.now(),p=a[s];i||(i=c),n[o]=r,a[o]=c;let u=s,l=0;for(;u!==o;)l+=n[u++],u%=e;if(o=(o+1)%e,o===s&&(s=(s+1)%e),c-i!ae.isUndefined(t[e])))).chunkSize});const t=this,n=this[Be]={length:e.length,timeWindow:e.timeWindow,ticksRate:e.ticksRate,chunkSize:e.chunkSize,maxRate:e.maxRate,minChunkSize:e.minChunkSize,bytesSeen:0,isCaptured:!1,notifiedBytesLoaded:0,ts:Date.now(),bytes:0,onReadCallback:null},a=Le(n.ticksRate*e.samplesCount,n.timeWindow);this.on("newListener",(e=>{"progress"===e&&(n.isCaptured||(n.isCaptured=!0))}));let i=0;n.updateProgress=function(e,t){let n=0;const a=1e3/t;let i=null;return function(t,o){const s=Date.now();if(t||s-n>a)return i&&(clearTimeout(i),i=null),n=s,e.apply(null,o);i||(i=setTimeout((()=>(i=null,n=Date.now(),e.apply(null,o))),a-(s-n)))}}((function(){const e=n.length,o=n.bytesSeen,s=o-i;if(!s||t.destroyed)return;const r=a(s);i=o,process.nextTick((()=>{t.emit("progress",{loaded:o,total:e,progress:e?o/e:void 0,bytes:s,rate:r||void 0,estimated:r&&e&&o<=e?(e-o)/r:void 0})}))}),n.ticksRate);const o=()=>{n.updateProgress(!0)};this.once("end",o),this.once("error",o)}_read(e){const t=this[Be];return t.onReadCallback&&t.onReadCallback(),super._read(e)}_transform(e,t,n){const a=this,i=this[Be],o=i.maxRate,s=this.readableHighWaterMark,r=i.timeWindow,c=o/(1e3/r),p=!1!==i.minChunkSize?Math.max(i.minChunkSize,.01*c):0;const u=(e,t)=>{const n=Buffer.byteLength(e);let u,l=null,d=s,m=0;if(o){const e=Date.now();(!i.ts||(m=e-i.ts)>=r)&&(i.ts=e,u=c-i.bytes,i.bytes=u<0?-u:0,m=0),u=c-i.bytes}if(o){if(u<=0)return setTimeout((()=>{t(null,e)}),r-m);ud&&n-d>p&&(l=e.subarray(d),e=e.subarray(0,d)),function(e,t){const n=Buffer.byteLength(e);i.bytesSeen+=n,i.bytes+=n,i.isCaptured&&i.updateProgress(),a.push(e)?process.nextTick(t):i.onReadCallback=()=>{i.onReadCallback=null,process.nextTick(t)}}(e,l?()=>{process.nextTick(t,null,l)}:t)};u(e,(function e(t,a){if(t)return n(t);a?u(a,e):n(null)}))}setLength(e){return this[Be].length=+e,this}}const ze=AxiosTransformStream,{asyncIterator:Ue}=Symbol,Ne=async function*(e){e.stream?yield*e.stream():e.arrayBuffer?yield await e.arrayBuffer():e[Ue]?yield*e[Ue]():yield e},Ie=ae.ALPHABET.ALPHA_DIGIT+"-_",We=new c.TextEncoder,He="\r\n",Ve=We.encode(He);class FormDataPart{constructor(e,t){const{escapeName:n}=this.constructor,a=ae.isString(t);let i=`Content-Disposition: form-data; name="${n(e)}"${!a&&t.name?`; filename="${n(t.name)}"`:""}\r\n`;a?t=We.encode(String(t).replace(/\r?\n|\r\n?/g,He)):i+=`Content-Type: ${t.type||"application/octet-stream"}\r\n`,this.headers=We.encode(i+He),this.contentLength=a?t.byteLength:t.size,this.size=this.headers.byteLength+this.contentLength+2,this.name=e,this.value=t}async*encode(){yield this.headers;const{value:e}=this;ae.isTypedArray(e)?yield e:yield*Ne(e),yield Ve}static escapeName(e){return String(e).replace(/[\r\n"]/g,(e=>({"\r":"%0D","\n":"%0A",'"':"%22"}[e])))}}const $e=(e,t,n)=>{const{tag:a="form-data-boundary",size:i=25,boundary:o=a+"-"+ae.generateString(i,Ie)}=n||{};if(!ae.isFormData(e))throw TypeError("FormData instance required");if(o.length<1||o.length>70)throw Error("boundary must be 10-70 characters long");const s=We.encode("--"+o+He),r=We.encode("--"+o+"--"+He+He);let c=r.byteLength;const p=Array.from(e.entries()).map((([e,t])=>{const n=new FormDataPart(e,t);return c+=n.size,n}));c+=s.byteLength*p.length,c=ae.toFiniteNumber(c);const u={"Content-Type":`multipart/form-data; boundary=${o}`};return Number.isFinite(c)&&(u["Content-Length"]=c),t&&t(u),l.Readable.from(async function*(){for(const e of p)yield s,yield*e.encode();yield r}())};class ZlibHeaderTransformStream extends _.default.Transform{__transform(e,t,n){this.push(e),n()}_transform(e,t,n){if(0!==e.length&&(this._transform=this.__transform,120!==e[0])){const e=Buffer.alloc(2);e[0]=120,e[1]=156,this.push(e,t)}this.__transform(e,t,n)}}const Ge=ZlibHeaderTransformStream,Je=(e,t)=>ae.isAsyncFn(e)?function(...n){const a=n.pop();e.apply(this,n).then((e=>{try{t?a(null,...t(e)):a(null,e)}catch(e){a(e)}}),a)}:e,Ke={flush:y.default.constants.Z_SYNC_FLUSH,finishFlush:y.default.constants.Z_SYNC_FLUSH},Ye={flush:y.default.constants.BROTLI_OPERATION_FLUSH,finishFlush:y.default.constants.BROTLI_OPERATION_FLUSH},Qe=ae.isFunction(y.default.createBrotliDecompress),{http:Ze,https:Xe}=g.default,et=/https:?/,tt=ge.protocols.map((e=>e+":"));function nt(e){e.beforeRedirects.proxy&&e.beforeRedirects.proxy(e),e.beforeRedirects.config&&e.beforeRedirects.config(e)}function at(e,t,n){let a=t;if(!a&&!1!==a){const e=o.getProxyForUrl(n);e&&(a=new URL(e))}if(a){if(a.username&&(a.auth=(a.username||"")+":"+(a.password||"")),a.auth){(a.auth.username||a.auth.password)&&(a.auth=(a.auth.username||"")+":"+(a.auth.password||""));const t=Buffer.from(a.auth,"utf8").toString("base64");e.headers["Proxy-Authorization"]="Basic "+t}e.headers.host=e.hostname+(e.port?":"+e.port:"");const t=a.hostname||a.host;e.hostname=t,e.host=t,e.port=a.port,e.path=n,a.protocol&&(e.protocol=a.protocol.includes(":")?a.protocol:`${a.protocol}:`)}e.beforeRedirects.proxy=function(e){at(e,t,e.href)}}const it="undefined"!=typeof process&&"process"===ae.kindOf(process),ot=(e,t)=>(({address:e,family:t})=>{if(!ae.isString(e))throw TypeError("address must be a string");return{address:e,family:t||(e.indexOf(".")<0?6:4)}})(ae.isObject(e)?e:{address:e,family:t}),st=it&&function(e){return t=async function(t,n,a){let{data:i,lookup:o,family:s}=e;const{responseType:r,responseEncoding:c}=e,p=e.method.toUpperCase();let u,l,d=!1;if(o){const e=Je(o,(e=>ae.isArray(e)?e:[e]));o=(t,n,a)=>{e(t,n,((e,t,i)=>{const o=ae.isArray(t)?t.map((e=>ot(e))):[ot(t,i)];n.all?a(e,o):a(e,o[0].address,o[0].family)}))}}const m=new w.default,f=()=>{e.cancelToken&&e.cancelToken.unsubscribe(h),e.signal&&e.signal.removeEventListener("abort",h),m.removeAllListeners()};function h(t){m.emit("abort",!t||t.type?new Te(null,e,l):t)}a(((e,t)=>{u=!0,t&&(d=!0,f())})),m.once("abort",n),(e.cancelToken||e.signal)&&(e.cancelToken&&e.cancelToken.subscribe(h),e.signal&&(e.signal.aborted?h():e.signal.addEventListener("abort",h)));const g=Ae(e.baseURL,e.url),j=new URL(g,"http://localhost"),O=j.protocol||tt[0];if("data:"===O){let a;if("GET"!==p)return qe(t,n,{status:405,statusText:"method not allowed",headers:{},config:e});try{a=function(e,t,n){const a=n&&n.Blob||ge.classes.Blob,i=Me(e);if(void 0===t&&a&&(t=!0),"data"===i){e=i.length?e.slice(i.length+1):e;const n=Fe.exec(e);if(!n)throw new ie("Invalid URL",ie.ERR_INVALID_URL);const o=n[1],s=n[2],r=n[3],c=Buffer.from(decodeURIComponent(r),s?"base64":"utf8");if(t){if(!a)throw new ie("Blob is not supported",ie.ERR_NOT_SUPPORT);return new a([c],{type:o})}return c}throw new ie("Unsupported protocol "+i,ie.ERR_NOT_SUPPORT)}(e.url,"blob"===r,{Blob:e.env&&e.env.Blob})}catch(t){throw ie.from(t,ie.ERR_BAD_REQUEST,e)}return"text"===r?(a=a.toString(c),c&&"utf8"!==c||(a=ae.stripBOM(a))):"stream"===r&&(a=_.default.Readable.from(a)),qe(t,n,{data:a,status:200,statusText:"OK",headers:new Se,config:e})}if(-1===tt.indexOf(O))return n(new ie("Unsupported protocol "+O,ie.ERR_BAD_REQUEST,e));const k=Se.from(e.headers).normalize();k.set("User-Agent","axios/1.6.0",!1);const P=e.onDownloadProgress,R=e.onUploadProgress,S=e.maxRate;let E,C;if(ae.isSpecCompliantForm(i)){const e=k.getContentType(/boundary=([-_\w\d]{10,70})/i);i=$e(i,(e=>{k.set(e)}),{tag:"axios-1.6.0-boundary",boundary:e&&e[1]||void 0})}else if(ae.isFormData(i)&&ae.isFunction(i.getHeaders)){if(k.set(i.getHeaders()),!k.hasContentLength())try{const e=await b.default.promisify(i.getLength).call(i);Number.isFinite(e)&&e>=0&&k.setContentLength(e)}catch(e){}}else if(ae.isBlob(i))i.size&&k.setContentType(i.type||"application/octet-stream"),k.setContentLength(i.size||0),i=_.default.Readable.from(Ne(i));else if(i&&!ae.isStream(i)){if(Buffer.isBuffer(i));else if(ae.isArrayBuffer(i))i=Buffer.from(new Uint8Array(i));else{if(!ae.isString(i))return n(new ie("Data after transformation must be a string, an ArrayBuffer, a Buffer, or a Stream",ie.ERR_BAD_REQUEST,e));i=Buffer.from(i,"utf-8")}if(k.setContentLength(i.length,!1),e.maxBodyLength>-1&&i.length>e.maxBodyLength)return n(new ie("Request body larger than maxBodyLength limit",ie.ERR_BAD_REQUEST,e))}const T=ae.toFiniteNumber(k.getContentLength());let q,A;ae.isArray(S)?(E=S[0],C=S[1]):E=C=S,i&&(R||E)&&(ae.isStream(i)||(i=_.default.Readable.from(i,{objectMode:!1})),i=_.default.pipeline([i,new ze({length:T,maxRate:ae.toFiniteNumber(E)})],ae.noop),R&&i.on("progress",(e=>{R(Object.assign(e,{upload:!0}))}))),e.auth&&(q=(e.auth.username||"")+":"+(e.auth.password||"")),!q&&j.username&&(q=j.username+":"+j.password),q&&k.delete("authorization");try{A=ve(j.pathname+j.search,e.params,e.paramsSerializer).replace(/^\?/,"")}catch(t){const a=new Error(t.message);return a.config=e,a.url=e.url,a.exists=!0,n(a)}k.set("Accept-Encoding","gzip, compress, deflate"+(Qe?", br":""),!1);const D={path:A,method:p,headers:k.toJSON(),agents:{http:e.httpAgent,https:e.httpsAgent},auth:q,protocol:O,family:s,beforeRedirect:nt,beforeRedirects:{}};let M;!ae.isUndefined(o)&&(D.lookup=o),e.socketPath?D.socketPath=e.socketPath:(D.hostname=j.hostname,D.port=j.port,at(D,e.proxy,O+"//"+j.hostname+(j.port?":"+j.port:"")+D.path));const F=et.test(D.protocol);if(D.agent=F?e.httpsAgent:e.httpAgent,e.transport?M=e.transport:0===e.maxRedirects?M=F?x.default:v.default:(e.maxRedirects&&(D.maxRedirects=e.maxRedirects),e.beforeRedirect&&(D.beforeRedirects.config=e.beforeRedirect),M=F?Xe:Ze),e.maxBodyLength>-1?D.maxBodyLength=e.maxBodyLength:D.maxBodyLength=1/0,e.insecureHTTPParser&&(D.insecureHTTPParser=e.insecureHTTPParser),l=M.request(D,(function(a){if(l.destroyed)return;const i=[a],o=+a.headers["content-length"];if(P){const e=new ze({length:ae.toFiniteNumber(o),maxRate:ae.toFiniteNumber(C)});P&&e.on("progress",(e=>{P(Object.assign(e,{download:!0}))})),i.push(e)}let s=a;const u=a.req||l;if(!1!==e.decompress&&a.headers["content-encoding"])switch("HEAD"!==p&&204!==a.statusCode||delete a.headers["content-encoding"],(a.headers["content-encoding"]||"").toLowerCase()){case"gzip":case"x-gzip":case"compress":case"x-compress":i.push(y.default.createUnzip(Ke)),delete a.headers["content-encoding"];break;case"deflate":i.push(new Ge),i.push(y.default.createUnzip(Ke)),delete a.headers["content-encoding"];break;case"br":Qe&&(i.push(y.default.createBrotliDecompress(Ye)),delete a.headers["content-encoding"])}s=i.length>1?_.default.pipeline(i,ae.noop):i[0];const h=_.default.finished(s,(()=>{h(),f()})),v={status:a.statusCode,statusText:a.statusMessage,headers:new Se(a.headers),config:e,request:u};if("stream"===r)v.data=s,qe(t,n,v);else{const a=[];let i=0;s.on("data",(function(t){a.push(t),i+=t.length,e.maxContentLength>-1&&i>e.maxContentLength&&(d=!0,s.destroy(),n(new ie("maxContentLength size of "+e.maxContentLength+" exceeded",ie.ERR_BAD_RESPONSE,e,u)))})),s.on("aborted",(function(){if(d)return;const t=new ie("maxContentLength size of "+e.maxContentLength+" exceeded",ie.ERR_BAD_RESPONSE,e,u);s.destroy(t),n(t)})),s.on("error",(function(t){l.destroyed||n(ie.from(t,null,e,u))})),s.on("end",(function(){try{let e=1===a.length?a[0]:Buffer.concat(a);"arraybuffer"!==r&&(e=e.toString(c),c&&"utf8"!==c||(e=ae.stripBOM(e))),v.data=e}catch(t){return n(ie.from(t,null,e,v.request,v))}qe(t,n,v)}))}m.once("abort",(e=>{s.destroyed||(s.emit("error",e),s.destroy())}))})),m.once("abort",(e=>{n(e),l.destroy(e)})),l.on("error",(function(t){n(ie.from(t,null,e,l))})),l.on("socket",(function(e){e.setKeepAlive(!0,6e4)})),e.timeout){const t=parseInt(e.timeout,10);if(Number.isNaN(t))return void n(new ie("error trying to parse `config.timeout` to int",ie.ERR_BAD_OPTION_VALUE,e,l));l.setTimeout(t,(function(){if(u)return;let t=e.timeout?"timeout of "+e.timeout+"ms exceeded":"timeout exceeded";const a=e.transitional||be;e.timeoutErrorMessage&&(t=e.timeoutErrorMessage),n(new ie(t,a.clarifyTimeoutError?ie.ETIMEDOUT:ie.ECONNABORTED,e,l)),h()}))}if(ae.isStream(i)){let t=!1,n=!1;i.on("end",(()=>{t=!0})),i.once("error",(e=>{n=!0,l.destroy(e)})),i.on("close",(()=>{t||n||h(new Te("Request stream has been aborted",e,l))})),i.pipe(l)}else l.end(i)},new Promise(((e,n)=>{let a,i;const o=(e,t)=>{i||(i=!0,a&&a(e,t))},s=e=>{o(e,!0),n(e)};t((t=>{o(t),e(t)}),s,(e=>a=e)).catch(s)}));var t},rt=ge.isStandardBrowserEnv?{write:function(e,t,n,a,i,o){const s=[];s.push(e+"="+encodeURIComponent(t)),ae.isNumber(n)&&s.push("expires="+new Date(n).toGMTString()),ae.isString(a)&&s.push("path="+a),ae.isString(i)&&s.push("domain="+i),!0===o&&s.push("secure"),document.cookie=s.join("; ")},read:function(e){const t=document.cookie.match(new RegExp("(^|;\\s*)("+e+")=([^;]*)"));return t?decodeURIComponent(t[3]):null},remove:function(e){this.write(e,"",Date.now()-864e5)}}:{write:function(){},read:function(){return null},remove:function(){}},ct=ge.isStandardBrowserEnv?function(){const e=/(msie|trident)/i.test(navigator.userAgent),t=document.createElement("a");let n;function a(n){let a=n;return e&&(t.setAttribute("href",a),a=t.href),t.setAttribute("href",a),{href:t.href,protocol:t.protocol?t.protocol.replace(/:$/,""):"",host:t.host,search:t.search?t.search.replace(/^\?/,""):"",hash:t.hash?t.hash.replace(/^#/,""):"",hostname:t.hostname,port:t.port,pathname:"/"===t.pathname.charAt(0)?t.pathname:"/"+t.pathname}}return n=a(window.location.href),function(e){const t=ae.isString(e)?a(e):e;return t.protocol===n.protocol&&t.host===n.host}}():function(){return!0};function pt(e,t){let n=0;const a=Le(50,250);return i=>{const o=i.loaded,s=i.lengthComputable?i.total:void 0,r=o-n,c=a(r);n=o;const p={loaded:o,total:s,progress:s?o/s:void 0,bytes:r,rate:c||void 0,estimated:c&&s&&o<=s?(s-o)/c:void 0,event:i};p[t?"download":"upload"]=!0,e(p)}}const ut={http:st,xhr:"undefined"!=typeof XMLHttpRequest&&function(e){return new Promise((function(t,n){let a=e.data;const i=Se.from(e.headers).normalize(),o=e.responseType;let s,r;function c(){e.cancelToken&&e.cancelToken.unsubscribe(s),e.signal&&e.signal.removeEventListener("abort",s)}ae.isFormData(a)&&(ge.isStandardBrowserEnv||ge.isStandardBrowserWebWorkerEnv?i.setContentType(!1):i.getContentType(/^\s*multipart\/form-data/)?ae.isString(r=i.getContentType())&&i.setContentType(r.replace(/^\s*(multipart\/form-data);+/,"$1")):i.setContentType("multipart/form-data"));let p=new XMLHttpRequest;if(e.auth){const t=e.auth.username||"",n=e.auth.password?unescape(encodeURIComponent(e.auth.password)):"";i.set("Authorization","Basic "+btoa(t+":"+n))}const u=Ae(e.baseURL,e.url);function l(){if(!p)return;const a=Se.from("getAllResponseHeaders"in p&&p.getAllResponseHeaders());qe((function(e){t(e),c()}),(function(e){n(e),c()}),{data:o&&"text"!==o&&"json"!==o?p.response:p.responseText,status:p.status,statusText:p.statusText,headers:a,config:e,request:p}),p=null}if(p.open(e.method.toUpperCase(),ve(u,e.params,e.paramsSerializer),!0),p.timeout=e.timeout,"onloadend"in p?p.onloadend=l:p.onreadystatechange=function(){p&&4===p.readyState&&(0!==p.status||p.responseURL&&0===p.responseURL.indexOf("file:"))&&setTimeout(l)},p.onabort=function(){p&&(n(new ie("Request aborted",ie.ECONNABORTED,e,p)),p=null)},p.onerror=function(){n(new ie("Network Error",ie.ERR_NETWORK,e,p)),p=null},p.ontimeout=function(){let t=e.timeout?"timeout of "+e.timeout+"ms exceeded":"timeout exceeded";const a=e.transitional||be;e.timeoutErrorMessage&&(t=e.timeoutErrorMessage),n(new ie(t,a.clarifyTimeoutError?ie.ETIMEDOUT:ie.ECONNABORTED,e,p)),p=null},ge.isStandardBrowserEnv){const t=ct(u)&&e.xsrfCookieName&&rt.read(e.xsrfCookieName);t&&i.set(e.xsrfHeaderName,t)}void 0===a&&i.setContentType(null),"setRequestHeader"in p&&ae.forEach(i.toJSON(),(function(e,t){p.setRequestHeader(t,e)})),ae.isUndefined(e.withCredentials)||(p.withCredentials=!!e.withCredentials),o&&"json"!==o&&(p.responseType=e.responseType),"function"==typeof e.onDownloadProgress&&p.addEventListener("progress",pt(e.onDownloadProgress,!0)),"function"==typeof e.onUploadProgress&&p.upload&&p.upload.addEventListener("progress",pt(e.onUploadProgress)),(e.cancelToken||e.signal)&&(s=t=>{p&&(n(!t||t.type?new Te(null,e,p):t),p.abort(),p=null)},e.cancelToken&&e.cancelToken.subscribe(s),e.signal&&(e.signal.aborted?s():e.signal.addEventListener("abort",s)));const d=Me(u);d&&-1===ge.protocols.indexOf(d)?n(new ie("Unsupported protocol "+d+":",ie.ERR_BAD_REQUEST,e)):p.send(a||null)}))}};ae.forEach(ut,((e,t)=>{if(e){try{Object.defineProperty(e,"name",{value:t})}catch(e){}Object.defineProperty(e,"adapterName",{value:t})}}));const lt=e=>`- ${e}`,dt=e=>ae.isFunction(e)||null===e||!1===e,mt=e=>{e=ae.isArray(e)?e:[e];const{length:t}=e;let n,a;const i={};for(let o=0;o`adapter ${e} `+(!1===t?"is not supported by the environment":"is not available in the build")));throw new ie("There is no suitable adapter to dispatch the request "+(t?e.length>1?"since :\n"+e.map(lt).join("\n"):" "+lt(e[0]):"as no adapter specified"),"ERR_NOT_SUPPORT")}return a};function ft(e){if(e.cancelToken&&e.cancelToken.throwIfRequested(),e.signal&&e.signal.aborted)throw new Te(null,e)}function ht(e){ft(e),e.headers=Se.from(e.headers),e.data=Ee.call(e,e.transformRequest),-1!==["post","put","patch"].indexOf(e.method)&&e.headers.setContentType("application/x-www-form-urlencoded",!1);return mt(e.adapter||we.adapter)(e).then((function(t){return ft(e),t.data=Ee.call(e,e.transformResponse,t),t.headers=Se.from(t.headers),t}),(function(t){return Ce(t)||(ft(e),t&&t.response&&(t.response.data=Ee.call(e,e.transformResponse,t.response),t.response.headers=Se.from(t.response.headers))),Promise.reject(t)}))}const vt=e=>e instanceof Se?e.toJSON():e;function xt(e,t){t=t||{};const n={};function a(e,t,n){return ae.isPlainObject(e)&&ae.isPlainObject(t)?ae.merge.call({caseless:n},e,t):ae.isPlainObject(t)?ae.merge({},t):ae.isArray(t)?t.slice():t}function i(e,t,n){return ae.isUndefined(t)?ae.isUndefined(e)?void 0:a(void 0,e,n):a(e,t,n)}function o(e,t){if(!ae.isUndefined(t))return a(void 0,t)}function s(e,t){return ae.isUndefined(t)?ae.isUndefined(e)?void 0:a(void 0,e):a(void 0,t)}function r(n,i,o){return o in t?a(n,i):o in e?a(void 0,n):void 0}const c={url:o,method:o,data:o,baseURL:s,transformRequest:s,transformResponse:s,paramsSerializer:s,timeout:s,timeoutMessage:s,withCredentials:s,adapter:s,responseType:s,xsrfCookieName:s,xsrfHeaderName:s,onUploadProgress:s,onDownloadProgress:s,decompress:s,maxContentLength:s,maxBodyLength:s,beforeRedirect:s,transport:s,httpAgent:s,httpsAgent:s,cancelToken:s,socketPath:s,responseEncoding:s,validateStatus:r,headers:(e,t)=>i(vt(e),vt(t),!0)};return ae.forEach(Object.keys(Object.assign({},e,t)),(function(a){const o=c[a]||i,s=o(e[a],t[a],a);ae.isUndefined(s)&&o!==r||(n[a]=s)})),n}const bt={};["object","boolean","number","function","string","symbol"].forEach(((e,t)=>{bt[e]=function(n){return typeof n===e||"a"+(t<1?"n ":" ")+e}}));const gt={};bt.transitional=function(e,t,n){function a(e,t){return"[Axios v1.6.0] Transitional option '"+e+"'"+t+(n?". "+n:"")}return(n,i,o)=>{if(!1===e)throw new ie(a(i," has been removed"+(t?" in "+t:"")),ie.ERR_DEPRECATED);return t&&!gt[i]&&(gt[i]=!0,console.warn(a(i," has been deprecated since v"+t+" and will be removed in the near future"))),!e||e(n,i,o)}};const yt={assertOptions:function(e,t,n){if("object"!=typeof e)throw new ie("options must be an object",ie.ERR_BAD_OPTION_VALUE);const a=Object.keys(e);let i=a.length;for(;i-- >0;){const o=a[i],s=t[o];if(s){const t=e[o],n=void 0===t||s(t,o,e);if(!0!==n)throw new ie("option "+o+" must be "+n,ie.ERR_BAD_OPTION_VALUE)}else if(!0!==n)throw new ie("Unknown option "+o,ie.ERR_BAD_OPTION)}},validators:bt},_t=yt.validators;class Axios{constructor(e){this.defaults=e,this.interceptors={request:new xe,response:new xe}}request(e,t){"string"==typeof e?(t=t||{}).url=e:t=e||{},t=xt(this.defaults,t);const{transitional:n,paramsSerializer:a,headers:i}=t;void 0!==n&&yt.assertOptions(n,{silentJSONParsing:_t.transitional(_t.boolean),forcedJSONParsing:_t.transitional(_t.boolean),clarifyTimeoutError:_t.transitional(_t.boolean)},!1),null!=a&&(ae.isFunction(a)?t.paramsSerializer={serialize:a}:yt.assertOptions(a,{encode:_t.function,serialize:_t.function},!0)),t.method=(t.method||this.defaults.method||"get").toLowerCase();let o=i&&ae.merge(i.common,i[t.method]);i&&ae.forEach(["delete","get","head","post","put","patch","common"],(e=>{delete i[e]})),t.headers=Se.concat(o,i);const s=[];let r=!0;this.interceptors.request.forEach((function(e){"function"==typeof e.runWhen&&!1===e.runWhen(t)||(r=r&&e.synchronous,s.unshift(e.fulfilled,e.rejected))}));const c=[];let p;this.interceptors.response.forEach((function(e){c.push(e.fulfilled,e.rejected)}));let u,l=0;if(!r){const e=[ht.bind(this),void 0];for(e.unshift.apply(e,s),e.push.apply(e,c),u=e.length,p=Promise.resolve(t);l{if(!n._listeners)return;let t=n._listeners.length;for(;t-- >0;)n._listeners[t](e);n._listeners=null})),this.promise.then=e=>{let t;const a=new Promise((e=>{n.subscribe(e),t=e})).then(e);return a.cancel=function(){n.unsubscribe(t)},a},e((function(e,a,i){n.reason||(n.reason=new Te(e,a,i),t(n.reason))}))}throwIfRequested(){if(this.reason)throw this.reason}subscribe(e){this.reason?e(this.reason):this._listeners?this._listeners.push(e):this._listeners=[e]}unsubscribe(e){if(!this._listeners)return;const t=this._listeners.indexOf(e);-1!==t&&this._listeners.splice(t,1)}static source(){let e;return{token:new CancelToken((function(t){e=t})),cancel:e}}}const jt=CancelToken;const Ot={Continue:100,SwitchingProtocols:101,Processing:102,EarlyHints:103,Ok:200,Created:201,Accepted:202,NonAuthoritativeInformation:203,NoContent:204,ResetContent:205,PartialContent:206,MultiStatus:207,AlreadyReported:208,ImUsed:226,MultipleChoices:300,MovedPermanently:301,Found:302,SeeOther:303,NotModified:304,UseProxy:305,Unused:306,TemporaryRedirect:307,PermanentRedirect:308,BadRequest:400,Unauthorized:401,PaymentRequired:402,Forbidden:403,NotFound:404,MethodNotAllowed:405,NotAcceptable:406,ProxyAuthenticationRequired:407,RequestTimeout:408,Conflict:409,Gone:410,LengthRequired:411,PreconditionFailed:412,PayloadTooLarge:413,UriTooLong:414,UnsupportedMediaType:415,RangeNotSatisfiable:416,ExpectationFailed:417,ImATeapot:418,MisdirectedRequest:421,UnprocessableEntity:422,Locked:423,FailedDependency:424,TooEarly:425,UpgradeRequired:426,PreconditionRequired:428,TooManyRequests:429,RequestHeaderFieldsTooLarge:431,UnavailableForLegalReasons:451,InternalServerError:500,NotImplemented:501,BadGateway:502,ServiceUnavailable:503,GatewayTimeout:504,HttpVersionNotSupported:505,VariantAlsoNegotiates:506,InsufficientStorage:507,LoopDetected:508,NotExtended:510,NetworkAuthenticationRequired:511};Object.entries(Ot).forEach((([e,t])=>{Ot[t]=e}));const kt=Ot;const Pt=function e(t){const n=new wt(t),a=j(wt.prototype.request,n);return ae.extend(a,wt.prototype,n,{allOwnKeys:!0}),ae.extend(a,n,null,{allOwnKeys:!0}),a.create=function(n){return e(xt(t,n))},a}(we);Pt.Axios=wt,Pt.CanceledError=Te,Pt.CancelToken=jt,Pt.isCancel=Ce,Pt.VERSION=De,Pt.toFormData=le,Pt.AxiosError=ie,Pt.Cancel=Pt.CanceledError,Pt.all=function(e){return Promise.all(e)},Pt.spread=function(e){return function(t){return e.apply(null,t)}},Pt.isAxiosError=function(e){return ae.isObject(e)&&!0===e.isAxiosError},Pt.mergeConfig=xt,Pt.AxiosHeaders=Se,Pt.formToJSON=e=>ye(ae.isHTMLForm(e)?new FormData(e):e),Pt.getAdapter=mt,Pt.HttpStatusCode=kt,Pt.default=Pt,e.exports=Pt},3765:e=>{"use strict";e.exports=JSON.parse('{"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/3gpphal+json":{"source":"iana","compressible":true},"application/3gpphalforms+json":{"source":"iana","compressible":true},"application/a2l":{"source":"iana"},"application/ace+cbor":{"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/at+jwt":{"source":"iana"},"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/captive+json":{"source":"iana","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/city+json":{"source":"iana","compressible":true},"application/clr":{"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,"extensions":["cpl"]},"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/dash-patch+xml":{"source":"iana","compressible":true,"extensions":["mpp"]},"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":["es","ecma"]},"application/edi-consent":{"source":"iana"},"application/edi-x12":{"source":"iana","compressible":false},"application/edifact":{"source":"iana","compressible":false},"application/efi":{"source":"iana"},"application/elm+json":{"source":"iana","charset":"UTF-8","compressible":true},"application/elm+xml":{"source":"iana","compressible":true},"application/emergencycalldata.cap+xml":{"source":"iana","charset":"UTF-8","compressible":true},"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/express":{"source":"iana","extensions":["exp"]},"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/jscalendar+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":{"source":"iana","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,"extensions":["mpf"]},"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/missing-blocks+cbor-seq":{"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},"application/mrb-publish+xml":{"source":"iana","compressible":true},"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/oauth-authz-req+jwt":{"source":"iana"},"application/oblivious-dns-message":{"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/opc-nodeset+xml":{"source":"iana","compressible":true},"application/oscore":{"source":"iana"},"application/oxps":{"source":"iana","extensions":["oxps"]},"application/p21":{"source":"iana"},"application/p21+zip":{"source":"iana","compressible":false},"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","extensions":["asc"]},"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.cyn":{"source":"iana","charset":"7-BIT"},"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/sarif+json":{"source":"iana","compressible":true},"application/sarif-external-properties+json":{"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/spdx+json":{"source":"iana","compressible":true},"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/token-introspection+jwt":{"source":"iana"},"application/toml":{"compressible":true,"extensions":["toml"]},"application/trickle-ice-sdpfrag":{"source":"iana"},"application/trig":{"source":"iana","extensions":["trig"]},"application/ttml+xml":{"source":"iana","compressible":true,"extensions":["ttml"]},"application/tve-trigger":{"source":"iana"},"application/tzif":{"source":"iana"},"application/tzif-leap":{"source":"iana"},"application/ubjson":{"compressible":false,"extensions":["ubj"]},"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,"extensions":["td"]},"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.5gnas":{"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.gtpc":{"source":"iana"},"application/vnd.3gpp.interworking-data":{"source":"iana"},"application/vnd.3gpp.lpp":{"source":"iana"},"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.ngap":{"source":"iana"},"application/vnd.3gpp.pfcp":{"source":"iana"},"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.s1ap":{"source":"iana"},"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.cmoca-cmresource":{"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-cmtable":{"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.age":{"source":"iana","extensions":["age"]},"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.arrow.file":{"source":"iana"},"application/vnd.apache.arrow.stream":{"source":"iana"},"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":["key"]},"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.cryptomator.encrypted":{"source":"iana"},"application/vnd.cryptomator.vault":{"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.cyclonedx+json":{"source":"iana","compressible":true},"application/vnd.cyclonedx+xml":{"source":"iana","compressible":true},"application/vnd.d2l.coursepackage1p0+zip":{"source":"iana","compressible":false},"application/vnd.d3m-dataset":{"source":"iana"},"application/vnd.d3m-problem":{"source":"iana"},"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","extensions":["dbf"]},"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.eclipse.ditto+json":{"source":"iana","compressible":true},"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.eu.kasparian.car+json":{"source":"iana","compressible":true},"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.familysearch.gedcom+zip":{"source":"iana","compressible":false},"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.fujifilm.fb.docuworks":{"source":"iana"},"application/vnd.fujifilm.fb.docuworks.binder":{"source":"iana"},"application/vnd.fujifilm.fb.docuworks.container":{"source":"iana"},"application/vnd.fujifilm.fb.jfi+xml":{"source":"iana","compressible":true},"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.slides":{"source":"iana"},"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.hl7cda+xml":{"source":"iana","charset":"UTF-8","compressible":true},"application/vnd.hl7v2+xml":{"source":"iana","charset":"UTF-8","compressible":true},"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","extensions":["mvt"]},"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.maxar.archive.3tz+zip":{"source":"iana","compressible":false},"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.nacamar.ybrid+json":{"source":"iana","compressible":true},"application/vnd.ncd.control":{"source":"iana"},"application/vnd.ncd.reference":{"source":"iana"},"application/vnd.nearst.inv+json":{"source":"iana","compressible":true},"application/vnd.nebumind.line":{"source":"iana"},"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+cbor":{"source":"iana"},"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.opentimestamps.ots":{"source":"iana"},"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","extensions":["rar"]},"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.resilient.logic":{"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.seis+json":{"source":"iana","compressible":true},"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.sycle+xml":{"source":"iana","compressible":true},"application/vnd.syft+json":{"source":"iana","compressible":true},"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.veritone.aion+json":{"source":"iana","compressible":true},"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.dpp":{"source":"iana"},"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":{"source":"iana","compressible":true,"extensions":["wasm"]},"application/watcherinfo+xml":{"source":"iana","compressible":true,"extensions":["wif"]},"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-iwork-keynote-sffkey":{"extensions":["key"]},"application/x-iwork-numbers-sffnumbers":{"extensions":["numbers"]},"application/x-iwork-pages-sffpages":{"extensions":["pages"]},"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},"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":["xsl","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","extensions":["amr"]},"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","opus"]},"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/scip":{"source":"iana"},"audio/silk":{"source":"apache","extensions":["sil"]},"audio/smv":{"source":"iana"},"audio/smv-qcp":{"source":"iana"},"audio/smv0":{"source":"iana"},"audio/sofa":{"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/tsvcis":{"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","extensions":["avci"]},"image/avcs":{"source":"iana","extensions":["avcs"]},"image/avif":{"source":"iana","compressible":false,"extensions":["avif"]},"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/ktx2":{"source":"iana","extensions":["ktx2"]},"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","compressible":true,"extensions":["ico"]},"image/vnd.mix":{"source":"iana"},"image/vnd.mozilla.apng":{"source":"iana"},"image/vnd.ms-dds":{"compressible":true,"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.pco.b16":{"source":"iana","extensions":["b16"]},"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/e57":{"source":"iana"},"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/step":{"source":"iana"},"model/step+xml":{"source":"iana","compressible":true,"extensions":["stpx"]},"model/step+zip":{"source":"iana","compressible":false,"extensions":["stpz"]},"model/step-xml+zip":{"source":"iana","compressible":false,"extensions":["stpxz"]},"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.pytha.pyox":{"source":"iana"},"model/vnd.rosette.annotated-data-model":{"source":"iana"},"model/vnd.sap.vds":{"source":"iana","extensions":["vds"]},"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/cql":{"source":"iana"},"text/cql-expression":{"source":"iana"},"text/cql-identifier":{"source":"iana"},"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/fhirpath":{"source":"iana"},"text/flexfec":{"source":"iana"},"text/fwdred":{"source":"iana"},"text/gff3":{"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/shaclc":{"source":"iana"},"text/shex":{"source":"iana","extensions":["shex"]},"text/slim":{"extensions":["slim","slm"]},"text/spdx":{"source":"iana","extensions":["spdx"]},"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.familysearch.gedcom":{"source":"iana","extensions":["ged"]},"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.hans":{"source":"iana"},"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":{"compressible":true,"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/av1":{"source":"iana"},"video/bmpeg":{"source":"iana"},"video/bt656":{"source":"iana"},"video/celb":{"source":"iana"},"video/dv":{"source":"iana"},"video/encaprtp":{"source":"iana"},"video/ffv1":{"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","extensions":["m4s"]},"video/jpeg":{"source":"iana","extensions":["jpgv"]},"video/jpeg2000":{"source":"iana"},"video/jpm":{"source":"apache","extensions":["jpm","jpgm"]},"video/jxsv":{"source":"iana"},"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/scip":{"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/vp9":{"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}}')}},t={};function n(a){var i=t[a];if(void 0!==i)return i.exports;var o=t[a]={id:a,loaded:!1,exports:{}};return e[a].call(o.exports,o,o.exports,n),o.loaded=!0,o.exports}n.nmd=e=>(e.paths=[],e.children||(e.children=[]),e);var a=n(7530);return a=a.default})())); +//# sourceMappingURL=data:application/json;charset=utf-8;base64,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QUFBLE1BQUE3TCxHQUNBNGpCLEVBQUEvWCxFQUFBLE1BQUE3TCxHQUNBNGpCLEVBQUEvWCxFQUFBLE1BQUE3TCxHQUNBNGpCLEVBQUEvWCxFQUFBLE1BQUE3TCxHQUNBNGpCLEVBQUEvWCxFQUFBLE1BQUE3TCxHQUNBNGpCLEVBQUEvWCxFQUFBLE1BQUE3TCxHQUNBNGpCLEVBQUEvWCxFQUFBLEtBQUE3TCxHQUNBNGpCLEVBQUEvWCxFQUFBLE1BQUE3TCxHQUNBNGpCLEVBQUEvWCxFQUFBLE1BQUE3TCxHQUNBNGpCLEVBQUEvWCxFQUFBLEtBQUE3TCxHQUNBNGpCLEVBQUEvWCxFQUFBLE1BQUE3TCxHQUNBNGpCLEVBQUEvWCxFQUFBLEtBQUE3TCxHQUNBNGpCLEVBQUEvWCxFQUFBLE1BQUE3TCxzNEJDYkE0akIsRUFBQS9YLEVBQUEsTUFBQTdMLEdBQ0E0akIsRUFBQS9YLEVBQUEsTUFBQTdMLEdBQ0E0akIsRUFBQS9YLEVBQUEsTUFBQTdMLEdBQ0E0akIsRUFBQS9YLEVBQUEsTUFBQTdMLEdBQ0E0akIsRUFBQS9YLEVBQUEsS0FBQTdMLHM0QkNKQTRqQixFQUFBL1gsRUFBQSxNQUFBN0wsR0FDQTRqQixFQUFBL1gsRUFBQSxNQUFBN0wsR0FDQTRqQixFQUFBL1gsRUFBQSxNQUFBN0wsR0FDQTRqQixFQUFBL1gsRUFBQSxNQUFBN0wsR0FDQTRqQixFQUFBL1gsRUFBQSxLQUFBN0wsdWxCQ0pBNGpCLEVBQUEvWCxFQUFBLE1BQUE3TCxzbEJDQUE0akIsRUFBQS9YLEVBQUEsTUFBQTdMLHVsQkNBQTRqQixFQUFBL1gsRUFBQSxNQUFBN0wsdWxCQ0FBNGpCLEVBQUEvWCxFQUFBLE1BQUE3TCxtcUJDQUE0akIsRUFBQS9YLEVBQUEsTUFBQTdMLEdBQ0E0akIsRUFBQS9YLEVBQUEsTUFBQTdMLHVsQkNEQTRqQixFQUFBL1gsRUFBQSxNQUFBN0wsdWxCQ0FBNGpCLEVBQUEvWCxFQUFBLE1BQUE3TCx1bEJDQUE0akIsRUFBQS9YLEVBQUEsTUFBQTdMLHVsQkNBQTRqQixFQUFBL1gsRUFBQSxNQUFBN0wsdTRC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LEVBQWVILEVBQXlCRSxHQUM1QyxRQUFxQng1QyxJQUFqQnk1QyxFQUNILE9BQU9BLEVBQWF4L0MsUUFHckIsSUFBSUMsRUFBU28vQyxFQUF5QkUsR0FBWSxDQUNqRHB0QyxHQUFJb3RDLEVBQ0pySyxRQUFRLEVBQ1JsMUMsUUFBUyxDQUFDLEdBVVgsT0FOQXkvQyxFQUFvQkYsR0FBVWo3QyxLQUFLckUsRUFBT0QsUUFBU0MsRUFBUUEsRUFBT0QsUUFBU3MvQyxHQUczRXIvQyxFQUFPaTFDLFFBQVMsRUFHVGoxQyxFQUFPRCxPQUNmLENDekJBcy9DLEVBQW9CSSxJQUFPei9DLElBQzFCQSxFQUFPMC9DLE1BQVEsR0FDVjEvQyxFQUFPMi9DLFdBQVUzL0MsRUFBTzIvQyxTQUFXLElBQ2pDMy9DLEdDQVIsSUFBSTQvQyxFQUFzQlAsRUFBb0IsMEJ6R085QyIsInNvdXJjZXMiOlsid2VicGFjazovL21haWxndW4vd2VicGFjay91bml2ZXJzYWxNb2R1bGVEZWZpbml0aW9uIiwid2VicGFjazovL21haWxndW4vLi9ub2RlX21vZHVsZXMvYXN5bmNraXQvaW5kZXguanMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL25vZGVfbW9kdWxlcy9hc3luY2tpdC9saWIvYWJvcnQuanMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL25vZGVfbW9kdWxlcy9hc3luY2tpdC9saWIvYXN5bmMuanMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL25vZGVfbW9kdWxlcy9hc3luY2tpdC9saWIvZGVmZXIuanMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL25vZGVfbW9kdWxlcy9hc3luY2tpdC9saWIvaXRlcmF0ZS5qcyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbm9kZV9tb2R1bGVzL2FzeW5ja2l0L2xpYi9zdGF0ZS5qcyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbm9kZV9tb2R1bGVzL2FzeW5ja2l0L2xpYi90ZXJtaW5hdG9yLmpzIiwid2VicGFjazovL21haWxndW4vLi9ub2RlX21vZHVsZXMvYXN5bmNraXQvcGFyYWxsZWwuanMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL25vZGVfbW9kdWxlcy9hc3luY2tpdC9zZXJpYWwuanMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL25vZGVfbW9kdWxlcy9hc3luY2tpdC9zZXJpYWxPcmRlcmVkLmpzIiwid2VicGFjazovL21haWxndW4vLi9ub2RlX21vZHVsZXMvYXhpb3Mvbm9kZV9tb2R1bGVzL2Zvcm0tZGF0YS9saWIvZm9ybV9kYXRhLmpzIiwid2VicGFjazovL21haWxndW4vLi9ub2RlX21vZHVsZXMvYXhpb3Mvbm9kZV9tb2R1bGVzL2Zvcm0tZGF0YS9saWIvcG9wdWxhdGUuanMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL2xpYi9DbGFzc2VzL0RvbWFpbnMvZG9tYWluLnRzIiwid2VicGFjazovL21haWxndW4vLi9saWIvQ2xhc3Nlcy9Eb21haW5zL2RvbWFpbnNDbGllbnQudHMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL2xpYi9DbGFzc2VzL0RvbWFpbnMvZG9tYWluc0NyZWRlbnRpYWxzLnRzIiwid2VicGFjazovL21haWxndW4vLi9saWIvQ2xhc3Nlcy9Eb21haW5zL2RvbWFpbnNUYWdzLnRzIiwid2VicGFjazovL21haWxndW4vLi9saWIvQ2xhc3Nlcy9Eb21haW5zL2RvbWFpbnNUZW1wbGF0ZXMudHMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL2xpYi9DbGFzc2VzL0V2ZW50cy50cyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbGliL0NsYXNzZXMvSVBQb29scy50cyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbGliL0NsYXNzZXMvSVBzLnRzIiwid2VicGFjazovL21haWxndW4vLi9saWIvQ2xhc3Nlcy9NYWlsZ3VuQ2xpZW50LnRzIiwid2VicGFjazovL21haWxndW4vLi9saWIvQ2xhc3Nlcy9NYWlsaW5nTGlzdHMvbWFpbExpc3RNZW1iZXJzLnRzIiwid2VicGFjazovL21haWxndW4vLi9saWIvQ2xhc3Nlcy9NYWlsaW5nTGlzdHMvbWFpbGluZ0xpc3RzLnRzIiwid2VicGFjazovL21haWxndW4vLi9saWIvQ2xhc3Nlcy9NZXNzYWdlcy50cyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbGliL0NsYXNzZXMvUm91dGVzLnRzIiwid2VicGFjazovL21haWxndW4vLi9saWIvQ2xhc3Nlcy9TdGF0cy9TdGF0c0NsaWVudC50cyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbGliL0NsYXNzZXMvU3RhdHMvU3RhdHNDb250YWluZXIudHMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL2xpYi9DbGFzc2VzL1N1YmFjY291bnRzLnRzIiwid2VicGFjazovL21haWxndW4vLi9saWIvQ2xhc3Nlcy9TdXBwcmVzc2lvbnMvQm91bmNlLnRzIiwid2VicGFjazovL21haWxndW4vLi9saWIvQ2xhc3Nlcy9TdXBwcmVzc2lvbnMvQ29tcGxhaW50LnRzIiwid2VicGFjazovL21haWxndW4vLi9saWIvQ2xhc3Nlcy9TdXBwcmVzc2lvbnMvU3VwcHJlc3Npb24udHMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL2xpYi9DbGFzc2VzL1N1cHByZXNzaW9ucy9TdXBwcmVzc2lvbnNDbGllbnQudHMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL2xpYi9DbGFzc2VzL1N1cHByZXNzaW9ucy9VbnN1YnNjcmliZS50cyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbGliL0NsYXNzZXMvU3VwcHJlc3Npb25zL1doaXRlTGlzdC50cyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbGliL0NsYXNzZXMvVmFsaWRhdGlvbnMvbXVsdGlwbGVWYWxpZGF0aW9uLnRzIiwid2VicGFjazovL21haWxndW4vLi9saWIvQ2xhc3Nlcy9WYWxpZGF0aW9ucy92YWxpZGF0ZS50cyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbGliL0NsYXNzZXMvV2ViaG9va3MudHMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL2xpYi9DbGFzc2VzL2NvbW1vbi9FcnJvci50cyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbGliL0NsYXNzZXMvY29tbW9uL0Zvcm1EYXRhQnVpbGRlci50cyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbGliL0NsYXNzZXMvY29tbW9uL05hdmlnYXRpb25UaHJ1UGFnZXMudHMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL2xpYi9DbGFzc2VzL2NvbW1vbi9SZXF1ZXN0LnRzIiwid2VicGFjazovL21haWxndW4vLi9saWIvRW51bXMvaW5kZXgudHMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL2xpYi9JbnRlcmZhY2VzL0NvbW1vbi9pbmRleC50cyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbGliL0ludGVyZmFjZXMvRG9tYWlucy9pbmRleC50cyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbGliL0ludGVyZmFjZXMvRXZlbnRDbGllbnQvaW5kZXgudHMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL2xpYi9JbnRlcmZhY2VzL0lQUG9vbHMvaW5kZXgudHMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL2xpYi9JbnRlcmZhY2VzL0lQcy9pbmRleC50cyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbGliL0ludGVyZmFjZXMvTWFpbGd1bkNsaWVudC9pbmRleC50cyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbGliL0ludGVyZmFjZXMvTWFpbGluZ0xpc3RzL2luZGV4LnRzIiwid2VicGFjazovL21haWxndW4vLi9saWIvSW50ZXJmYWNlcy9NZXNzYWdlcy9pbmRleC50cyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbGliL0ludGVyZmFjZXMvUm91dGVzL2luZGV4LnRzIiwid2VicGFjazovL21haWxndW4vLi9saWIvSW50ZXJmYWNlcy9TdGF0cy9pbmRleC50cyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbGliL0ludGVyZmFjZXMvU3ViYWNjb3VudHMvaW5kZXgudHMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL2xpYi9JbnRlcmZhY2VzL1N1cHByZXNzaW9ucy9pbmRleC50cyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbGliL0ludGVyZmFjZXMvVmFsaWRhdGlvbnMvaW5kZXgudHMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL2xpYi9JbnRlcmZhY2VzL1dlYmhvb2tzL2luZGV4LnRzIiwid2VicGFjazovL21haWxndW4vLi9saWIvSW50ZXJmYWNlcy9pbmRleC50cyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbGliL1R5cGVzL0NvbW1vbi9pbmRleC50cyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbGliL1R5cGVzL0RvbWFpbnMvaW5kZXgudHMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL2xpYi9UeXBlcy9FdmVudHMvaW5kZXgudHMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL2xpYi9UeXBlcy9JUFBvb2xzL2luZGV4LnRzIiwid2VicGFjazovL21haWxndW4vLi9saWIvVHlwZXMvSVBzL2luZGV4LnRzIiwid2VicGFjazovL21haWxndW4vLi9saWIvVHlwZXMvTWFpbGd1bkNsaWVudC9pbmRleC50cyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbGliL1R5cGVzL01haWxpbmdMaXN0cy9pbmRleC50cyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbGliL1R5cGVzL01lc3NhZ2VzL2luZGV4LnRzIiwid2VicGFjazovL21haWxndW4vLi9saWIvVHlwZXMvUm91dGVzL2luZGV4LnRzIiwid2VicGFjazovL21haWxndW4vLi9saWIvVHlwZXMvU3RhdHMvaW5kZXgudHMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL2xpYi9UeXBlcy9TdWJhY2NvdW50cy9pbmRleC50cyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbGliL1R5cGVzL1N1cHByZXNzaW9ucy9pbmRleC50cyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbGliL1R5cGVzL1ZhbGlkYXRpb25zL2luZGV4LnRzIiwid2VicGFjazovL21haWxndW4vLi9saWIvVHlwZXMvV2ViaG9va3MvaW5kZXgudHMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL2xpYi9UeXBlcy9pbmRleC50cyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbGliL2luZGV4LnRzIiwid2VicGFjazovL21haWxndW4vLi9ub2RlX21vZHVsZXMvYmFzZS02NC9iYXNlNjQuanMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL25vZGVfbW9kdWxlcy9jb21iaW5lZC1zdHJlYW0vbGliL2NvbWJpbmVkX3N0cmVhbS5qcyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbm9kZV9tb2R1bGVzL2RlYnVnL3NyYy9icm93c2VyLmpzIiwid2VicGFjazovL21haWxndW4vLi9ub2RlX21vZHVsZXMvZGVidWcvc3JjL2NvbW1vbi5qcyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbm9kZV9tb2R1bGVzL2RlYnVnL3NyYy9pbmRleC5qcyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbm9kZV9tb2R1bGVzL2RlYnVnL3NyYy9ub2RlLmpzIiwid2VicGFjazovL21haWxndW4vLi9ub2RlX21vZHVsZXMvZGVsYXllZC1zdHJlYW0vbGliL2RlbGF5ZWRfc3RyZWFtLmpzIiwid2VicGFjazovL21haWxndW4vLi9ub2RlX21vZHVsZXMvZm9sbG93LXJlZGlyZWN0cy9kZWJ1Zy5qcyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbm9kZV9tb2R1bGVzL2ZvbGxvdy1yZWRpcmVjdHMvaW5kZXguanMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL25vZGVfbW9kdWxlcy9oYXMtZmxhZy9pbmRleC5qcyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbm9kZV9tb2R1bGVzL21pbWUtZGIvaW5kZXguanMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL25vZGVfbW9kdWxlcy9taW1lLXR5cGVzL2luZGV4LmpzIiwid2VicGFjazovL21haWxndW4vLi9ub2RlX21vZHVsZXMvbXMvaW5kZXguanMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL25vZGVfbW9kdWxlcy9wcm94eS1mcm9tLWVudi9pbmRleC5qcyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbm9kZV9tb2R1bGVzL3N1cHBvcnRzLWNvbG9yL2luZGV4LmpzIiwid2VicGFjazovL21haWxndW4vLi9ub2RlX21vZHVsZXMvdXJsLWpvaW4vbGliL3VybC1qb2luLmpzIiwid2VicGFjazovL21haWxndW4vZXh0ZXJuYWwgbm9kZS1jb21tb25qcyBcImFzc2VydFwiIiwid2VicGFjazovL21haWxndW4vZXh0ZXJuYWwgbm9kZS1jb21tb25qcyBcImV2ZW50c1wiIiwid2VicGFjazovL21haWxndW4vZXh0ZXJuYWwgbm9kZS1jb21tb25qcyBcImZzXCIiLCJ3ZWJwYWNrOi8vbWFpbGd1bi9leHRlcm5hbCBub2RlLWNvbW1vbmpzIFwiaHR0cFwiIiwid2VicGFjazovL21haWxndW4vZXh0ZXJuYWwgbm9kZS1jb21tb25qcyBcImh0dHBzXCIiLCJ3ZWJwYWNrOi8vbWFpbGd1bi9leHRlcm5hbCBub2RlLWNvbW1vbmpzIFwib3NcIiIsIndlYnBhY2s6Ly9tYWlsZ3VuL2V4dGVybmFsIG5vZGUtY29tbW9uanMgXCJwYXRoXCIiLCJ3ZWJwYWNrOi8vbWFpbGd1bi9leHRlcm5hbCBub2RlLWNvbW1vbmpzIFwic3RyZWFtXCIiLCJ3ZWJwYWNrOi8vbWFpbGd1bi9leHRlcm5hbCBub2RlLWNvbW1vbmpzIFwidHR5XCIiLCJ3ZWJwYWNrOi8vbWFpbGd1bi9leHRlcm5hbCBub2RlLWNvbW1vbmpzIFwidXJsXCIiLCJ3ZWJwYWNrOi8vbWFpbGd1bi9leHRlcm5hbCBub2RlLWNvbW1vbmpzIFwidXRpbFwiIiwid2VicGFjazovL21haWxndW4vZXh0ZXJuYWwgbm9kZS1jb21tb25qcyBcInpsaWJcIiIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbm9kZV9tb2R1bGVzL2F4aW9zL2Rpc3Qvbm9kZS9heGlvcy5janMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi93ZWJwYWNrL2Jvb3RzdHJhcCIsIndlYnBhY2s6Ly9tYWlsZ3VuL3dlYnBhY2svcnVudGltZS9ub2RlIG1vZHVsZSBkZWNvcmF0b3IiLCJ3ZWJwYWNrOi8vbWFpbGd1bi93ZWJwYWNrL3N0YXJ0dXAiXSwic291cmNlc0NvbnRlbnQiOlsiKGZ1bmN0aW9uIHdlYnBhY2tVbml2ZXJzYWxNb2R1bGVEZWZpbml0aW9uKHJvb3QsIGZhY3RvcnkpIHtcblx0aWYodHlwZW9mIGV4cG9ydHMgPT09ICdvYmplY3QnICYmIHR5cGVvZiBtb2R1bGUgPT09ICdvYmplY3QnKVxuXHRcdG1vZHVsZS5leHBvcnRzID0gZmFjdG9yeSgpO1xuXHRlbHNlIGlmKHR5cGVvZiBkZWZpbmUgPT09ICdmdW5jdGlvbicgJiYgZGVmaW5lLmFtZClcblx0XHRkZWZpbmUoW10sIGZhY3RvcnkpO1xuXHRlbHNlIGlmKHR5cGVvZiBleHBvcnRzID09PSAnb2JqZWN0Jylcblx0XHRleHBvcnRzW1wibWFpbGd1blwiXSA9IGZhY3RvcnkoKTtcblx0ZWxzZVxuXHRcdHJvb3RbXCJtYWlsZ3VuXCJdID0gZmFjdG9yeSgpO1xufSkodGhpcywgKCkgPT4ge1xucmV0dXJuICIsIm1vZHVsZS5leHBvcnRzID1cbntcbiAgcGFyYWxsZWwgICAgICA6IHJlcXVpcmUoJy4vcGFyYWxsZWwuanMnKSxcbiAgc2VyaWFsICAgICAgICA6IHJlcXVpcmUoJy4vc2VyaWFsLmpzJyksXG4gIHNlcmlhbE9yZGVyZWQgOiByZXF1aXJlKCcuL3NlcmlhbE9yZGVyZWQuanMnKVxufTtcbiIsIi8vIEFQSVxubW9kdWxlLmV4cG9ydHMgPSBhYm9ydDtcblxuLyoqXG4gKiBBYm9ydHMgbGVmdG92ZXIgYWN0aXZlIGpvYnNcbiAqXG4gKiBAcGFyYW0ge29iamVjdH0gc3RhdGUgLSBjdXJyZW50IHN0YXRlIG9iamVjdFxuICovXG5mdW5jdGlvbiBhYm9ydChzdGF0ZSlcbntcbiAgT2JqZWN0LmtleXMoc3RhdGUuam9icykuZm9yRWFjaChjbGVhbi5iaW5kKHN0YXRlKSk7XG5cbiAgLy8gcmVzZXQgbGVmdG92ZXIgam9ic1xuICBzdGF0ZS5qb2JzID0ge307XG59XG5cbi8qKlxuICogQ2xlYW5zIHVwIGxlZnRvdmVyIGpvYiBieSBpbnZva2luZyBhYm9ydCBmdW5jdGlvbiBmb3IgdGhlIHByb3ZpZGVkIGpvYiBpZFxuICpcbiAqIEB0aGlzICBzdGF0ZVxuICogQHBhcmFtIHtzdHJpbmd8bnVtYmVyfSBrZXkgLSBqb2IgaWQgdG8gYWJvcnRcbiAqL1xuZnVuY3Rpb24gY2xlYW4oa2V5KVxue1xuICBpZiAodHlwZW9mIHRoaXMuam9ic1trZXldID09ICdmdW5jdGlvbicpXG4gIHtcbiAgICB0aGlzLmpvYnNba2V5XSgpO1xuICB9XG59XG4iLCJ2YXIgZGVmZXIgPSByZXF1aXJlKCcuL2RlZmVyLmpzJyk7XG5cbi8vIEFQSVxubW9kdWxlLmV4cG9ydHMgPSBhc3luYztcblxuLyoqXG4gKiBSdW5zIHByb3ZpZGVkIGNhbGxiYWNrIGFzeW5jaHJvbm91c2x5XG4gKiBldmVuIGlmIGNhbGxiYWNrIGl0c2VsZiBpcyBub3RcbiAqXG4gKiBAcGFyYW0gICB7ZnVuY3Rpb259IGNhbGxiYWNrIC0gY2FsbGJhY2sgdG8gaW52b2tlXG4gKiBAcmV0dXJucyB7ZnVuY3Rpb259IC0gYXVnbWVudGVkIGNhbGxiYWNrXG4gKi9cbmZ1bmN0aW9uIGFzeW5jKGNhbGxiYWNrKVxue1xuICB2YXIgaXNBc3luYyA9IGZhbHNlO1xuXG4gIC8vIGNoZWNrIGlmIGFzeW5jIGhhcHBlbmVkXG4gIGRlZmVyKGZ1bmN0aW9uKCkgeyBpc0FzeW5jID0gdHJ1ZTsgfSk7XG5cbiAgcmV0dXJuIGZ1bmN0aW9uIGFzeW5jX2NhbGxiYWNrKGVyciwgcmVzdWx0KVxuICB7XG4gICAgaWYgKGlzQXN5bmMpXG4gICAge1xuICAgICAgY2FsbGJhY2soZXJyLCByZXN1bHQpO1xuICAgIH1cbiAgICBlbHNlXG4gICAge1xuICAgICAgZGVmZXIoZnVuY3Rpb24gbmV4dFRpY2tfY2FsbGJhY2soKVxuICAgICAge1xuICAgICAgICBjYWxsYmFjayhlcnIsIHJlc3VsdCk7XG4gICAgICB9KTtcbiAgICB9XG4gIH07XG59XG4iLCJtb2R1bGUuZXhwb3J0cyA9IGRlZmVyO1xuXG4vKipcbiAqIFJ1bnMgcHJvdmlkZWQgZnVuY3Rpb24gb24gbmV4dCBpdGVyYXRpb24gb2YgdGhlIGV2ZW50IGxvb3BcbiAqXG4gKiBAcGFyYW0ge2Z1bmN0aW9ufSBmbiAtIGZ1bmN0aW9uIHRvIHJ1blxuICovXG5mdW5jdGlvbiBkZWZlcihmbilcbntcbiAgdmFyIG5leHRUaWNrID0gdHlwZW9mIHNldEltbWVkaWF0ZSA9PSAnZnVuY3Rpb24nXG4gICAgPyBzZXRJbW1lZGlhdGVcbiAgICA6IChcbiAgICAgIHR5cGVvZiBwcm9jZXNzID09ICdvYmplY3QnICYmIHR5cGVvZiBwcm9jZXNzLm5leHRUaWNrID09ICdmdW5jdGlvbidcbiAgICAgID8gcHJvY2Vzcy5uZXh0VGlja1xuICAgICAgOiBudWxsXG4gICAgKTtcblxuICBpZiAobmV4dFRpY2spXG4gIHtcbiAgICBuZXh0VGljayhmbik7XG4gIH1cbiAgZWxzZVxuICB7XG4gICAgc2V0VGltZW91dChmbiwgMCk7XG4gIH1cbn1cbiIsInZhciBhc3luYyA9IHJlcXVpcmUoJy4vYXN5bmMuanMnKVxuICAsIGFib3J0ID0gcmVxdWlyZSgnLi9hYm9ydC5qcycpXG4gIDtcblxuLy8gQVBJXG5tb2R1bGUuZXhwb3J0cyA9IGl0ZXJhdGU7XG5cbi8qKlxuICogSXRlcmF0ZXMgb3ZlciBlYWNoIGpvYiBvYmplY3RcbiAqXG4gKiBAcGFyYW0ge2FycmF5fG9iamVjdH0gbGlzdCAtIGFycmF5IG9yIG9iamVjdCAobmFtZWQgbGlzdCkgdG8gaXRlcmF0ZSBvdmVyXG4gKiBAcGFyYW0ge2Z1bmN0aW9ufSBpdGVyYXRvciAtIGl0ZXJhdG9yIHRvIHJ1blxuICogQHBhcmFtIHtvYmplY3R9IHN0YXRlIC0gY3VycmVudCBqb2Igc3RhdHVzXG4gKiBAcGFyYW0ge2Z1bmN0aW9ufSBjYWxsYmFjayAtIGludm9rZWQgd2hlbiBhbGwgZWxlbWVudHMgcHJvY2Vzc2VkXG4gKi9cbmZ1bmN0aW9uIGl0ZXJhdGUobGlzdCwgaXRlcmF0b3IsIHN0YXRlLCBjYWxsYmFjaylcbntcbiAgLy8gc3RvcmUgY3VycmVudCBpbmRleFxuICB2YXIga2V5ID0gc3RhdGVbJ2tleWVkTGlzdCddID8gc3RhdGVbJ2tleWVkTGlzdCddW3N0YXRlLmluZGV4XSA6IHN0YXRlLmluZGV4O1xuXG4gIHN0YXRlLmpvYnNba2V5XSA9IHJ1bkpvYihpdGVyYXRvciwga2V5LCBsaXN0W2tleV0sIGZ1bmN0aW9uKGVycm9yLCBvdXRwdXQpXG4gIHtcbiAgICAvLyBkb24ndCByZXBlYXQgeW91cnNlbGZcbiAgICAvLyBza2lwIHNlY29uZGFyeSBjYWxsYmFja3NcbiAgICBpZiAoIShrZXkgaW4gc3RhdGUuam9icykpXG4gICAge1xuICAgICAgcmV0dXJuO1xuICAgIH1cblxuICAgIC8vIGNsZWFuIHVwIGpvYnNcbiAgICBkZWxldGUgc3RhdGUuam9ic1trZXldO1xuXG4gICAgaWYgKGVycm9yKVxuICAgIHtcbiAgICAgIC8vIGRvbid0IHByb2Nlc3MgcmVzdCBvZiB0aGUgcmVzdWx0c1xuICAgICAgLy8gc3RvcCBzdGlsbCBhY3RpdmUgam9ic1xuICAgICAgLy8gYW5kIHJlc2V0IHRoZSBsaXN0XG4gICAgICBhYm9ydChzdGF0ZSk7XG4gICAgfVxuICAgIGVsc2VcbiAgICB7XG4gICAgICBzdGF0ZS5yZXN1bHRzW2tleV0gPSBvdXRwdXQ7XG4gICAgfVxuXG4gICAgLy8gcmV0dXJuIHNhbHZhZ2VkIHJlc3VsdHNcbiAgICBjYWxsYmFjayhlcnJvciwgc3RhdGUucmVzdWx0cyk7XG4gIH0pO1xufVxuXG4vKipcbiAqIFJ1bnMgaXRlcmF0b3Igb3ZlciBwcm92aWRlZCBqb2IgZWxlbWVudFxuICpcbiAqIEBwYXJhbSAgIHtmdW5jdGlvbn0gaXRlcmF0b3IgLSBpdGVyYXRvciB0byBpbnZva2VcbiAqIEBwYXJhbSAgIHtzdHJpbmd8bnVtYmVyfSBrZXkgLSBrZXkvaW5kZXggb2YgdGhlIGVsZW1lbnQgaW4gdGhlIGxpc3Qgb2Ygam9ic1xuICogQHBhcmFtICAge21peGVkfSBpdGVtIC0gam9iIGRlc2NyaXB0aW9uXG4gKiBAcGFyYW0gICB7ZnVuY3Rpb259IGNhbGxiYWNrIC0gaW52b2tlZCBhZnRlciBpdGVyYXRvciBpcyBkb25lIHdpdGggdGhlIGpvYlxuICogQHJldHVybnMge2Z1bmN0aW9ufG1peGVkfSAtIGpvYiBhYm9ydCBmdW5jdGlvbiBvciBzb21ldGhpbmcgZWxzZVxuICovXG5mdW5jdGlvbiBydW5Kb2IoaXRlcmF0b3IsIGtleSwgaXRlbSwgY2FsbGJhY2spXG57XG4gIHZhciBhYm9ydGVyO1xuXG4gIC8vIGFsbG93IHNob3J0Y3V0IGlmIGl0ZXJhdG9yIGV4cGVjdHMgb25seSB0d28gYXJndW1lbnRzXG4gIGlmIChpdGVyYXRvci5sZW5ndGggPT0gMilcbiAge1xuICAgIGFib3J0ZXIgPSBpdGVyYXRvcihpdGVtLCBhc3luYyhjYWxsYmFjaykpO1xuICB9XG4gIC8vIG90aGVyd2lzZSBnbyB3aXRoIGZ1bGwgdGhyZWUgYXJndW1lbnRzXG4gIGVsc2VcbiAge1xuICAgIGFib3J0ZXIgPSBpdGVyYXRvcihpdGVtLCBrZXksIGFzeW5jKGNhbGxiYWNrKSk7XG4gIH1cblxuICByZXR1cm4gYWJvcnRlcjtcbn1cbiIsIi8vIEFQSVxubW9kdWxlLmV4cG9ydHMgPSBzdGF0ZTtcblxuLyoqXG4gKiBDcmVhdGVzIGluaXRpYWwgc3RhdGUgb2JqZWN0XG4gKiBmb3IgaXRlcmF0aW9uIG92ZXIgbGlzdFxuICpcbiAqIEBwYXJhbSAgIHthcnJheXxvYmplY3R9IGxpc3QgLSBsaXN0IHRvIGl0ZXJhdGUgb3ZlclxuICogQHBhcmFtICAge2Z1bmN0aW9ufG51bGx9IHNvcnRNZXRob2QgLSBmdW5jdGlvbiB0byB1c2UgZm9yIGtleXMgc29ydCxcbiAqICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG9yIGBudWxsYCB0byBrZWVwIHRoZW0gYXMgaXNcbiAqIEByZXR1cm5zIHtvYmplY3R9IC0gaW5pdGlhbCBzdGF0ZSBvYmplY3RcbiAqL1xuZnVuY3Rpb24gc3RhdGUobGlzdCwgc29ydE1ldGhvZClcbntcbiAgdmFyIGlzTmFtZWRMaXN0ID0gIUFycmF5LmlzQXJyYXkobGlzdClcbiAgICAsIGluaXRTdGF0ZSA9XG4gICAge1xuICAgICAgaW5kZXggICAgOiAwLFxuICAgICAga2V5ZWRMaXN0OiBpc05hbWVkTGlzdCB8fCBzb3J0TWV0aG9kID8gT2JqZWN0LmtleXMobGlzdCkgOiBudWxsLFxuICAgICAgam9icyAgICAgOiB7fSxcbiAgICAgIHJlc3VsdHMgIDogaXNOYW1lZExpc3QgPyB7fSA6IFtdLFxuICAgICAgc2l6ZSAgICAgOiBpc05hbWVkTGlzdCA/IE9iamVjdC5rZXlzKGxpc3QpLmxlbmd0aCA6IGxpc3QubGVuZ3RoXG4gICAgfVxuICAgIDtcblxuICBpZiAoc29ydE1ldGhvZClcbiAge1xuICAgIC8vIHNvcnQgYXJyYXkga2V5cyBiYXNlZCBvbiBpdCdzIHZhbHVlc1xuICAgIC8vIHNvcnQgb2JqZWN0J3Mga2V5cyBqdXN0IG9uIG93biBtZXJpdFxuICAgIGluaXRTdGF0ZS5rZXllZExpc3Quc29ydChpc05hbWVkTGlzdCA/IHNvcnRNZXRob2QgOiBmdW5jdGlvbihhLCBiKVxuICAgIHtcbiAgICAgIHJldHVybiBzb3J0TWV0aG9kKGxpc3RbYV0sIGxpc3RbYl0pO1xuICAgIH0pO1xuICB9XG5cbiAgcmV0dXJuIGluaXRTdGF0ZTtcbn1cbiIsInZhciBhYm9ydCA9IHJlcXVpcmUoJy4vYWJvcnQuanMnKVxuICAsIGFzeW5jID0gcmVxdWlyZSgnLi9hc3luYy5qcycpXG4gIDtcblxuLy8gQVBJXG5tb2R1bGUuZXhwb3J0cyA9IHRlcm1pbmF0b3I7XG5cbi8qKlxuICogVGVybWluYXRlcyBqb2JzIGluIHRoZSBhdHRhY2hlZCBzdGF0ZSBjb250ZXh0XG4gKlxuICogQHRoaXMgIEFzeW5jS2l0U3RhdGUjXG4gKiBAcGFyYW0ge2Z1bmN0aW9ufSBjYWxsYmFjayAtIGZpbmFsIGNhbGxiYWNrIHRvIGludm9rZSBhZnRlciB0ZXJtaW5hdGlvblxuICovXG5mdW5jdGlvbiB0ZXJtaW5hdG9yKGNhbGxiYWNrKVxue1xuICBpZiAoIU9iamVjdC5rZXlzKHRoaXMuam9icykubGVuZ3RoKVxuICB7XG4gICAgcmV0dXJuO1xuICB9XG5cbiAgLy8gZmFzdCBmb3J3YXJkIGl0ZXJhdGlvbiBpbmRleFxuICB0aGlzLmluZGV4ID0gdGhpcy5zaXplO1xuXG4gIC8vIGFib3J0IGpvYnNcbiAgYWJvcnQodGhpcyk7XG5cbiAgLy8gc2VuZCBiYWNrIHJlc3VsdHMgd2UgaGF2ZSBzbyBmYXJcbiAgYXN5bmMoY2FsbGJhY2spKG51bGwsIHRoaXMucmVzdWx0cyk7XG59XG4iLCJ2YXIgaXRlcmF0ZSAgICA9IHJlcXVpcmUoJy4vbGliL2l0ZXJhdGUuanMnKVxuICAsIGluaXRTdGF0ZSAgPSByZXF1aXJlKCcuL2xpYi9zdGF0ZS5qcycpXG4gICwgdGVybWluYXRvciA9IHJlcXVpcmUoJy4vbGliL3Rlcm1pbmF0b3IuanMnKVxuICA7XG5cbi8vIFB1YmxpYyBBUElcbm1vZHVsZS5leHBvcnRzID0gcGFyYWxsZWw7XG5cbi8qKlxuICogUnVucyBpdGVyYXRvciBvdmVyIHByb3ZpZGVkIGFycmF5IGVsZW1lbnRzIGluIHBhcmFsbGVsXG4gKlxuICogQHBhcmFtICAge2FycmF5fG9iamVjdH0gbGlzdCAtIGFycmF5IG9yIG9iamVjdCAobmFtZWQgbGlzdCkgdG8gaXRlcmF0ZSBvdmVyXG4gKiBAcGFyYW0gICB7ZnVuY3Rpb259IGl0ZXJhdG9yIC0gaXRlcmF0b3IgdG8gcnVuXG4gKiBAcGFyYW0gICB7ZnVuY3Rpb259IGNhbGxiYWNrIC0gaW52b2tlZCB3aGVuIGFsbCBlbGVtZW50cyBwcm9jZXNzZWRcbiAqIEByZXR1cm5zIHtmdW5jdGlvbn0gLSBqb2JzIHRlcm1pbmF0b3JcbiAqL1xuZnVuY3Rpb24gcGFyYWxsZWwobGlzdCwgaXRlcmF0b3IsIGNhbGxiYWNrKVxue1xuICB2YXIgc3RhdGUgPSBpbml0U3RhdGUobGlzdCk7XG5cbiAgd2hpbGUgKHN0YXRlLmluZGV4IDwgKHN0YXRlWydrZXllZExpc3QnXSB8fCBsaXN0KS5sZW5ndGgpXG4gIHtcbiAgICBpdGVyYXRlKGxpc3QsIGl0ZXJhdG9yLCBzdGF0ZSwgZnVuY3Rpb24oZXJyb3IsIHJlc3VsdClcbiAgICB7XG4gICAgICBpZiAoZXJyb3IpXG4gICAgICB7XG4gICAgICAgIGNhbGxiYWNrKGVycm9yLCByZXN1bHQpO1xuICAgICAgICByZXR1cm47XG4gICAgICB9XG5cbiAgICAgIC8vIGxvb2tzIGxpa2UgaXQncyB0aGUgbGFzdCBvbmVcbiAgICAgIGlmIChPYmplY3Qua2V5cyhzdGF0ZS5qb2JzKS5sZW5ndGggPT09IDApXG4gICAgICB7XG4gICAgICAgIGNhbGxiYWNrKG51bGwsIHN0YXRlLnJlc3VsdHMpO1xuICAgICAgICByZXR1cm47XG4gICAgICB9XG4gICAgfSk7XG5cbiAgICBzdGF0ZS5pbmRleCsrO1xuICB9XG5cbiAgcmV0dXJuIHRlcm1pbmF0b3IuYmluZChzdGF0ZSwgY2FsbGJhY2spO1xufVxuIiwidmFyIHNlcmlhbE9yZGVyZWQgPSByZXF1aXJlKCcuL3NlcmlhbE9yZGVyZWQuanMnKTtcblxuLy8gUHVibGljIEFQSVxubW9kdWxlLmV4cG9ydHMgPSBzZXJpYWw7XG5cbi8qKlxuICogUnVucyBpdGVyYXRvciBvdmVyIHByb3ZpZGVkIGFycmF5IGVsZW1lbnRzIGluIHNlcmllc1xuICpcbiAqIEBwYXJhbSAgIHthcnJheXxvYmplY3R9IGxpc3QgLSBhcnJheSBvciBvYmplY3QgKG5hbWVkIGxpc3QpIHRvIGl0ZXJhdGUgb3ZlclxuICogQHBhcmFtICAge2Z1bmN0aW9ufSBpdGVyYXRvciAtIGl0ZXJhdG9yIHRvIHJ1blxuICogQHBhcmFtICAge2Z1bmN0aW9ufSBjYWxsYmFjayAtIGludm9rZWQgd2hlbiBhbGwgZWxlbWVudHMgcHJvY2Vzc2VkXG4gKiBAcmV0dXJucyB7ZnVuY3Rpb259IC0gam9icyB0ZXJtaW5hdG9yXG4gKi9cbmZ1bmN0aW9uIHNlcmlhbChsaXN0LCBpdGVyYXRvciwgY2FsbGJhY2spXG57XG4gIHJldHVybiBzZXJpYWxPcmRlcmVkKGxpc3QsIGl0ZXJhdG9yLCBudWxsLCBjYWxsYmFjayk7XG59XG4iLCJ2YXIgaXRlcmF0ZSAgICA9IHJlcXVpcmUoJy4vbGliL2l0ZXJhdGUuanMnKVxuICAsIGluaXRTdGF0ZSAgPSByZXF1aXJlKCcuL2xpYi9zdGF0ZS5qcycpXG4gICwgdGVybWluYXRvciA9IHJlcXVpcmUoJy4vbGliL3Rlcm1pbmF0b3IuanMnKVxuICA7XG5cbi8vIFB1YmxpYyBBUElcbm1vZHVsZS5leHBvcnRzID0gc2VyaWFsT3JkZXJlZDtcbi8vIHNvcnRpbmcgaGVscGVyc1xubW9kdWxlLmV4cG9ydHMuYXNjZW5kaW5nICA9IGFzY2VuZGluZztcbm1vZHVsZS5leHBvcnRzLmRlc2NlbmRpbmcgPSBkZXNjZW5kaW5nO1xuXG4vKipcbiAqIFJ1bnMgaXRlcmF0b3Igb3ZlciBwcm92aWRlZCBzb3J0ZWQgYXJyYXkgZWxlbWVudHMgaW4gc2VyaWVzXG4gKlxuICogQHBhcmFtICAge2FycmF5fG9iamVjdH0gbGlzdCAtIGFycmF5IG9yIG9iamVjdCAobmFtZWQgbGlzdCkgdG8gaXRlcmF0ZSBvdmVyXG4gKiBAcGFyYW0gICB7ZnVuY3Rpb259IGl0ZXJhdG9yIC0gaXRlcmF0b3IgdG8gcnVuXG4gKiBAcGFyYW0gICB7ZnVuY3Rpb259IHNvcnRNZXRob2QgLSBjdXN0b20gc29ydCBmdW5jdGlvblxuICogQHBhcmFtICAge2Z1bmN0aW9ufSBjYWxsYmFjayAtIGludm9rZWQgd2hlbiBhbGwgZWxlbWVudHMgcHJvY2Vzc2VkXG4gKiBAcmV0dXJucyB7ZnVuY3Rpb259IC0gam9icyB0ZXJtaW5hdG9yXG4gKi9cbmZ1bmN0aW9uIHNlcmlhbE9yZGVyZWQobGlzdCwgaXRlcmF0b3IsIHNvcnRNZXRob2QsIGNhbGxiYWNrKVxue1xuICB2YXIgc3RhdGUgPSBpbml0U3RhdGUobGlzdCwgc29ydE1ldGhvZCk7XG5cbiAgaXRlcmF0ZShsaXN0LCBpdGVyYXRvciwgc3RhdGUsIGZ1bmN0aW9uIGl0ZXJhdG9ySGFuZGxlcihlcnJvciwgcmVzdWx0KVxuICB7XG4gICAgaWYgKGVycm9yKVxuICAgIHtcbiAgICAgIGNhbGxiYWNrKGVycm9yLCByZXN1bHQpO1xuICAgICAgcmV0dXJuO1xuICAgIH1cblxuICAgIHN0YXRlLmluZGV4Kys7XG5cbiAgICAvLyBhcmUgd2UgdGhlcmUgeWV0P1xuICAgIGlmIChzdGF0ZS5pbmRleCA8IChzdGF0ZVsna2V5ZWRMaXN0J10gfHwgbGlzdCkubGVuZ3RoKVxuICAgIHtcbiAgICAgIGl0ZXJhdGUobGlzdCwgaXRlcmF0b3IsIHN0YXRlLCBpdGVyYXRvckhhbmRsZXIpO1xuICAgICAgcmV0dXJuO1xuICAgIH1cblxuICAgIC8vIGRvbmUgaGVyZVxuICAgIGNhbGxiYWNrKG51bGwsIHN0YXRlLnJlc3VsdHMpO1xuICB9KTtcblxuICByZXR1cm4gdGVybWluYXRvci5iaW5kKHN0YXRlLCBjYWxsYmFjayk7XG59XG5cbi8qXG4gKiAtLSBTb3J0IG1ldGhvZHNcbiAqL1xuXG4vKipcbiAqIHNvcnQgaGVscGVyIHRvIHNvcnQgYXJyYXkgZWxlbWVudHMgaW4gYXNjZW5kaW5nIG9yZGVyXG4gKlxuICogQHBhcmFtICAge21peGVkfSBhIC0gYW4gaXRlbSB0byBjb21wYXJlXG4gKiBAcGFyYW0gICB7bWl4ZWR9IGIgLSBhbiBpdGVtIHRvIGNvbXBhcmVcbiAqIEByZXR1cm5zIHtudW1iZXJ9IC0gY29tcGFyaXNvbiByZXN1bHRcbiAqL1xuZnVuY3Rpb24gYXNjZW5kaW5nKGEsIGIpXG57XG4gIHJldHVybiBhIDwgYiA/IC0xIDogYSA+IGIgPyAxIDogMDtcbn1cblxuLyoqXG4gKiBzb3J0IGhlbHBlciB0byBzb3J0IGFycmF5IGVsZW1lbnRzIGluIGRlc2NlbmRpbmcgb3JkZXJcbiAqXG4gKiBAcGFyYW0gICB7bWl4ZWR9IGEgLSBhbiBpdGVtIHRvIGNvbXBhcmVcbiAqIEBwYXJhbSAgIHttaXhlZH0gYiAtIGFuIGl0ZW0gdG8gY29tcGFyZVxuICogQHJldHVybnMge251bWJlcn0gLSBjb21wYXJpc29uIHJlc3VsdFxuICovXG5mdW5jdGlvbiBkZXNjZW5kaW5nKGEsIGIpXG57XG4gIHJldHVybiAtMSAqIGFzY2VuZGluZyhhLCBiKTtcbn1cbiIsInZhciBDb21iaW5lZFN0cmVhbSA9IHJlcXVpcmUoJ2NvbWJpbmVkLXN0cmVhbScpO1xudmFyIHV0aWwgPSByZXF1aXJlKCd1dGlsJyk7XG52YXIgcGF0aCA9IHJlcXVpcmUoJ3BhdGgnKTtcbnZhciBodHRwID0gcmVxdWlyZSgnaHR0cCcpO1xudmFyIGh0dHBzID0gcmVxdWlyZSgnaHR0cHMnKTtcbnZhciBwYXJzZVVybCA9IHJlcXVpcmUoJ3VybCcpLnBhcnNlO1xudmFyIGZzID0gcmVxdWlyZSgnZnMnKTtcbnZhciBTdHJlYW0gPSByZXF1aXJlKCdzdHJlYW0nKS5TdHJlYW07XG52YXIgbWltZSA9IHJlcXVpcmUoJ21pbWUtdHlwZXMnKTtcbnZhciBhc3luY2tpdCA9IHJlcXVpcmUoJ2FzeW5ja2l0Jyk7XG52YXIgcG9wdWxhdGUgPSByZXF1aXJlKCcuL3BvcHVsYXRlLmpzJyk7XG5cbi8vIFB1YmxpYyBBUElcbm1vZHVsZS5leHBvcnRzID0gRm9ybURhdGE7XG5cbi8vIG1ha2UgaXQgYSBTdHJlYW1cbnV0aWwuaW5oZXJpdHMoRm9ybURhdGEsIENvbWJpbmVkU3RyZWFtKTtcblxuLyoqXG4gKiBDcmVhdGUgcmVhZGFibGUgXCJtdWx0aXBhcnQvZm9ybS1kYXRhXCIgc3RyZWFtcy5cbiAqIENhbiBiZSB1c2VkIHRvIHN1Ym1pdCBmb3Jtc1xuICogYW5kIGZpbGUgdXBsb2FkcyB0byBvdGhlciB3ZWIgYXBwbGljYXRpb25zLlxuICpcbiAqIEBjb25zdHJ1Y3RvclxuICogQHBhcmFtIHtPYmplY3R9IG9wdGlvbnMgLSBQcm9wZXJ0aWVzIHRvIGJlIGFkZGVkL292ZXJyaWRlbiBmb3IgRm9ybURhdGEgYW5kIENvbWJpbmVkU3RyZWFtXG4gKi9cbmZ1bmN0aW9uIEZvcm1EYXRhKG9wdGlvbnMpIHtcbiAgaWYgKCEodGhpcyBpbnN0YW5jZW9mIEZvcm1EYXRhKSkge1xuICAgIHJldHVybiBuZXcgRm9ybURhdGEob3B0aW9ucyk7XG4gIH1cblxuICB0aGlzLl9vdmVyaGVhZExlbmd0aCA9IDA7XG4gIHRoaXMuX3ZhbHVlTGVuZ3RoID0gMDtcbiAgdGhpcy5fdmFsdWVzVG9NZWFzdXJlID0gW107XG5cbiAgQ29tYmluZWRTdHJlYW0uY2FsbCh0aGlzKTtcblxuICBvcHRpb25zID0gb3B0aW9ucyB8fCB7fTtcbiAgZm9yICh2YXIgb3B0aW9uIGluIG9wdGlvbnMpIHtcbiAgICB0aGlzW29wdGlvbl0gPSBvcHRpb25zW29wdGlvbl07XG4gIH1cbn1cblxuRm9ybURhdGEuTElORV9CUkVBSyA9ICdcXHJcXG4nO1xuRm9ybURhdGEuREVGQVVMVF9DT05URU5UX1RZUEUgPSAnYXBwbGljYXRpb24vb2N0ZXQtc3RyZWFtJztcblxuRm9ybURhdGEucHJvdG90eXBlLmFwcGVuZCA9IGZ1bmN0aW9uKGZpZWxkLCB2YWx1ZSwgb3B0aW9ucykge1xuXG4gIG9wdGlvbnMgPSBvcHRpb25zIHx8IHt9O1xuXG4gIC8vIGFsbG93IGZpbGVuYW1lIGFzIHNpbmdsZSBvcHRpb25cbiAgaWYgKHR5cGVvZiBvcHRpb25zID09ICdzdHJpbmcnKSB7XG4gICAgb3B0aW9ucyA9IHtmaWxlbmFtZTogb3B0aW9uc307XG4gIH1cblxuICB2YXIgYXBwZW5kID0gQ29tYmluZWRTdHJlYW0ucHJvdG90eXBlLmFwcGVuZC5iaW5kKHRoaXMpO1xuXG4gIC8vIGFsbCB0aGF0IHN0cmVhbXkgYnVzaW5lc3MgY2FuJ3QgaGFuZGxlIG51bWJlcnNcbiAgaWYgKHR5cGVvZiB2YWx1ZSA9PSAnbnVtYmVyJykge1xuICAgIHZhbHVlID0gJycgKyB2YWx1ZTtcbiAgfVxuXG4gIC8vIGh0dHBzOi8vZ2l0aHViLmNvbS9mZWxpeGdlL25vZGUtZm9ybS1kYXRhL2lzc3Vlcy8zOFxuICBpZiAodXRpbC5pc0FycmF5KHZhbHVlKSkge1xuICAgIC8vIFBsZWFzZSBjb252ZXJ0IHlvdXIgYXJyYXkgaW50byBzdHJpbmdcbiAgICAvLyB0aGUgd2F5IHdlYiBzZXJ2ZXIgZXhwZWN0cyBpdFxuICAgIHRoaXMuX2Vycm9yKG5ldyBFcnJvcignQXJyYXlzIGFyZSBub3Qgc3VwcG9ydGVkLicpKTtcbiAgICByZXR1cm47XG4gIH1cblxuICB2YXIgaGVhZGVyID0gdGhpcy5fbXVsdGlQYXJ0SGVhZGVyKGZpZWxkLCB2YWx1ZSwgb3B0aW9ucyk7XG4gIHZhciBmb290ZXIgPSB0aGlzLl9tdWx0aVBhcnRGb290ZXIoKTtcblxuICBhcHBlbmQoaGVhZGVyKTtcbiAgYXBwZW5kKHZhbHVlKTtcbiAgYXBwZW5kKGZvb3Rlcik7XG5cbiAgLy8gcGFzcyBhbG9uZyBvcHRpb25zLmtub3duTGVuZ3RoXG4gIHRoaXMuX3RyYWNrTGVuZ3RoKGhlYWRlciwgdmFsdWUsIG9wdGlvbnMpO1xufTtcblxuRm9ybURhdGEucHJvdG90eXBlLl90cmFja0xlbmd0aCA9IGZ1bmN0aW9uKGhlYWRlciwgdmFsdWUsIG9wdGlvbnMpIHtcbiAgdmFyIHZhbHVlTGVuZ3RoID0gMDtcblxuICAvLyB1c2VkIHcvIGdldExlbmd0aFN5bmMoKSwgd2hlbiBsZW5ndGggaXMga25vd24uXG4gIC8vIGUuZy4gZm9yIHN0cmVhbWluZyBkaXJlY3RseSBmcm9tIGEgcmVtb3RlIHNlcnZlcixcbiAgLy8gdy8gYSBrbm93biBmaWxlIGEgc2l6ZSwgYW5kIG5vdCB3YW50aW5nIHRvIHdhaXQgZm9yXG4gIC8vIGluY29taW5nIGZpbGUgdG8gZmluaXNoIHRvIGdldCBpdHMgc2l6ZS5cbiAgaWYgKG9wdGlvbnMua25vd25MZW5ndGggIT0gbnVsbCkge1xuICAgIHZhbHVlTGVuZ3RoICs9ICtvcHRpb25zLmtub3duTGVuZ3RoO1xuICB9IGVsc2UgaWYgKEJ1ZmZlci5pc0J1ZmZlcih2YWx1ZSkpIHtcbiAgICB2YWx1ZUxlbmd0aCA9IHZhbHVlLmxlbmd0aDtcbiAgfSBlbHNlIGlmICh0eXBlb2YgdmFsdWUgPT09ICdzdHJpbmcnKSB7XG4gICAgdmFsdWVMZW5ndGggPSBCdWZmZXIuYnl0ZUxlbmd0aCh2YWx1ZSk7XG4gIH1cblxuICB0aGlzLl92YWx1ZUxlbmd0aCArPSB2YWx1ZUxlbmd0aDtcblxuICAvLyBAY2hlY2sgd2h5IGFkZCBDUkxGPyBkb2VzIHRoaXMgYWNjb3VudCBmb3IgY3VzdG9tL211bHRpcGxlIENSTEZzP1xuICB0aGlzLl9vdmVyaGVhZExlbmd0aCArPVxuICAgIEJ1ZmZlci5ieXRlTGVuZ3RoKGhlYWRlcikgK1xuICAgIEZvcm1EYXRhLkxJTkVfQlJFQUsubGVuZ3RoO1xuXG4gIC8vIGVtcHR5IG9yIGVpdGhlciBkb2Vzbid0IGhhdmUgcGF0aCBvciBub3QgYW4gaHR0cCByZXNwb25zZSBvciBub3QgYSBzdHJlYW1cbiAgaWYgKCF2YWx1ZSB8fCAoICF2YWx1ZS5wYXRoICYmICEodmFsdWUucmVhZGFibGUgJiYgdmFsdWUuaGFzT3duUHJvcGVydHkoJ2h0dHBWZXJzaW9uJykpICYmICEodmFsdWUgaW5zdGFuY2VvZiBTdHJlYW0pKSkge1xuICAgIHJldHVybjtcbiAgfVxuXG4gIC8vIG5vIG5lZWQgdG8gYm90aGVyIHdpdGggdGhlIGxlbmd0aFxuICBpZiAoIW9wdGlvbnMua25vd25MZW5ndGgpIHtcbiAgICB0aGlzLl92YWx1ZXNUb01lYXN1cmUucHVzaCh2YWx1ZSk7XG4gIH1cbn07XG5cbkZvcm1EYXRhLnByb3RvdHlwZS5fbGVuZ3RoUmV0cmlldmVyID0gZnVuY3Rpb24odmFsdWUsIGNhbGxiYWNrKSB7XG5cbiAgaWYgKHZhbHVlLmhhc093blByb3BlcnR5KCdmZCcpKSB7XG5cbiAgICAvLyB0YWtlIHJlYWQgcmFuZ2UgaW50byBhIGFjY291bnRcbiAgICAvLyBgZW5kYCA9IEluZmluaXR5IOKAkz4gcmVhZCBmaWxlIHRpbGwgdGhlIGVuZFxuICAgIC8vXG4gICAgLy8gVE9ETzogTG9va3MgbGlrZSB0aGVyZSBpcyBidWcgaW4gTm9kZSBmcy5jcmVhdGVSZWFkU3RyZWFtXG4gICAgLy8gaXQgZG9lc24ndCByZXNwZWN0IGBlbmRgIG9wdGlvbnMgd2l0aG91dCBgc3RhcnRgIG9wdGlvbnNcbiAgICAvLyBGaXggaXQgd2hlbiBub2RlIGZpeGVzIGl0LlxuICAgIC8vIGh0dHBzOi8vZ2l0aHViLmNvbS9qb3llbnQvbm9kZS9pc3N1ZXMvNzgxOVxuICAgIGlmICh2YWx1ZS5lbmQgIT0gdW5kZWZpbmVkICYmIHZhbHVlLmVuZCAhPSBJbmZpbml0eSAmJiB2YWx1ZS5zdGFydCAhPSB1bmRlZmluZWQpIHtcblxuICAgICAgLy8gd2hlbiBlbmQgc3BlY2lmaWVkXG4gICAgICAvLyBubyBuZWVkIHRvIGNhbGN1bGF0ZSByYW5nZVxuICAgICAgLy8gaW5jbHVzaXZlLCBzdGFydHMgd2l0aCAwXG4gICAgICBjYWxsYmFjayhudWxsLCB2YWx1ZS5lbmQgKyAxIC0gKHZhbHVlLnN0YXJ0ID8gdmFsdWUuc3RhcnQgOiAwKSk7XG5cbiAgICAvLyBub3QgdGhhdCBmYXN0IHNub29weVxuICAgIH0gZWxzZSB7XG4gICAgICAvLyBzdGlsbCBuZWVkIHRvIGZldGNoIGZpbGUgc2l6ZSBmcm9tIGZzXG4gICAgICBmcy5zdGF0KHZhbHVlLnBhdGgsIGZ1bmN0aW9uKGVyciwgc3RhdCkge1xuXG4gICAgICAgIHZhciBmaWxlU2l6ZTtcblxuICAgICAgICBpZiAoZXJyKSB7XG4gICAgICAgICAgY2FsbGJhY2soZXJyKTtcbiAgICAgICAgICByZXR1cm47XG4gICAgICAgIH1cblxuICAgICAgICAvLyB1cGRhdGUgZmluYWwgc2l6ZSBiYXNlZCBvbiB0aGUgcmFuZ2Ugb3B0aW9uc1xuICAgICAgICBmaWxlU2l6ZSA9IHN0YXQuc2l6ZSAtICh2YWx1ZS5zdGFydCA/IHZhbHVlLnN0YXJ0IDogMCk7XG4gICAgICAgIGNhbGxiYWNrKG51bGwsIGZpbGVTaXplKTtcbiAgICAgIH0pO1xuICAgIH1cblxuICAvLyBvciBodHRwIHJlc3BvbnNlXG4gIH0gZWxzZSBpZiAodmFsdWUuaGFzT3duUHJvcGVydHkoJ2h0dHBWZXJzaW9uJykpIHtcbiAgICBjYWxsYmFjayhudWxsLCArdmFsdWUuaGVhZGVyc1snY29udGVudC1sZW5ndGgnXSk7XG5cbiAgLy8gb3IgcmVxdWVzdCBzdHJlYW0gaHR0cDovL2dpdGh1Yi5jb20vbWlrZWFsL3JlcXVlc3RcbiAgfSBlbHNlIGlmICh2YWx1ZS5oYXNPd25Qcm9wZXJ0eSgnaHR0cE1vZHVsZScpKSB7XG4gICAgLy8gd2FpdCB0aWxsIHJlc3BvbnNlIGNvbWUgYmFja1xuICAgIHZhbHVlLm9uKCdyZXNwb25zZScsIGZ1bmN0aW9uKHJlc3BvbnNlKSB7XG4gICAgICB2YWx1ZS5wYXVzZSgpO1xuICAgICAgY2FsbGJhY2sobnVsbCwgK3Jlc3BvbnNlLmhlYWRlcnNbJ2NvbnRlbnQtbGVuZ3RoJ10pO1xuICAgIH0pO1xuICAgIHZhbHVlLnJlc3VtZSgpO1xuXG4gIC8vIHNvbWV0aGluZyBlbHNlXG4gIH0gZWxzZSB7XG4gICAgY2FsbGJhY2soJ1Vua25vd24gc3RyZWFtJyk7XG4gIH1cbn07XG5cbkZvcm1EYXRhLnByb3RvdHlwZS5fbXVsdGlQYXJ0SGVhZGVyID0gZnVuY3Rpb24oZmllbGQsIHZhbHVlLCBvcHRpb25zKSB7XG4gIC8vIGN1c3RvbSBoZWFkZXIgc3BlY2lmaWVkIChhcyBzdHJpbmcpP1xuICAvLyBpdCBiZWNvbWVzIHJlc3BvbnNpYmxlIGZvciBib3VuZGFyeVxuICAvLyAoZS5nLiB0byBoYW5kbGUgZXh0cmEgQ1JMRnMgb24gLk5FVCBzZXJ2ZXJzKVxuICBpZiAodHlwZW9mIG9wdGlvbnMuaGVhZGVyID09ICdzdHJpbmcnKSB7XG4gICAgcmV0dXJuIG9wdGlvbnMuaGVhZGVyO1xuICB9XG5cbiAgdmFyIGNvbnRlbnREaXNwb3NpdGlvbiA9IHRoaXMuX2dldENvbnRlbnREaXNwb3NpdGlvbih2YWx1ZSwgb3B0aW9ucyk7XG4gIHZhciBjb250ZW50VHlwZSA9IHRoaXMuX2dldENvbnRlbnRUeXBlKHZhbHVlLCBvcHRpb25zKTtcblxuICB2YXIgY29udGVudHMgPSAnJztcbiAgdmFyIGhlYWRlcnMgID0ge1xuICAgIC8vIGFkZCBjdXN0b20gZGlzcG9zaXRpb24gYXMgdGhpcmQgZWxlbWVudCBvciBrZWVwIGl0IHR3byBlbGVtZW50cyBpZiBub3RcbiAgICAnQ29udGVudC1EaXNwb3NpdGlvbic6IFsnZm9ybS1kYXRhJywgJ25hbWU9XCInICsgZmllbGQgKyAnXCInXS5jb25jYXQoY29udGVudERpc3Bvc2l0aW9uIHx8IFtdKSxcbiAgICAvLyBpZiBubyBjb250ZW50IHR5cGUuIGFsbG93IGl0IHRvIGJlIGVtcHR5IGFycmF5XG4gICAgJ0NvbnRlbnQtVHlwZSc6IFtdLmNvbmNhdChjb250ZW50VHlwZSB8fCBbXSlcbiAgfTtcblxuICAvLyBhbGxvdyBjdXN0b20gaGVhZGVycy5cbiAgaWYgKHR5cGVvZiBvcHRpb25zLmhlYWRlciA9PSAnb2JqZWN0Jykge1xuICAgIHBvcHVsYXRlKGhlYWRlcnMsIG9wdGlvbnMuaGVhZGVyKTtcbiAgfVxuXG4gIHZhciBoZWFkZXI7XG4gIGZvciAodmFyIHByb3AgaW4gaGVhZGVycykge1xuICAgIGlmICghaGVhZGVycy5oYXNPd25Qcm9wZXJ0eShwcm9wKSkgY29udGludWU7XG4gICAgaGVhZGVyID0gaGVhZGVyc1twcm9wXTtcblxuICAgIC8vIHNraXAgbnVsbGlzaCBoZWFkZXJzLlxuICAgIGlmIChoZWFkZXIgPT0gbnVsbCkge1xuICAgICAgY29udGludWU7XG4gICAgfVxuXG4gICAgLy8gY29udmVydCBhbGwgaGVhZGVycyB0byBhcnJheXMuXG4gICAgaWYgKCFBcnJheS5pc0FycmF5KGhlYWRlcikpIHtcbiAgICAgIGhlYWRlciA9IFtoZWFkZXJdO1xuICAgIH1cblxuICAgIC8vIGFkZCBub24tZW1wdHkgaGVhZGVycy5cbiAgICBpZiAoaGVhZGVyLmxlbmd0aCkge1xuICAgICAgY29udGVudHMgKz0gcHJvcCArICc6ICcgKyBoZWFkZXIuam9pbignOyAnKSArIEZvcm1EYXRhLkxJTkVfQlJFQUs7XG4gICAgfVxuICB9XG5cbiAgcmV0dXJuICctLScgKyB0aGlzLmdldEJvdW5kYXJ5KCkgKyBGb3JtRGF0YS5MSU5FX0JSRUFLICsgY29udGVudHMgKyBGb3JtRGF0YS5MSU5FX0JSRUFLO1xufTtcblxuRm9ybURhdGEucHJvdG90eXBlLl9nZXRDb250ZW50RGlzcG9zaXRpb24gPSBmdW5jdGlvbih2YWx1ZSwgb3B0aW9ucykge1xuXG4gIHZhciBmaWxlbmFtZVxuICAgICwgY29udGVudERpc3Bvc2l0aW9uXG4gICAgO1xuXG4gIGlmICh0eXBlb2Ygb3B0aW9ucy5maWxlcGF0aCA9PT0gJ3N0cmluZycpIHtcbiAgICAvLyBjdXN0b20gZmlsZXBhdGggZm9yIHJlbGF0aXZlIHBhdGhzXG4gICAgZmlsZW5hbWUgPSBwYXRoLm5vcm1hbGl6ZShvcHRpb25zLmZpbGVwYXRoKS5yZXBsYWNlKC9cXFxcL2csICcvJyk7XG4gIH0gZWxzZSBpZiAob3B0aW9ucy5maWxlbmFtZSB8fCB2YWx1ZS5uYW1lIHx8IHZhbHVlLnBhdGgpIHtcbiAgICAvLyBjdXN0b20gZmlsZW5hbWUgdGFrZSBwcmVjZWRlbmNlXG4gICAgLy8gZm9ybWlkYWJsZSBhbmQgdGhlIGJyb3dzZXIgYWRkIGEgbmFtZSBwcm9wZXJ0eVxuICAgIC8vIGZzLSBhbmQgcmVxdWVzdC0gc3RyZWFtcyBoYXZlIHBhdGggcHJvcGVydHlcbiAgICBmaWxlbmFtZSA9IHBhdGguYmFzZW5hbWUob3B0aW9ucy5maWxlbmFtZSB8fCB2YWx1ZS5uYW1lIHx8IHZhbHVlLnBhdGgpO1xuICB9IGVsc2UgaWYgKHZhbHVlLnJlYWRhYmxlICYmIHZhbHVlLmhhc093blByb3BlcnR5KCdodHRwVmVyc2lvbicpKSB7XG4gICAgLy8gb3IgdHJ5IGh0dHAgcmVzcG9uc2VcbiAgICBmaWxlbmFtZSA9IHBhdGguYmFzZW5hbWUodmFsdWUuY2xpZW50Ll9odHRwTWVzc2FnZS5wYXRoIHx8ICcnKTtcbiAgfVxuXG4gIGlmIChmaWxlbmFtZSkge1xuICAgIGNvbnRlbnREaXNwb3NpdGlvbiA9ICdmaWxlbmFtZT1cIicgKyBmaWxlbmFtZSArICdcIic7XG4gIH1cblxuICByZXR1cm4gY29udGVudERpc3Bvc2l0aW9uO1xufTtcblxuRm9ybURhdGEucHJvdG90eXBlLl9nZXRDb250ZW50VHlwZSA9IGZ1bmN0aW9uKHZhbHVlLCBvcHRpb25zKSB7XG5cbiAgLy8gdXNlIGN1c3RvbSBjb250ZW50LXR5cGUgYWJvdmUgYWxsXG4gIHZhciBjb250ZW50VHlwZSA9IG9wdGlvbnMuY29udGVudFR5cGU7XG5cbiAgLy8gb3IgdHJ5IGBuYW1lYCBmcm9tIGZvcm1pZGFibGUsIGJyb3dzZXJcbiAgaWYgKCFjb250ZW50VHlwZSAmJiB2YWx1ZS5uYW1lKSB7XG4gICAgY29udGVudFR5cGUgPSBtaW1lLmxvb2t1cCh2YWx1ZS5uYW1lKTtcbiAgfVxuXG4gIC8vIG9yIHRyeSBgcGF0aGAgZnJvbSBmcy0sIHJlcXVlc3QtIHN0cmVhbXNcbiAgaWYgKCFjb250ZW50VHlwZSAmJiB2YWx1ZS5wYXRoKSB7XG4gICAgY29udGVudFR5cGUgPSBtaW1lLmxvb2t1cCh2YWx1ZS5wYXRoKTtcbiAgfVxuXG4gIC8vIG9yIGlmIGl0J3MgaHR0cC1yZXBvbnNlXG4gIGlmICghY29udGVudFR5cGUgJiYgdmFsdWUucmVhZGFibGUgJiYgdmFsdWUuaGFzT3duUHJvcGVydHkoJ2h0dHBWZXJzaW9uJykpIHtcbiAgICBjb250ZW50VHlwZSA9IHZhbHVlLmhlYWRlcnNbJ2NvbnRlbnQtdHlwZSddO1xuICB9XG5cbiAgLy8gb3IgZ3Vlc3MgaXQgZnJvbSB0aGUgZmlsZXBhdGggb3IgZmlsZW5hbWVcbiAgaWYgKCFjb250ZW50VHlwZSAmJiAob3B0aW9ucy5maWxlcGF0aCB8fCBvcHRpb25zLmZpbGVuYW1lKSkge1xuICAgIGNvbnRlbnRUeXBlID0gbWltZS5sb29rdXAob3B0aW9ucy5maWxlcGF0aCB8fCBvcHRpb25zLmZpbGVuYW1lKTtcbiAgfVxuXG4gIC8vIGZhbGxiYWNrIHRvIHRoZSBkZWZhdWx0IGNvbnRlbnQgdHlwZSBpZiBgdmFsdWVgIGlzIG5vdCBzaW1wbGUgdmFsdWVcbiAgaWYgKCFjb250ZW50VHlwZSAmJiB0eXBlb2YgdmFsdWUgPT0gJ29iamVjdCcpIHtcbiAgICBjb250ZW50VHlwZSA9IEZvcm1EYXRhLkRFRkFVTFRfQ09OVEVOVF9UWVBFO1xuICB9XG5cbiAgcmV0dXJuIGNvbnRlbnRUeXBlO1xufTtcblxuRm9ybURhdGEucHJvdG90eXBlLl9tdWx0aVBhcnRGb290ZXIgPSBmdW5jdGlvbigpIHtcbiAgcmV0dXJuIGZ1bmN0aW9uKG5leHQpIHtcbiAgICB2YXIgZm9vdGVyID0gRm9ybURhdGEuTElORV9CUkVBSztcblxuICAgIHZhciBsYXN0UGFydCA9ICh0aGlzLl9zdHJlYW1zLmxlbmd0aCA9PT0gMCk7XG4gICAgaWYgKGxhc3RQYXJ0KSB7XG4gICAgICBmb290ZXIgKz0gdGhpcy5fbGFzdEJvdW5kYXJ5KCk7XG4gICAgfVxuXG4gICAgbmV4dChmb290ZXIpO1xuICB9LmJpbmQodGhpcyk7XG59O1xuXG5Gb3JtRGF0YS5wcm90b3R5cGUuX2xhc3RCb3VuZGFyeSA9IGZ1bmN0aW9uKCkge1xuICByZXR1cm4gJy0tJyArIHRoaXMuZ2V0Qm91bmRhcnkoKSArICctLScgKyBGb3JtRGF0YS5MSU5FX0JSRUFLO1xufTtcblxuRm9ybURhdGEucHJvdG90eXBlLmdldEhlYWRlcnMgPSBmdW5jdGlvbih1c2VySGVhZGVycykge1xuICB2YXIgaGVhZGVyO1xuICB2YXIgZm9ybUhlYWRlcnMgPSB7XG4gICAgJ2NvbnRlbnQtdHlwZSc6ICdtdWx0aXBhcnQvZm9ybS1kYXRhOyBib3VuZGFyeT0nICsgdGhpcy5nZXRCb3VuZGFyeSgpXG4gIH07XG5cbiAgZm9yIChoZWFkZXIgaW4gdXNlckhlYWRlcnMpIHtcbiAgICBpZiAodXNlckhlYWRlcnMuaGFzT3duUHJvcGVydHkoaGVhZGVyKSkge1xuICAgICAgZm9ybUhlYWRlcnNbaGVhZGVyLnRvTG93ZXJDYXNlKCldID0gdXNlckhlYWRlcnNbaGVhZGVyXTtcbiAgICB9XG4gIH1cblxuICByZXR1cm4gZm9ybUhlYWRlcnM7XG59O1xuXG5Gb3JtRGF0YS5wcm90b3R5cGUuc2V0Qm91bmRhcnkgPSBmdW5jdGlvbihib3VuZGFyeSkge1xuICB0aGlzLl9ib3VuZGFyeSA9IGJvdW5kYXJ5O1xufTtcblxuRm9ybURhdGEucHJvdG90eXBlLmdldEJvdW5kYXJ5ID0gZnVuY3Rpb24oKSB7XG4gIGlmICghdGhpcy5fYm91bmRhcnkpIHtcbiAgICB0aGlzLl9nZW5lcmF0ZUJvdW5kYXJ5KCk7XG4gIH1cblxuICByZXR1cm4gdGhpcy5fYm91bmRhcnk7XG59O1xuXG5Gb3JtRGF0YS5wcm90b3R5cGUuZ2V0QnVmZmVyID0gZnVuY3Rpb24oKSB7XG4gIHZhciBkYXRhQnVmZmVyID0gbmV3IEJ1ZmZlci5hbGxvYyggMCApO1xuICB2YXIgYm91bmRhcnkgPSB0aGlzLmdldEJvdW5kYXJ5KCk7XG5cbiAgLy8gQ3JlYXRlIHRoZSBmb3JtIGNvbnRlbnQuIEFkZCBMaW5lIGJyZWFrcyB0byB0aGUgZW5kIG9mIGRhdGEuXG4gIGZvciAodmFyIGkgPSAwLCBsZW4gPSB0aGlzLl9zdHJlYW1zLmxlbmd0aDsgaSA8IGxlbjsgaSsrKSB7XG4gICAgaWYgKHR5cGVvZiB0aGlzLl9zdHJlYW1zW2ldICE9PSAnZnVuY3Rpb24nKSB7XG5cbiAgICAgIC8vIEFkZCBjb250ZW50IHRvIHRoZSBidWZmZXIuXG4gICAgICBpZihCdWZmZXIuaXNCdWZmZXIodGhpcy5fc3RyZWFtc1tpXSkpIHtcbiAgICAgICAgZGF0YUJ1ZmZlciA9IEJ1ZmZlci5jb25jYXQoIFtkYXRhQnVmZmVyLCB0aGlzLl9zdHJlYW1zW2ldXSk7XG4gICAgICB9ZWxzZSB7XG4gICAgICAgIGRhdGFCdWZmZXIgPSBCdWZmZXIuY29uY2F0KCBbZGF0YUJ1ZmZlciwgQnVmZmVyLmZyb20odGhpcy5fc3RyZWFtc1tpXSldKTtcbiAgICAgIH1cblxuICAgICAgLy8gQWRkIGJyZWFrIGFmdGVyIGNvbnRlbnQuXG4gICAgICBpZiAodHlwZW9mIHRoaXMuX3N0cmVhbXNbaV0gIT09ICdzdHJpbmcnIHx8IHRoaXMuX3N0cmVhbXNbaV0uc3Vic3RyaW5nKCAyLCBib3VuZGFyeS5sZW5ndGggKyAyICkgIT09IGJvdW5kYXJ5KSB7XG4gICAgICAgIGRhdGFCdWZmZXIgPSBCdWZmZXIuY29uY2F0KCBbZGF0YUJ1ZmZlciwgQnVmZmVyLmZyb20oRm9ybURhdGEuTElORV9CUkVBSyldICk7XG4gICAgICB9XG4gICAgfVxuICB9XG5cbiAgLy8gQWRkIHRoZSBmb290ZXIgYW5kIHJldHVybiB0aGUgQnVmZmVyIG9iamVjdC5cbiAgcmV0dXJuIEJ1ZmZlci5jb25jYXQoIFtkYXRhQnVmZmVyLCBCdWZmZXIuZnJvbSh0aGlzLl9sYXN0Qm91bmRhcnkoKSldICk7XG59O1xuXG5Gb3JtRGF0YS5wcm90b3R5cGUuX2dlbmVyYXRlQm91bmRhcnkgPSBmdW5jdGlvbigpIHtcbiAgLy8gVGhpcyBnZW5lcmF0ZXMgYSA1MCBjaGFyYWN0ZXIgYm91bmRhcnkgc2ltaWxhciB0byB0aG9zZSB1c2VkIGJ5IEZpcmVmb3guXG4gIC8vIFRoZXkgYXJlIG9wdGltaXplZCBmb3IgYm95ZXItbW9vcmUgcGFyc2luZy5cbiAgdmFyIGJvdW5kYXJ5ID0gJy0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tJztcbiAgZm9yICh2YXIgaSA9IDA7IGkgPCAyNDsgaSsrKSB7XG4gICAgYm91bmRhcnkgKz0gTWF0aC5mbG9vcihNYXRoLnJhbmRvbSgpICogMTApLnRvU3RyaW5nKDE2KTtcbiAgfVxuXG4gIHRoaXMuX2JvdW5kYXJ5ID0gYm91bmRhcnk7XG59O1xuXG4vLyBOb3RlOiBnZXRMZW5ndGhTeW5jIERPRVNOJ1QgY2FsY3VsYXRlIHN0cmVhbXMgbGVuZ3RoXG4vLyBBcyB3b3JrYXJvdW5kIG9uZSBjYW4gY2FsY3VsYXRlIGZpbGUgc2l6ZSBtYW51YWxseVxuLy8gYW5kIGFkZCBpdCBhcyBrbm93bkxlbmd0aCBvcHRpb25cbkZvcm1EYXRhLnByb3RvdHlwZS5nZXRMZW5ndGhTeW5jID0gZnVuY3Rpb24oKSB7XG4gIHZhciBrbm93bkxlbmd0aCA9IHRoaXMuX292ZXJoZWFkTGVuZ3RoICsgdGhpcy5fdmFsdWVMZW5ndGg7XG5cbiAgLy8gRG9uJ3QgZ2V0IGNvbmZ1c2VkLCB0aGVyZSBhcmUgMyBcImludGVybmFsXCIgc3RyZWFtcyBmb3IgZWFjaCBrZXl2YWwgcGFpclxuICAvLyBzbyBpdCBiYXNpY2FsbHkgY2hlY2tzIGlmIHRoZXJlIGlzIGFueSB2YWx1ZSBhZGRlZCB0byB0aGUgZm9ybVxuICBpZiAodGhpcy5fc3RyZWFtcy5sZW5ndGgpIHtcbiAgICBrbm93bkxlbmd0aCArPSB0aGlzLl9sYXN0Qm91bmRhcnkoKS5sZW5ndGg7XG4gIH1cblxuICAvLyBodHRwczovL2dpdGh1Yi5jb20vZm9ybS1kYXRhL2Zvcm0tZGF0YS9pc3N1ZXMvNDBcbiAgaWYgKCF0aGlzLmhhc0tub3duTGVuZ3RoKCkpIHtcbiAgICAvLyBTb21lIGFzeW5jIGxlbmd0aCByZXRyaWV2ZXJzIGFyZSBwcmVzZW50XG4gICAgLy8gdGhlcmVmb3JlIHN5bmNocm9ub3VzIGxlbmd0aCBjYWxjdWxhdGlvbiBpcyBmYWxzZS5cbiAgICAvLyBQbGVhc2UgdXNlIGdldExlbmd0aChjYWxsYmFjaykgdG8gZ2V0IHByb3BlciBsZW5ndGhcbiAgICB0aGlzLl9lcnJvcihuZXcgRXJyb3IoJ0Nhbm5vdCBjYWxjdWxhdGUgcHJvcGVyIGxlbmd0aCBpbiBzeW5jaHJvbm91cyB3YXkuJykpO1xuICB9XG5cbiAgcmV0dXJuIGtub3duTGVuZ3RoO1xufTtcblxuLy8gUHVibGljIEFQSSB0byBjaGVjayBpZiBsZW5ndGggb2YgYWRkZWQgdmFsdWVzIGlzIGtub3duXG4vLyBodHRwczovL2dpdGh1Yi5jb20vZm9ybS1kYXRhL2Zvcm0tZGF0YS9pc3N1ZXMvMTk2XG4vLyBodHRwczovL2dpdGh1Yi5jb20vZm9ybS1kYXRhL2Zvcm0tZGF0YS9pc3N1ZXMvMjYyXG5Gb3JtRGF0YS5wcm90b3R5cGUuaGFzS25vd25MZW5ndGggPSBmdW5jdGlvbigpIHtcbiAgdmFyIGhhc0tub3duTGVuZ3RoID0gdHJ1ZTtcblxuICBpZiAodGhpcy5fdmFsdWVzVG9NZWFzdXJlLmxlbmd0aCkge1xuICAgIGhhc0tub3duTGVuZ3RoID0gZmFsc2U7XG4gIH1cblxuICByZXR1cm4gaGFzS25vd25MZW5ndGg7XG59O1xuXG5Gb3JtRGF0YS5wcm90b3R5cGUuZ2V0TGVuZ3RoID0gZnVuY3Rpb24oY2IpIHtcbiAgdmFyIGtub3duTGVuZ3RoID0gdGhpcy5fb3ZlcmhlYWRMZW5ndGggKyB0aGlzLl92YWx1ZUxlbmd0aDtcblxuICBpZiAodGhpcy5fc3RyZWFtcy5sZW5ndGgpIHtcbiAgICBrbm93bkxlbmd0aCArPSB0aGlzLl9sYXN0Qm91bmRhcnkoKS5sZW5ndGg7XG4gIH1cblxuICBpZiAoIXRoaXMuX3ZhbHVlc1RvTWVhc3VyZS5sZW5ndGgpIHtcbiAgICBwcm9jZXNzLm5leHRUaWNrKGNiLmJpbmQodGhpcywgbnVsbCwga25vd25MZW5ndGgpKTtcbiAgICByZXR1cm47XG4gIH1cblxuICBhc3luY2tpdC5wYXJhbGxlbCh0aGlzLl92YWx1ZXNUb01lYXN1cmUsIHRoaXMuX2xlbmd0aFJldHJpZXZlciwgZnVuY3Rpb24oZXJyLCB2YWx1ZXMpIHtcbiAgICBpZiAoZXJyKSB7XG4gICAgICBjYihlcnIpO1xuICAgICAgcmV0dXJuO1xuICAgIH1cblxuICAgIHZhbHVlcy5mb3JFYWNoKGZ1bmN0aW9uKGxlbmd0aCkge1xuICAgICAga25vd25MZW5ndGggKz0gbGVuZ3RoO1xuICAgIH0pO1xuXG4gICAgY2IobnVsbCwga25vd25MZW5ndGgpO1xuICB9KTtcbn07XG5cbkZvcm1EYXRhLnByb3RvdHlwZS5zdWJtaXQgPSBmdW5jdGlvbihwYXJhbXMsIGNiKSB7XG4gIHZhciByZXF1ZXN0XG4gICAgLCBvcHRpb25zXG4gICAgLCBkZWZhdWx0cyA9IHttZXRob2Q6ICdwb3N0J31cbiAgICA7XG5cbiAgLy8gcGFyc2UgcHJvdmlkZWQgdXJsIGlmIGl0J3Mgc3RyaW5nXG4gIC8vIG9yIHRyZWF0IGl0IGFzIG9wdGlvbnMgb2JqZWN0XG4gIGlmICh0eXBlb2YgcGFyYW1zID09ICdzdHJpbmcnKSB7XG5cbiAgICBwYXJhbXMgPSBwYXJzZVVybChwYXJhbXMpO1xuICAgIG9wdGlvbnMgPSBwb3B1bGF0ZSh7XG4gICAgICBwb3J0OiBwYXJhbXMucG9ydCxcbiAgICAgIHBhdGg6IHBhcmFtcy5wYXRobmFtZSxcbiAgICAgIGhvc3Q6IHBhcmFtcy5ob3N0bmFtZSxcbiAgICAgIHByb3RvY29sOiBwYXJhbXMucHJvdG9jb2xcbiAgICB9LCBkZWZhdWx0cyk7XG5cbiAgLy8gdXNlIGN1c3RvbSBwYXJhbXNcbiAgfSBlbHNlIHtcblxuICAgIG9wdGlvbnMgPSBwb3B1bGF0ZShwYXJhbXMsIGRlZmF1bHRzKTtcbiAgICAvLyBpZiBubyBwb3J0IHByb3ZpZGVkIHVzZSBkZWZhdWx0IG9uZVxuICAgIGlmICghb3B0aW9ucy5wb3J0KSB7XG4gICAgICBvcHRpb25zLnBvcnQgPSBvcHRpb25zLnByb3RvY29sID09ICdodHRwczonID8gNDQzIDogODA7XG4gICAgfVxuICB9XG5cbiAgLy8gcHV0IHRoYXQgZ29vZCBjb2RlIGluIGdldEhlYWRlcnMgdG8gc29tZSB1c2VcbiAgb3B0aW9ucy5oZWFkZXJzID0gdGhpcy5nZXRIZWFkZXJzKHBhcmFtcy5oZWFkZXJzKTtcblxuICAvLyBodHRwcyBpZiBzcGVjaWZpZWQsIGZhbGxiYWNrIHRvIGh0dHAgaW4gYW55IG90aGVyIGNhc2VcbiAgaWYgKG9wdGlvbnMucHJvdG9jb2wgPT0gJ2h0dHBzOicpIHtcbiAgICByZXF1ZXN0ID0gaHR0cHMucmVxdWVzdChvcHRpb25zKTtcbiAgfSBlbHNlIHtcbiAgICByZXF1ZXN0ID0gaHR0cC5yZXF1ZXN0KG9wdGlvbnMpO1xuICB9XG5cbiAgLy8gZ2V0IGNvbnRlbnQgbGVuZ3RoIGFuZCBmaXJlIGF3YXlcbiAgdGhpcy5nZXRMZW5ndGgoZnVuY3Rpb24oZXJyLCBsZW5ndGgpIHtcbiAgICBpZiAoZXJyICYmIGVyciAhPT0gJ1Vua25vd24gc3RyZWFtJykge1xuICAgICAgdGhpcy5fZXJyb3IoZXJyKTtcbiAgICAgIHJldHVybjtcbiAgICB9XG5cbiAgICAvLyBhZGQgY29udGVudCBsZW5ndGhcbiAgICBpZiAobGVuZ3RoKSB7XG4gICAgICByZXF1ZXN0LnNldEhlYWRlcignQ29udGVudC1MZW5ndGgnLCBsZW5ndGgpO1xuICAgIH1cblxuICAgIHRoaXMucGlwZShyZXF1ZXN0KTtcbiAgICBpZiAoY2IpIHtcbiAgICAgIHZhciBvblJlc3BvbnNlO1xuXG4gICAgICB2YXIgY2FsbGJhY2sgPSBmdW5jdGlvbiAoZXJyb3IsIHJlc3BvbmNlKSB7XG4gICAgICAgIHJlcXVlc3QucmVtb3ZlTGlzdGVuZXIoJ2Vycm9yJywgY2FsbGJhY2spO1xuICAgICAgICByZXF1ZXN0LnJlbW92ZUxpc3RlbmVyKCdyZXNwb25zZScsIG9uUmVzcG9uc2UpO1xuXG4gICAgICAgIHJldHVybiBjYi5jYWxsKHRoaXMsIGVycm9yLCByZXNwb25jZSk7XG4gICAgICB9O1xuXG4gICAgICBvblJlc3BvbnNlID0gY2FsbGJhY2suYmluZCh0aGlzLCBudWxsKTtcblxuICAgICAgcmVxdWVzdC5vbignZXJyb3InLCBjYWxsYmFjayk7XG4gICAgICByZXF1ZXN0Lm9uKCdyZXNwb25zZScsIG9uUmVzcG9uc2UpO1xuICAgIH1cbiAgfS5iaW5kKHRoaXMpKTtcblxuICByZXR1cm4gcmVxdWVzdDtcbn07XG5cbkZvcm1EYXRhLnByb3RvdHlwZS5fZXJyb3IgPSBmdW5jdGlvbihlcnIpIHtcbiAgaWYgKCF0aGlzLmVycm9yKSB7XG4gICAgdGhpcy5lcnJvciA9IGVycjtcbiAgICB0aGlzLnBhdXNlKCk7XG4gICAgdGhpcy5lbWl0KCdlcnJvcicsIGVycik7XG4gIH1cbn07XG5cbkZvcm1EYXRhLnByb3RvdHlwZS50b1N0cmluZyA9IGZ1bmN0aW9uICgpIHtcbiAgcmV0dXJuICdbb2JqZWN0IEZvcm1EYXRhXSc7XG59O1xuIiwiLy8gcG9wdWxhdGVzIG1pc3NpbmcgdmFsdWVzXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uKGRzdCwgc3JjKSB7XG5cbiAgT2JqZWN0LmtleXMoc3JjKS5mb3JFYWNoKGZ1bmN0aW9uKHByb3ApXG4gIHtcbiAgICBkc3RbcHJvcF0gPSBkc3RbcHJvcF0gfHwgc3JjW3Byb3BdO1xuICB9KTtcblxuICByZXR1cm4gZHN0O1xufTtcbiIsImltcG9ydCB7XG4gIEROU1JlY29yZCxcbiAgRG9tYWluRGF0YSxcbiAgRG9tYWluU2hvcnREYXRhLFxuICBURG9tYWluXG59IGZyb20gJy4uLy4uL1R5cGVzL0RvbWFpbnMnO1xuXG4vKiBlc2xpbnQtZGlzYWJsZSBjYW1lbGNhc2UgKi9cbmV4cG9ydCBkZWZhdWx0IGNsYXNzIERvbWFpbiBpbXBsZW1lbnRzIFREb21haW4ge1xuICBuYW1lOiBzdHJpbmc7XG4gIHJlcXVpcmVfdGxzOiBib29sZWFuO1xuICBza2lwX3ZlcmlmaWNhdGlvbjogYm9vbGVhbjtcbiAgc3RhdGU6IHN0cmluZztcbiAgd2lsZGNhcmQ6IGJvb2xlYW47XG4gIHNwYW1fYWN0aW9uOiBzdHJpbmc7XG4gIGNyZWF0ZWRfYXQ6IHN0cmluZztcbiAgc210cF9wYXNzd29yZDogc3RyaW5nO1xuICBzbXRwX2xvZ2luOiBzdHJpbmc7XG4gIHR5cGU6IHN0cmluZztcbiAgcmVjZWl2aW5nX2Ruc19yZWNvcmRzOiBETlNSZWNvcmRbXSB8IG51bGw7XG4gIHNlbmRpbmdfZG5zX3JlY29yZHM6IEROU1JlY29yZFtdIHwgbnVsbDtcbiAgaWQ/OiBzdHJpbmc7XG4gIGlzX2Rpc2FibGVkPzogYm9vbGVhbjtcbiAgd2ViX3ByZWZpeD86IHN0cmluZztcbiAgd2ViX3NjaGVtZT86IHN0cmluZztcblxuICBjb25zdHJ1Y3RvcihcbiAgICBkYXRhOiBEb21haW5TaG9ydERhdGEgfCBEb21haW5EYXRhLFxuICAgIHJlY2VpdmluZz86IEROU1JlY29yZFtdIHwgbnVsbCxcbiAgICBzZW5kaW5nPzogRE5TUmVjb3JkW10gfCBudWxsXG4gICkge1xuICAgIHRoaXMubmFtZSA9IGRhdGEubmFtZTtcbiAgICB0aGlzLnJlcXVpcmVfdGxzID0gZGF0YS5yZXF1aXJlX3RscztcbiAgICB0aGlzLnNraXBfdmVyaWZpY2F0aW9uID0gZGF0YS5za2lwX3ZlcmlmaWNhdGlvbjtcbiAgICB0aGlzLnN0YXRlID0gZGF0YS5zdGF0ZTtcbiAgICB0aGlzLndpbGRjYXJkID0gZGF0YS53aWxkY2FyZDtcbiAgICB0aGlzLnNwYW1fYWN0aW9uID0gZGF0YS5zcGFtX2FjdGlvbjtcbiAgICB0aGlzLmNyZWF0ZWRfYXQgPSBkYXRhLmNyZWF0ZWRfYXQ7XG4gICAgdGhpcy5zbXRwX3Bhc3N3b3JkID0gZGF0YS5zbXRwX3Bhc3N3b3JkO1xuICAgIHRoaXMuc210cF9sb2dpbiA9IGRhdGEuc210cF9sb2dpbjtcbiAgICB0aGlzLnR5cGUgPSBkYXRhLnR5cGU7XG4gICAgdGhpcy5yZWNlaXZpbmdfZG5zX3JlY29yZHMgPSByZWNlaXZpbmcgfHwgbnVsbDtcbiAgICB0aGlzLnNlbmRpbmdfZG5zX3JlY29yZHMgPSBzZW5kaW5nIHx8IG51bGw7XG4gICAgLypcbiAgICAgIGRvbWFpbiBsaXN0IGhhcyBzaG9ydGVyIHJlc3BvbnNlIHRoZW4gZ2V0LCBjcmVhdGUsIGFuZCB1cGRhdGUgbWV0aG9kcy5cbiAgICAqL1xuXG4gICAgY29uc3QgZHluYW1pY0tleXM6IChrZXlvZiBEb21haW5EYXRhKVtdID0gWydpZCcsICdpc19kaXNhYmxlZCcsICd3ZWJfcHJlZml4JywgJ3dlYl9zY2hlbWUnXTtcblxuICAgIGNvbnN0IGR5bmFtaWNQcm9wZXJ0aWVzID0gZHluYW1pY0tleXMucmVkdWNlKChhY2MsIHByb3BlcnR5TmFtZSkgPT4ge1xuICAgICAgaWYgKHByb3BlcnR5TmFtZSBpbiBkYXRhKSB7XG4gICAgICAgIGNvbnN0IHByb3AgPSBwcm9wZXJ0eU5hbWUgYXMga2V5b2YgRG9tYWluO1xuICAgICAgICBhY2NbcHJvcF0gPSAoZGF0YSBhcyBEb21haW5EYXRhKVtwcm9wZXJ0eU5hbWVdO1xuICAgICAgfVxuICAgICAgcmV0dXJuIGFjYztcbiAgICB9LCB7fSBhcyBSZWNvcmQ8a2V5b2YgRG9tYWluLCBzdHJpbmcgfCBib29sZWFuPik7XG4gICAgT2JqZWN0LmFzc2lnbih0aGlzLCBkeW5hbWljUHJvcGVydGllcyk7XG4gIH1cbn1cbiIsImltcG9ydCB1cmxqb2luIGZyb20gJ3VybC1qb2luJztcbmltcG9ydCB7XG4gIElEb21haW5UZW1wbGF0ZXNDbGllbnQsXG4gIElEb21haW5UYWdzQ2xpZW50LFxuICBJRG9tYWluQ3JlZGVudGlhbHMsXG4gIElEb21haW5zQ2xpZW50XG59IGZyb20gJy4uLy4uL0ludGVyZmFjZXMvRG9tYWlucyc7XG5cbmltcG9ydCB7IEFQSVJlc3BvbnNlIH0gZnJvbSAnLi4vLi4vVHlwZXMvQ29tbW9uL0FwaVJlc3BvbnNlJztcbmltcG9ydCBBUElFcnJvciBmcm9tICcuLi9jb21tb24vRXJyb3InO1xuaW1wb3J0IHsgQVBJRXJyb3JPcHRpb25zIH0gZnJvbSAnLi4vLi4vVHlwZXMvQ29tbW9uJztcblxuaW1wb3J0IFJlcXVlc3QgZnJvbSAnLi4vY29tbW9uL1JlcXVlc3QnO1xuXG5pbXBvcnQgRG9tYWluQ3JlZGVudGlhbHNDbGllbnQgZnJvbSAnLi9kb21haW5zQ3JlZGVudGlhbHMnO1xuaW1wb3J0IERvbWFpblRlbXBsYXRlc0NsaWVudCBmcm9tICcuL2RvbWFpbnNUZW1wbGF0ZXMnO1xuaW1wb3J0IERvbWFpblRhZ3NDbGllbnQgZnJvbSAnLi9kb21haW5zVGFncyc7XG5pbXBvcnQge1xuICBEZXN0cm95ZWREb21haW5SZXNwb25zZSxcbiAgTWVzc2FnZVJlc3BvbnNlLFxuICBEb21haW5MaXN0UmVzcG9uc2VEYXRhLFxuICBEb21haW5SZXNwb25zZURhdGEsXG4gIERvbWFpblRyYWNraW5nUmVzcG9uc2UsXG4gIERvbWFpblRyYWNraW5nRGF0YSxcbiAgVXBkYXRlRG9tYWluVHJhY2tpbmdSZXNwb25zZSxcbiAgVXBkYXRlZE9wZW5UcmFja2luZyxcbiAgRG9tYWluc1F1ZXJ5LFxuICBEb21haW5JbmZvLFxuICBDb25uZWN0aW9uU2V0dGluZ3MsXG4gIENvbm5lY3Rpb25TZXR0aW5nc1Jlc3BvbnNlLFxuICBVcGRhdGVkQ29ubmVjdGlvblNldHRpbmdzLFxuICBVcGRhdGVkQ29ubmVjdGlvblNldHRpbmdzUmVzLFxuICBPcGVuVHJhY2tpbmdJbmZvLFxuICBDbGlja1RyYWNraW5nSW5mbyxcbiAgVW5zdWJzY3JpYmVUcmFja2luZ0luZm8sXG4gIFJlcGxhY2VtZW50Rm9yUG9vbCxcbiAgREtJTUF1dGhvcml0eUluZm8sXG4gIFVwZGF0ZWRES0lNQXV0aG9yaXR5LFxuICBVcGRhdGVkREtJTUF1dGhvcml0eVJlc3BvbnNlLFxuICBES0lNU2VsZWN0b3JJbmZvLFxuICBVcGRhdGVkREtJTVNlbGVjdG9yUmVzcG9uc2UsXG4gIFdlYlByZWZpeEluZm8sXG4gIFVwZGF0ZWRXZWJQcmVmaXhSZXNwb25zZSxcbiAgVERvbWFpbixcbiAgRG9tYWluVXBkYXRlSW5mbyxcbiAgRG9tYWluVXBkYXRlSW5mb1JlcSxcbiAgRG9tYWluSW5mb1JlcSxcbiAgQm9vbFRvU3RyaW5nLFxufSBmcm9tICcuLi8uLi9UeXBlcy9Eb21haW5zJztcbmltcG9ydCBEb21haW4gZnJvbSAnLi9kb21haW4nO1xuXG5leHBvcnQgZGVmYXVsdCBjbGFzcyBEb21haW5zQ2xpZW50IGltcGxlbWVudHMgSURvbWFpbnNDbGllbnQge1xuICByZXF1ZXN0OiBSZXF1ZXN0O1xuICBwdWJsaWMgZG9tYWluQ3JlZGVudGlhbHM6IElEb21haW5DcmVkZW50aWFscztcbiAgcHVibGljIGRvbWFpblRlbXBsYXRlczogSURvbWFpblRlbXBsYXRlc0NsaWVudDtcbiAgcHVibGljIGRvbWFpblRhZ3M6IElEb21haW5UYWdzQ2xpZW50O1xuXG4gIGNvbnN0cnVjdG9yKFxuICAgIHJlcXVlc3Q6IFJlcXVlc3QsXG4gICAgZG9tYWluQ3JlZGVudGlhbHNDbGllbnQ6IERvbWFpbkNyZWRlbnRpYWxzQ2xpZW50LFxuICAgIGRvbWFpblRlbXBsYXRlc0NsaWVudDogRG9tYWluVGVtcGxhdGVzQ2xpZW50LFxuICAgIGRvbWFpblRhZ3NDbGllbnQ6IERvbWFpblRhZ3NDbGllbnRcbiAgKSB7XG4gICAgdGhpcy5yZXF1ZXN0ID0gcmVxdWVzdDtcbiAgICB0aGlzLmRvbWFpbkNyZWRlbnRpYWxzID0gZG9tYWluQ3JlZGVudGlhbHNDbGllbnQ7XG4gICAgdGhpcy5kb21haW5UZW1wbGF0ZXMgPSBkb21haW5UZW1wbGF0ZXNDbGllbnQ7XG4gICAgdGhpcy5kb21haW5UYWdzID0gZG9tYWluVGFnc0NsaWVudDtcbiAgfVxuXG4gIHByaXZhdGUgX2hhbmRsZUJvb2xWYWx1ZXMoXG4gICAgZGF0YTogRG9tYWluSW5mbyB8IERvbWFpblVwZGF0ZUluZm9cbiAgKTogRG9tYWluSW5mb1JlcSB8IERvbWFpblVwZGF0ZUluZm9SZXEge1xuICAgIGNvbnN0IHByb3BzRm9yUmVwbGFjZW1lbnQgPSBkYXRhIGFzIEJvb2xUb1N0cmluZztcbiAgICBjb25zdCByZXBsYWNlZFByb3BzID0gT2JqZWN0LmtleXMocHJvcHNGb3JSZXBsYWNlbWVudCkucmVkdWNlKChhY2MsIGtleSkgPT4ge1xuICAgICAgY29uc3QgcHJvcCA9IGtleSBhcyBrZXlvZiBCb29sVG9TdHJpbmc7XG4gICAgICBpZiAodHlwZW9mIHByb3BzRm9yUmVwbGFjZW1lbnRbcHJvcF0gPT09ICdib29sZWFuJykge1xuICAgICAgICBjb25zdCB2YWx1ZSA9IHByb3BzRm9yUmVwbGFjZW1lbnRbcHJvcF0gYXMgYm9vbGVhbjtcbiAgICAgICAgYWNjW3Byb3BdID0gKHZhbHVlLnRvU3RyaW5nKCkgPT09ICd0cnVlJykgPyAndHJ1ZScgOiAnZmFsc2UnO1xuICAgICAgfVxuICAgICAgcmV0dXJuIGFjYztcbiAgICB9LCB7fSBhcyBSZWNvcmQ8a2V5b2YgQm9vbFRvU3RyaW5nLCAndHJ1ZSd8ICdmYWxzZSc+KTtcbiAgICByZXR1cm4geyAuLi5kYXRhLCAuLi5yZXBsYWNlZFByb3BzIH0gYXMgRG9tYWluVXBkYXRlSW5mb1JlcSB8IERvbWFpbkluZm9SZXE7XG4gIH1cblxuICBwcml2YXRlIF9wYXJzZU1lc3NhZ2UocmVzcG9uc2U6IERlc3Ryb3llZERvbWFpblJlc3BvbnNlKSA6IE1lc3NhZ2VSZXNwb25zZSB7XG4gICAgcmV0dXJuIHJlc3BvbnNlLmJvZHk7XG4gIH1cblxuICBwcml2YXRlIHBhcnNlRG9tYWluTGlzdChyZXNwb25zZTogRG9tYWluTGlzdFJlc3BvbnNlRGF0YSk6IFREb21haW5bXSB7XG4gICAgaWYgKHJlc3BvbnNlLmJvZHkgJiYgcmVzcG9uc2UuYm9keS5pdGVtcykge1xuICAgICAgcmV0dXJuIHJlc3BvbnNlLmJvZHkuaXRlbXMubWFwKGZ1bmN0aW9uIChpdGVtKSB7XG4gICAgICAgIHJldHVybiBuZXcgRG9tYWluKGl0ZW0pO1xuICAgICAgfSk7XG4gICAgfVxuICAgIHJldHVybiBbXTtcbiAgfVxuXG4gIHByaXZhdGUgX3BhcnNlRG9tYWluKHJlc3BvbnNlOiBEb21haW5SZXNwb25zZURhdGEpOiBURG9tYWluIHtcbiAgICByZXR1cm4gbmV3IERvbWFpbihcbiAgICAgIHJlc3BvbnNlLmJvZHkuZG9tYWluLFxuICAgICAgcmVzcG9uc2UuYm9keS5yZWNlaXZpbmdfZG5zX3JlY29yZHMsXG4gICAgICByZXNwb25zZS5ib2R5LnNlbmRpbmdfZG5zX3JlY29yZHNcbiAgICApO1xuICB9XG5cbiAgcHJpdmF0ZSBfcGFyc2VUcmFja2luZ1NldHRpbmdzKHJlc3BvbnNlOiBEb21haW5UcmFja2luZ1Jlc3BvbnNlKSA6IERvbWFpblRyYWNraW5nRGF0YSB7XG4gICAgcmV0dXJuIHJlc3BvbnNlLmJvZHkudHJhY2tpbmc7XG4gIH1cblxuICBwcml2YXRlIF9wYXJzZVRyYWNraW5nVXBkYXRlKHJlc3BvbnNlOiBVcGRhdGVEb21haW5UcmFja2luZ1Jlc3BvbnNlKSA6VXBkYXRlZE9wZW5UcmFja2luZyB7XG4gICAgcmV0dXJuIHJlc3BvbnNlLmJvZHk7XG4gIH1cblxuICBsaXN0KHF1ZXJ5PzogRG9tYWluc1F1ZXJ5KTogUHJvbWlzZTxURG9tYWluW10+IHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LmdldCgnL3YzL2RvbWFpbnMnLCBxdWVyeSlcbiAgICAgIC50aGVuKChyZXMgOiBBUElSZXNwb25zZSkgPT4gdGhpcy5wYXJzZURvbWFpbkxpc3QocmVzIGFzIERvbWFpbkxpc3RSZXNwb25zZURhdGEpKTtcbiAgfVxuXG4gIGdldChkb21haW46IHN0cmluZykgOiBQcm9taXNlPFREb21haW4+IHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LmdldChgL3YzL2RvbWFpbnMvJHtkb21haW59YClcbiAgICAgIC50aGVuKChyZXMgOiBBUElSZXNwb25zZSkgPT4gdGhpcy5fcGFyc2VEb21haW4ocmVzIGFzIERvbWFpblJlc3BvbnNlRGF0YSkpO1xuICB9XG5cbiAgY3JlYXRlKGRhdGE6IERvbWFpbkluZm8pIDogUHJvbWlzZTxURG9tYWluPiB7XG4gICAgY29uc3QgcG9zdE9iaiA9IHRoaXMuX2hhbmRsZUJvb2xWYWx1ZXMoZGF0YSk7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5wb3N0V2l0aEZEKCcvdjMvZG9tYWlucycsIHBvc3RPYmopXG4gICAgICAudGhlbigocmVzIDogQVBJUmVzcG9uc2UpID0+IHRoaXMuX3BhcnNlRG9tYWluKHJlcyBhcyBEb21haW5SZXNwb25zZURhdGEpKTtcbiAgfVxuXG4gIHVwZGF0ZShkb21haW46IHN0cmluZywgZGF0YTogRG9tYWluVXBkYXRlSW5mbykgOiBQcm9taXNlPFREb21haW4+IHtcbiAgICBjb25zdCBwdXREYXRhID0gdGhpcy5faGFuZGxlQm9vbFZhbHVlcyhkYXRhKTtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LnB1dFdpdGhGRChgL3YzL2RvbWFpbnMvJHtkb21haW59YCwgcHV0RGF0YSlcbiAgICAgIC50aGVuKChyZXMgOiBBUElSZXNwb25zZSkgPT4gdGhpcy5fcGFyc2VEb21haW4ocmVzIGFzIERvbWFpblJlc3BvbnNlRGF0YSkpO1xuICB9XG5cbiAgdmVyaWZ5KGRvbWFpbjogc3RyaW5nKTogUHJvbWlzZTxURG9tYWluPiB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5wdXQoYC92My9kb21haW5zLyR7ZG9tYWlufS92ZXJpZnlgKVxuICAgICAgLnRoZW4oKHJlcyA6IEFQSVJlc3BvbnNlKSA9PiB0aGlzLl9wYXJzZURvbWFpbihyZXMgYXMgRG9tYWluUmVzcG9uc2VEYXRhKSk7XG4gIH1cblxuICBkZXN0cm95KGRvbWFpbjogc3RyaW5nKTogUHJvbWlzZTxNZXNzYWdlUmVzcG9uc2U+IHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LmRlbGV0ZShgL3YzL2RvbWFpbnMvJHtkb21haW59YClcbiAgICAgIC50aGVuKChyZXMgOiBBUElSZXNwb25zZSkgPT4gdGhpcy5fcGFyc2VNZXNzYWdlKHJlcyBhcyBEZXN0cm95ZWREb21haW5SZXNwb25zZSkpO1xuICB9XG5cbiAgZ2V0Q29ubmVjdGlvbihkb21haW46IHN0cmluZyk6IFByb21pc2U8Q29ubmVjdGlvblNldHRpbmdzPiB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5nZXQoYC92My9kb21haW5zLyR7ZG9tYWlufS9jb25uZWN0aW9uYClcbiAgICAgIC50aGVuKChyZXMgOiBBUElSZXNwb25zZSkgPT4gcmVzIGFzIENvbm5lY3Rpb25TZXR0aW5nc1Jlc3BvbnNlKVxuICAgICAgLnRoZW4oKHJlczpDb25uZWN0aW9uU2V0dGluZ3NSZXNwb25zZSkgPT4gcmVzLmJvZHkuY29ubmVjdGlvbiBhcyBDb25uZWN0aW9uU2V0dGluZ3MpO1xuICB9XG5cbiAgdXBkYXRlQ29ubmVjdGlvbihkb21haW46IHN0cmluZywgZGF0YTogQ29ubmVjdGlvblNldHRpbmdzKTogUHJvbWlzZTxVcGRhdGVkQ29ubmVjdGlvblNldHRpbmdzPiB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5wdXQoYC92My9kb21haW5zLyR7ZG9tYWlufS9jb25uZWN0aW9uYCwgZGF0YSlcbiAgICAgIC50aGVuKChyZXMgOiBBUElSZXNwb25zZSkgPT4gcmVzIGFzIFVwZGF0ZWRDb25uZWN0aW9uU2V0dGluZ3NSZXMpXG4gICAgICAudGhlbigocmVzOlVwZGF0ZWRDb25uZWN0aW9uU2V0dGluZ3NSZXMpID0+IHJlcy5ib2R5IGFzIFVwZGF0ZWRDb25uZWN0aW9uU2V0dGluZ3MpO1xuICB9XG5cbiAgLy8gVHJhY2tpbmdcblxuICBnZXRUcmFja2luZyhkb21haW46IHN0cmluZykgOiBQcm9taXNlPERvbWFpblRyYWNraW5nRGF0YT4ge1xuICAgIHJldHVybiB0aGlzLnJlcXVlc3QuZ2V0KHVybGpvaW4oJy92My9kb21haW5zJywgZG9tYWluLCAndHJhY2tpbmcnKSlcbiAgICAgIC50aGVuKHRoaXMuX3BhcnNlVHJhY2tpbmdTZXR0aW5ncyk7XG4gIH1cblxuICB1cGRhdGVUcmFja2luZyhcbiAgICBkb21haW46IHN0cmluZyxcbiAgICB0eXBlOiBzdHJpbmcsXG4gICAgZGF0YTogT3BlblRyYWNraW5nSW5mbyB8IENsaWNrVHJhY2tpbmdJbmZvIHwgVW5zdWJzY3JpYmVUcmFja2luZ0luZm9cbiAgKTogUHJvbWlzZTxVcGRhdGVkT3BlblRyYWNraW5nPiB7XG4gICAgaWYgKHR5cGVvZiBkYXRhPy5hY3RpdmUgPT09ICdib29sZWFuJykge1xuICAgICAgdGhyb3cgbmV3IEFQSUVycm9yKHsgc3RhdHVzOiA0MDAsIHN0YXR1c1RleHQ6ICdSZWNlaXZlZCBib29sZWFuIHZhbHVlIGZvciBhY3RpdmUgcHJvcGVydHknLCBib2R5OiB7IG1lc3NhZ2U6ICdQcm9wZXJ0eSBcImFjdGl2ZVwiIG11c3QgY29udGFpbiBzdHJpbmcgdmFsdWUuJyB9IH0gYXMgQVBJRXJyb3JPcHRpb25zKTtcbiAgICB9XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5wdXRXaXRoRkQodXJsam9pbignL3YzL2RvbWFpbnMnLCBkb21haW4sICd0cmFja2luZycsIHR5cGUpLCBkYXRhKVxuICAgICAgLnRoZW4oKHJlcyA6IEFQSVJlc3BvbnNlKSA9PiB0aGlzLl9wYXJzZVRyYWNraW5nVXBkYXRlKHJlcyBhcyBVcGRhdGVEb21haW5UcmFja2luZ1Jlc3BvbnNlKSk7XG4gIH1cblxuICAvLyBJUHNcblxuICBnZXRJcHMoZG9tYWluOiBzdHJpbmcpOiBQcm9taXNlPHN0cmluZ1tdPiB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5nZXQodXJsam9pbignL3YzL2RvbWFpbnMnLCBkb21haW4sICdpcHMnKSlcbiAgICAgIC50aGVuKChyZXNwb25zZTogQVBJUmVzcG9uc2UpID0+IHJlc3BvbnNlPy5ib2R5Py5pdGVtcyk7XG4gIH1cblxuICBhc3NpZ25JcChkb21haW46IHN0cmluZywgaXA6IHN0cmluZyk6IFByb21pc2U8QVBJUmVzcG9uc2U+IHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LnBvc3RXaXRoRkQodXJsam9pbignL3YzL2RvbWFpbnMnLCBkb21haW4sICdpcHMnKSwgeyBpcCB9KTtcbiAgfVxuXG4gIGRlbGV0ZUlwKGRvbWFpbjogc3RyaW5nLCBpcDogc3RyaW5nKTogUHJvbWlzZTxBUElSZXNwb25zZT4ge1xuICAgIHJldHVybiB0aGlzLnJlcXVlc3QuZGVsZXRlKHVybGpvaW4oJy92My9kb21haW5zJywgZG9tYWluLCAnaXBzJywgaXApKTtcbiAgfVxuXG4gIGxpbmtJcFBvb2woZG9tYWluOiBzdHJpbmcsIHBvb2xJZDogc3RyaW5nKTogUHJvbWlzZTxBUElSZXNwb25zZT4ge1xuICAgIHJldHVybiB0aGlzLnJlcXVlc3QucG9zdFdpdGhGRCh1cmxqb2luKCcvdjMvZG9tYWlucycsIGRvbWFpbiwgJ2lwcycpLCB7IHBvb2xfaWQ6IHBvb2xJZCB9KTtcbiAgfVxuXG4gIHVubGlua0lwUG9sbChkb21haW46IHN0cmluZywgcmVwbGFjZW1lbnQ6IFJlcGxhY2VtZW50Rm9yUG9vbCk6IFByb21pc2U8QVBJUmVzcG9uc2U+IHtcbiAgICBsZXQgc2VhcmNoUGFyYW1zID0gJyc7XG4gICAgaWYgKHJlcGxhY2VtZW50LnBvb2xfaWQgJiYgcmVwbGFjZW1lbnQuaXApIHtcbiAgICAgIHRocm93IG5ldyBBUElFcnJvcihcbiAgICAgICAge1xuICAgICAgICAgIHN0YXR1czogNDAwLFxuICAgICAgICAgIHN0YXR1c1RleHQ6ICdUb28gbXVjaCBkYXRhIGZvciByZXBsYWNlbWVudCcsXG4gICAgICAgICAgYm9keTogeyBtZXNzYWdlOiAnUGxlYXNlIHNwZWNpZnkgZWl0aGVyIHBvb2xfaWQgb3IgaXAgKG5vdCBib3RoKScgfVxuICAgICAgICB9IGFzIEFQSUVycm9yT3B0aW9uc1xuICAgICAgKTtcbiAgICB9IGVsc2UgaWYgKHJlcGxhY2VtZW50LnBvb2xfaWQpIHtcbiAgICAgIHNlYXJjaFBhcmFtcyA9IGA/cG9vbF9pZD0ke3JlcGxhY2VtZW50LnBvb2xfaWR9YDtcbiAgICB9IGVsc2UgaWYgKHJlcGxhY2VtZW50LmlwKSB7XG4gICAgICBzZWFyY2hQYXJhbXMgPSBgP2lwPSR7cmVwbGFjZW1lbnQuaXB9YDtcbiAgICB9XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5kZWxldGUodXJsam9pbignL3YzL2RvbWFpbnMnLCBkb21haW4sICdpcHMnLCAnaXBfcG9vbCcsIHNlYXJjaFBhcmFtcykpO1xuICB9XG5cbiAgdXBkYXRlREtJTUF1dGhvcml0eShkb21haW46IHN0cmluZywgZGF0YTogREtJTUF1dGhvcml0eUluZm8pOiBQcm9taXNlPFVwZGF0ZWRES0lNQXV0aG9yaXR5PiB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5wdXQoYC92My9kb21haW5zLyR7ZG9tYWlufS9ka2ltX2F1dGhvcml0eWAsIHt9LCB7IHF1ZXJ5OiBgc2VsZj0ke2RhdGEuc2VsZn1gIH0pXG4gICAgICAudGhlbigocmVzIDogQVBJUmVzcG9uc2UpID0+IHJlcyBhcyBVcGRhdGVkREtJTUF1dGhvcml0eVJlc3BvbnNlKVxuICAgICAgLnRoZW4oKHJlcyA6IFVwZGF0ZWRES0lNQXV0aG9yaXR5UmVzcG9uc2UpID0+IHJlcy5ib2R5IGFzIFVwZGF0ZWRES0lNQXV0aG9yaXR5KTtcbiAgfVxuXG4gIHVwZGF0ZURLSU1TZWxlY3Rvcihkb21haW46IHN0cmluZywgZGF0YTogREtJTVNlbGVjdG9ySW5mbyk6IFByb21pc2U8VXBkYXRlZERLSU1TZWxlY3RvclJlc3BvbnNlPiB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5wdXQoYC92My9kb21haW5zLyR7ZG9tYWlufS9ka2ltX3NlbGVjdG9yYCwge30sIHsgcXVlcnk6IGBka2ltX3NlbGVjdG9yPSR7ZGF0YS5ka2ltU2VsZWN0b3J9YCB9KVxuICAgICAgLnRoZW4oKHJlcyA6IEFQSVJlc3BvbnNlKSA9PiByZXMgYXMgVXBkYXRlZERLSU1TZWxlY3RvclJlc3BvbnNlKTtcbiAgfVxuXG4gIHVwZGF0ZVdlYlByZWZpeChkb21haW46IHN0cmluZywgZGF0YTogV2ViUHJlZml4SW5mbyk6IFByb21pc2U8VXBkYXRlZFdlYlByZWZpeFJlc3BvbnNlPiB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5wdXQoYC92My9kb21haW5zLyR7ZG9tYWlufS93ZWJfcHJlZml4YCwge30sIHsgcXVlcnk6IGB3ZWJfcHJlZml4PSR7ZGF0YS53ZWJQcmVmaXh9YCB9KVxuICAgICAgLnRoZW4oKHJlcyA6IEFQSVJlc3BvbnNlKSA9PiByZXMgYXMgVXBkYXRlZFdlYlByZWZpeFJlc3BvbnNlKTtcbiAgfVxufVxuIiwiaW1wb3J0IHVybGpvaW4gZnJvbSAndXJsLWpvaW4nO1xuaW1wb3J0IHsgQVBJUmVzcG9uc2UgfSBmcm9tICcuLi8uLi9UeXBlcy9Db21tb24vQXBpUmVzcG9uc2UnO1xuaW1wb3J0IHsgSURvbWFpbkNyZWRlbnRpYWxzIH0gZnJvbSAnLi4vLi4vSW50ZXJmYWNlcy9Eb21haW5zJztcbmltcG9ydCB7XG4gIERvbWFpbkNyZWRlbnRpYWxzUmVzcG9uc2VEYXRhLFxuICBEb21haW5DcmVkZW50aWFsc0xpc3QsXG4gIENyZWF0ZWRVcGRhdGVkRG9tYWluQ3JlZGVudGlhbHNSZXNwb25zZSxcbiAgRG9tYWluQ3JlZGVudGlhbHNSZXN1bHQsXG4gIERlbGV0ZWREb21haW5DcmVkZW50aWFsc1Jlc3BvbnNlLFxuICBEb21haW5DcmVkZW50aWFsc1F1ZXJ5LFxuICBEb21haW5DcmVkZW50aWFscyxcbiAgVXBkYXRlRG9tYWluQ3JlZGVudGlhbHNEYXRhXG59IGZyb20gJy4uLy4uL1R5cGVzL0RvbWFpbnMnO1xuaW1wb3J0IFJlcXVlc3QgZnJvbSAnLi4vY29tbW9uL1JlcXVlc3QnO1xuXG5leHBvcnQgZGVmYXVsdCBjbGFzcyBEb21haW5DcmVkZW50aWFsc0NsaWVudCBpbXBsZW1lbnRzIElEb21haW5DcmVkZW50aWFscyB7XG4gIGJhc2VSb3V0ZTogc3RyaW5nO1xuICByZXF1ZXN0OiBSZXF1ZXN0O1xuXG4gIGNvbnN0cnVjdG9yKHJlcXVlc3Q6IFJlcXVlc3QpIHtcbiAgICB0aGlzLnJlcXVlc3QgPSByZXF1ZXN0O1xuICAgIHRoaXMuYmFzZVJvdXRlID0gJy92My9kb21haW5zLyc7XG4gIH1cblxuICBwcml2YXRlIF9wYXJzZURvbWFpbkNyZWRlbnRpYWxzTGlzdChcbiAgICByZXNwb25zZTogRG9tYWluQ3JlZGVudGlhbHNSZXNwb25zZURhdGFcbiAgKTogRG9tYWluQ3JlZGVudGlhbHNMaXN0IHtcbiAgICByZXR1cm4ge1xuICAgICAgaXRlbXM6IHJlc3BvbnNlLmJvZHkuaXRlbXMsXG4gICAgICB0b3RhbENvdW50OiByZXNwb25zZS5ib2R5LnRvdGFsX2NvdW50XG4gICAgfTtcbiAgfVxuXG4gIHByaXZhdGUgX3BhcnNlTWVzc2FnZVJlc3BvbnNlKFxuICAgIHJlc3BvbnNlOiBDcmVhdGVkVXBkYXRlZERvbWFpbkNyZWRlbnRpYWxzUmVzcG9uc2VcbiAgKTogRG9tYWluQ3JlZGVudGlhbHNSZXN1bHQge1xuICAgIGNvbnN0IHJlc3VsdCA9IHtcbiAgICAgIHN0YXR1czogcmVzcG9uc2Uuc3RhdHVzLFxuICAgICAgbWVzc2FnZTogcmVzcG9uc2UuYm9keS5tZXNzYWdlXG4gICAgfSBhcyBEb21haW5DcmVkZW50aWFsc1Jlc3VsdDtcbiAgICByZXR1cm4gcmVzdWx0O1xuICB9XG5cbiAgcHJpdmF0ZSBfcGFyc2VEZWxldGVkUmVzcG9uc2UoXG4gICAgcmVzcG9uc2U6RGVsZXRlZERvbWFpbkNyZWRlbnRpYWxzUmVzcG9uc2VcbiAgKTogRG9tYWluQ3JlZGVudGlhbHNSZXN1bHQge1xuICAgIGNvbnN0IHJlc3VsdCA9IHtcbiAgICAgIHN0YXR1czogcmVzcG9uc2Uuc3RhdHVzLFxuICAgICAgbWVzc2FnZTogcmVzcG9uc2UuYm9keS5tZXNzYWdlLFxuICAgICAgc3BlYzogcmVzcG9uc2UuYm9keS5zcGVjXG4gICAgfSBhcyBEb21haW5DcmVkZW50aWFsc1Jlc3VsdDtcblxuICAgIHJldHVybiByZXN1bHQ7XG4gIH1cblxuICBsaXN0KGRvbWFpbjogc3RyaW5nLCBxdWVyeT86IERvbWFpbkNyZWRlbnRpYWxzUXVlcnkpOiBQcm9taXNlPERvbWFpbkNyZWRlbnRpYWxzTGlzdD4ge1xuICAgIHJldHVybiB0aGlzLnJlcXVlc3QuZ2V0KHVybGpvaW4odGhpcy5iYXNlUm91dGUsIGRvbWFpbiwgJy9jcmVkZW50aWFscycpLCBxdWVyeSlcbiAgICAgIC50aGVuKFxuICAgICAgICAocmVzOiBBUElSZXNwb25zZSkgPT4gdGhpcy5fcGFyc2VEb21haW5DcmVkZW50aWFsc0xpc3QocmVzIGFzIERvbWFpbkNyZWRlbnRpYWxzUmVzcG9uc2VEYXRhKVxuICAgICAgKTtcbiAgfVxuXG4gIGNyZWF0ZShcbiAgICBkb21haW46IHN0cmluZyxcbiAgICBkYXRhOiBEb21haW5DcmVkZW50aWFsc1xuICApOiBQcm9taXNlPERvbWFpbkNyZWRlbnRpYWxzUmVzdWx0PiB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5wb3N0V2l0aEZEKGAke3RoaXMuYmFzZVJvdXRlfSR7ZG9tYWlufS9jcmVkZW50aWFsc2AsIGRhdGEpXG4gICAgICAudGhlbigocmVzOiBBUElSZXNwb25zZSkgPT4gdGhpcy5fcGFyc2VNZXNzYWdlUmVzcG9uc2UocmVzKSk7XG4gIH1cblxuICB1cGRhdGUoXG4gICAgZG9tYWluOiBzdHJpbmcsXG4gICAgY3JlZGVudGlhbHNMb2dpbjogc3RyaW5nLFxuICAgIGRhdGE6IFVwZGF0ZURvbWFpbkNyZWRlbnRpYWxzRGF0YVxuICApOiBQcm9taXNlPERvbWFpbkNyZWRlbnRpYWxzUmVzdWx0PiB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5wdXRXaXRoRkQoYCR7dGhpcy5iYXNlUm91dGV9JHtkb21haW59L2NyZWRlbnRpYWxzLyR7Y3JlZGVudGlhbHNMb2dpbn1gLCBkYXRhKVxuICAgICAgLnRoZW4oKHJlczogQVBJUmVzcG9uc2UpID0+IHRoaXMuX3BhcnNlTWVzc2FnZVJlc3BvbnNlKHJlcykpO1xuICB9XG5cbiAgZGVzdHJveShcbiAgICBkb21haW46IHN0cmluZyxcbiAgICBjcmVkZW50aWFsc0xvZ2luOiBzdHJpbmdcbiAgKTogUHJvbWlzZTxEb21haW5DcmVkZW50aWFsc1Jlc3VsdD4ge1xuICAgIHJldHVybiB0aGlzLnJlcXVlc3QuZGVsZXRlKGAke3RoaXMuYmFzZVJvdXRlfSR7ZG9tYWlufS9jcmVkZW50aWFscy8ke2NyZWRlbnRpYWxzTG9naW59YClcbiAgICAgIC50aGVuKChyZXM6IEFQSVJlc3BvbnNlKSA9PiB0aGlzLl9wYXJzZURlbGV0ZWRSZXNwb25zZShyZXMpKTtcbiAgfVxufVxuIiwiaW1wb3J0IHVybGpvaW4gZnJvbSAndXJsLWpvaW4nO1xuaW1wb3J0IHsgQVBJUmVzcG9uc2UgfSBmcm9tICcuLi8uLi9UeXBlcy9Db21tb24vQXBpUmVzcG9uc2UnO1xuaW1wb3J0IFJlcXVlc3QgZnJvbSAnLi4vY29tbW9uL1JlcXVlc3QnO1xuXG5pbXBvcnQge1xuICBJRG9tYWluVGFnU3RhdGlzdGljUmVzdWx0LFxuICBJRG9tYWluVGFnc0NsaWVudFxufSBmcm9tICcuLi8uLi9JbnRlcmZhY2VzL0RvbWFpbnMnO1xuaW1wb3J0IE5hdmlnYXRpb25UaHJ1UGFnZXMgZnJvbSAnLi4vY29tbW9uL05hdmlnYXRpb25UaHJ1UGFnZXMnO1xuaW1wb3J0IHsgUmVzb2x1dGlvbiB9IGZyb20gJy4uLy4uL0VudW1zJztcbmltcG9ydCB7XG4gIERvbWFpblRhZ3NJdGVtLFxuICBEb21haW5UYWdzSXRlbUluZm8sXG4gIERvbWFpblRhZ1N0YXRpc3RpY0l0ZW0sXG4gIERvbWFpblRhZ1N0YXRBUElSZXNwb25zZSxcbiAgRG9tYWluVGFnQVBJUmVzcG9uc2VTdGF0c0l0ZW0sXG4gIERvbWFpblRhZ3NMaXN0LFxuICBEb21haW5UYWdzUmVzcG9uc2VEYXRhLFxuICBEb21haW5UYWdzUXVlcnksXG4gIERvbWFpblRhZ3NNZXNzYWdlUmVzLFxuICBEb21haW5UYWdzU3RhdGlzdGljUXVlcnksXG4gIERvbWFpblRhZ0NvdW50cmllc0FnZ3JlZ2F0aW9uLFxuICBEb21haW5UYWdDb3VudHJpZXNBUElSZXNwb25zZSxcbiAgRG9tYWluVGFnUHJvdmlkZXJzQWdncmVnYXRpb24sXG4gIERvbWFpblRhZ1Byb3ZpZGVyc0FQSVJlc3BvbnNlLFxuICBEb21haW5UYWdEZXZpY2VzQWdncmVnYXRpb24sXG4gIERvbWFpblRhZ0RldmljZXNBUElSZXNwb25zZVxufSBmcm9tICcuLi8uLi9UeXBlcy9Eb21haW5zJztcblxuZXhwb3J0IGNsYXNzIERvbWFpblRhZyBpbXBsZW1lbnRzIERvbWFpblRhZ3NJdGVtIHtcbiAgdGFnOiBzdHJpbmc7XG4gIGRlc2NyaXB0aW9uOiBzdHJpbmc7XG4gICdmaXJzdC1zZWVuJzogRGF0ZTtcbiAgJ2xhc3Qtc2Vlbic6IERhdGU7XG5cbiAgY29uc3RydWN0b3IodGFnSW5mbzogRG9tYWluVGFnc0l0ZW1JbmZvKSB7XG4gICAgdGhpcy50YWcgPSB0YWdJbmZvLnRhZztcbiAgICB0aGlzLmRlc2NyaXB0aW9uID0gdGFnSW5mby5kZXNjcmlwdGlvbjtcbiAgICB0aGlzWydmaXJzdC1zZWVuJ10gPSBuZXcgRGF0ZSh0YWdJbmZvWydmaXJzdC1zZWVuJ10pO1xuICAgIHRoaXNbJ2xhc3Qtc2VlbiddID0gbmV3IERhdGUodGFnSW5mb1snbGFzdC1zZWVuJ10pO1xuICB9XG59XG5cbmV4cG9ydCBjbGFzcyBEb21haW5UYWdTdGF0aXN0aWMgaW1wbGVtZW50cyBJRG9tYWluVGFnU3RhdGlzdGljUmVzdWx0IHtcbiAgdGFnOiBzdHJpbmc7XG4gIGRlc2NyaXB0aW9uOiBzdHJpbmc7XG4gIHN0YXJ0OiBEYXRlO1xuICBlbmQ6IERhdGU7XG4gIHJlc29sdXRpb246IFJlc29sdXRpb247XG4gIHN0YXRzOiBEb21haW5UYWdTdGF0aXN0aWNJdGVtW107XG5cbiAgY29uc3RydWN0b3IodGFnU3RhdGlzdGljSW5mbzogRG9tYWluVGFnU3RhdEFQSVJlc3BvbnNlKSB7XG4gICAgdGhpcy50YWcgPSB0YWdTdGF0aXN0aWNJbmZvLmJvZHkudGFnO1xuICAgIHRoaXMuZGVzY3JpcHRpb24gPSB0YWdTdGF0aXN0aWNJbmZvLmJvZHkuZGVzY3JpcHRpb247XG4gICAgdGhpcy5zdGFydCA9IG5ldyBEYXRlKHRhZ1N0YXRpc3RpY0luZm8uYm9keS5zdGFydCk7XG4gICAgdGhpcy5lbmQgPSBuZXcgRGF0ZSh0YWdTdGF0aXN0aWNJbmZvLmJvZHkuZW5kKTtcbiAgICB0aGlzLnJlc29sdXRpb24gPSB0YWdTdGF0aXN0aWNJbmZvLmJvZHkucmVzb2x1dGlvbjtcbiAgICB0aGlzLnN0YXRzID0gdGFnU3RhdGlzdGljSW5mby5ib2R5LnN0YXRzLm1hcChmdW5jdGlvbiAoc3RhdDogRG9tYWluVGFnQVBJUmVzcG9uc2VTdGF0c0l0ZW0pIHtcbiAgICAgIGNvbnN0IHJlcyA9IHsgLi4uc3RhdCwgdGltZTogbmV3IERhdGUoc3RhdC50aW1lKSB9O1xuICAgICAgcmV0dXJuIHJlcztcbiAgICB9KTtcbiAgfVxufVxuXG5leHBvcnQgZGVmYXVsdCBjbGFzcyBEb21haW5UYWdzQ2xpZW50XG4gIGV4dGVuZHMgTmF2aWdhdGlvblRocnVQYWdlczxEb21haW5UYWdzTGlzdD5cbiAgaW1wbGVtZW50cyBJRG9tYWluVGFnc0NsaWVudCB7XG4gIGJhc2VSb3V0ZTogc3RyaW5nO1xuICByZXF1ZXN0OiBSZXF1ZXN0O1xuXG4gIGNvbnN0cnVjdG9yKHJlcXVlc3Q6IFJlcXVlc3QpIHtcbiAgICBzdXBlcihyZXF1ZXN0KTtcbiAgICB0aGlzLnJlcXVlc3QgPSByZXF1ZXN0O1xuICAgIHRoaXMuYmFzZVJvdXRlID0gJy92My8nO1xuICB9XG5cbiAgcHJvdGVjdGVkIHBhcnNlTGlzdChcbiAgICByZXNwb25zZTogRG9tYWluVGFnc1Jlc3BvbnNlRGF0YSxcbiAgKTogRG9tYWluVGFnc0xpc3Qge1xuICAgIGNvbnN0IGRhdGEgPSB7fSBhcyBEb21haW5UYWdzTGlzdDtcbiAgICBkYXRhLml0ZW1zID0gcmVzcG9uc2UuYm9keS5pdGVtcy5tYXAoKHRhZ0luZm86IERvbWFpblRhZ3NJdGVtSW5mbykgPT4gbmV3IERvbWFpblRhZyh0YWdJbmZvKSk7XG5cbiAgICBkYXRhLnBhZ2VzID0gdGhpcy5wYXJzZVBhZ2VMaW5rcyhyZXNwb25zZSwgJz8nLCAndGFnJyk7XG4gICAgZGF0YS5zdGF0dXMgPSByZXNwb25zZS5zdGF0dXM7XG4gICAgcmV0dXJuIGRhdGE7XG4gIH1cblxuICBwcml2YXRlIF9wYXJzZVRhZ1N0YXRpc3RpYyhcbiAgICByZXNwb25zZTogRG9tYWluVGFnU3RhdEFQSVJlc3BvbnNlXG4gICk6IElEb21haW5UYWdTdGF0aXN0aWNSZXN1bHQge1xuICAgIHJldHVybiBuZXcgRG9tYWluVGFnU3RhdGlzdGljKHJlc3BvbnNlKTtcbiAgfVxuXG4gIGFzeW5jIGxpc3QoZG9tYWluOiBzdHJpbmcsIHF1ZXJ5PzogRG9tYWluVGFnc1F1ZXJ5KTogUHJvbWlzZTxEb21haW5UYWdzTGlzdD4ge1xuICAgIHJldHVybiB0aGlzLnJlcXVlc3RMaXN0V2l0aFBhZ2VzKHVybGpvaW4odGhpcy5iYXNlUm91dGUsIGRvbWFpbiwgJy90YWdzJyksIHF1ZXJ5KTtcbiAgfVxuXG4gIGdldChkb21haW46IHN0cmluZywgdGFnOiBzdHJpbmcpOiBQcm9taXNlPERvbWFpblRhZ3NJdGVtPiB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5nZXQodXJsam9pbih0aGlzLmJhc2VSb3V0ZSwgZG9tYWluLCAnL3RhZ3MnLCB0YWcpKVxuICAgICAgLnRoZW4oXG4gICAgICAgIChyZXM6IEFQSVJlc3BvbnNlKSA9PiBuZXcgRG9tYWluVGFnKHJlcy5ib2R5KVxuICAgICAgKTtcbiAgfVxuXG4gIHVwZGF0ZShkb21haW46IHN0cmluZywgdGFnOiBzdHJpbmcsIGRlc2NyaXB0aW9uOiBzdHJpbmcpOiBQcm9taXNlPERvbWFpblRhZ3NNZXNzYWdlUmVzPiB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5wdXQodXJsam9pbih0aGlzLmJhc2VSb3V0ZSwgZG9tYWluLCAnL3RhZ3MnLCB0YWcpLCBkZXNjcmlwdGlvbilcbiAgICAgIC50aGVuKFxuICAgICAgICAocmVzOiBBUElSZXNwb25zZSkgPT4gcmVzLmJvZHkgYXMgRG9tYWluVGFnc01lc3NhZ2VSZXNcbiAgICAgICk7XG4gIH1cblxuICBkZXN0cm95KFxuICAgIGRvbWFpbjogc3RyaW5nLFxuICAgIHRhZzogc3RyaW5nXG4gICk6IFByb21pc2U8RG9tYWluVGFnc01lc3NhZ2VSZXM+IHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LmRlbGV0ZShgJHt0aGlzLmJhc2VSb3V0ZX0ke2RvbWFpbn0vdGFncy8ke3RhZ31gKVxuICAgICAgLnRoZW4oKHJlczogQVBJUmVzcG9uc2UpID0+IChcbiAgICAgICAge1xuICAgICAgICAgIG1lc3NhZ2U6IHJlcy5ib2R5Lm1lc3NhZ2UsXG4gICAgICAgICAgc3RhdHVzOiByZXMuc3RhdHVzXG4gICAgICAgIH0gYXMgRG9tYWluVGFnc01lc3NhZ2VSZXMpKTtcbiAgfVxuXG4gIHN0YXRpc3RpYyhkb21haW46IHN0cmluZywgdGFnOiBzdHJpbmcsIHF1ZXJ5OiBEb21haW5UYWdzU3RhdGlzdGljUXVlcnkpXG4gICAgOiBQcm9taXNlPERvbWFpblRhZ1N0YXRpc3RpYz4ge1xuICAgIHJldHVybiB0aGlzLnJlcXVlc3QuZ2V0KHVybGpvaW4odGhpcy5iYXNlUm91dGUsIGRvbWFpbiwgJy90YWdzJywgdGFnLCAnc3RhdHMnKSwgcXVlcnkpXG4gICAgICAudGhlbihcbiAgICAgICAgKHJlczogQVBJUmVzcG9uc2UpID0+IHRoaXMuX3BhcnNlVGFnU3RhdGlzdGljKHJlcylcbiAgICAgICk7XG4gIH1cblxuICBjb3VudHJpZXMoZG9tYWluOiBzdHJpbmcsIHRhZzogc3RyaW5nKTogUHJvbWlzZTxEb21haW5UYWdDb3VudHJpZXNBZ2dyZWdhdGlvbj4ge1xuICAgIHJldHVybiB0aGlzLnJlcXVlc3QuZ2V0KHVybGpvaW4odGhpcy5iYXNlUm91dGUsIGRvbWFpbiwgJy90YWdzJywgdGFnLCAnc3RhdHMvYWdncmVnYXRlcy9jb3VudHJpZXMnKSlcbiAgICAgIC50aGVuKFxuICAgICAgICAocmVzOiBEb21haW5UYWdDb3VudHJpZXNBUElSZXNwb25zZSkgPT4gcmVzLmJvZHkgYXMgRG9tYWluVGFnQ291bnRyaWVzQWdncmVnYXRpb25cbiAgICAgICk7XG4gIH1cblxuICBwcm92aWRlcnMoZG9tYWluOiBzdHJpbmcsIHRhZzogc3RyaW5nKTogUHJvbWlzZTxEb21haW5UYWdQcm92aWRlcnNBZ2dyZWdhdGlvbj4ge1xuICAgIHJldHVybiB0aGlzLnJlcXVlc3QuZ2V0KHVybGpvaW4odGhpcy5iYXNlUm91dGUsIGRvbWFpbiwgJy90YWdzJywgdGFnLCAnc3RhdHMvYWdncmVnYXRlcy9wcm92aWRlcnMnKSlcbiAgICAgIC50aGVuKFxuICAgICAgICAocmVzOiBEb21haW5UYWdQcm92aWRlcnNBUElSZXNwb25zZSkgPT4gcmVzLmJvZHkgYXMgRG9tYWluVGFnUHJvdmlkZXJzQWdncmVnYXRpb25cbiAgICAgICk7XG4gIH1cblxuICBkZXZpY2VzKGRvbWFpbjogc3RyaW5nLCB0YWc6IHN0cmluZyk6IFByb21pc2U8RG9tYWluVGFnRGV2aWNlc0FnZ3JlZ2F0aW9uPiB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5nZXQodXJsam9pbih0aGlzLmJhc2VSb3V0ZSwgZG9tYWluLCAnL3RhZ3MnLCB0YWcsICdzdGF0cy9hZ2dyZWdhdGVzL2RldmljZXMnKSlcbiAgICAgIC50aGVuKFxuICAgICAgICAocmVzOiBEb21haW5UYWdEZXZpY2VzQVBJUmVzcG9uc2UpID0+IHJlcy5ib2R5IGFzIERvbWFpblRhZ0RldmljZXNBZ2dyZWdhdGlvblxuICAgICAgKTtcbiAgfVxufVxuIiwiaW1wb3J0IHVybGpvaW4gZnJvbSAndXJsLWpvaW4nO1xuaW1wb3J0IFJlcXVlc3QgZnJvbSAnLi4vY29tbW9uL1JlcXVlc3QnO1xuXG5pbXBvcnQge1xuICBDcmVhdGVEb21haW5UZW1wbGF0ZUFQSVJlc3BvbnNlLFxuICBDcmVhdGVEb21haW5UZW1wbGF0ZVZlcnNpb25BUElSZXNwb25zZSxcbiAgQ3JlYXRlRG9tYWluVGVtcGxhdGVWZXJzaW9uUmVzdWx0LFxuICBEb21haW5UZW1wbGF0ZURhdGEsXG4gIERvbWFpblRlbXBsYXRlc1F1ZXJ5LFxuICBEb21haW5UZW1wbGF0ZVVwZGF0ZURhdGEsXG4gIERvbWFpblRlbXBsYXRlVXBkYXRlVmVyc2lvbkRhdGEsXG4gIERvbWFpblRlbXBsYXRlVmVyc2lvbkRhdGEsXG4gIEdldERvbWFpblRlbXBsYXRlQVBJUmVzcG9uc2UsXG4gIExpc3REb21haW5UZW1wbGF0ZXNBUElSZXNwb25zZSxcbiAgTGlzdERvbWFpblRlbXBsYXRlc1Jlc3VsdCxcbiAgTGlzdERvbWFpblRlbXBsYXRlVmVyc2lvbnNBUElSZXNwb25zZSxcbiAgTGlzdERvbWFpblRlbXBsYXRlVmVyc2lvbnNSZXN1bHQsXG4gIE11dGF0ZURvbWFpblRlbXBsYXRlVmVyc2lvbkFQSVJlc3BvbnNlLFxuICBNdXRhdGVEb21haW5UZW1wbGF0ZVZlcnNpb25SZXN1bHQsXG4gIE5vdGlmaWNhdGlvbkFQSVJlc3BvbnNlLFxuICBOb3RpZmljYXRpb25SZXN1bHQsXG4gIFNob3J0VGVtcGxhdGVWZXJzaW9uLFxuICBUZW1wbGF0ZVF1ZXJ5LFxuICBUZW1wbGF0ZVZlcnNpb24sXG4gIFVwZGF0ZU9yRGVsZXRlRG9tYWluVGVtcGxhdGVBUElSZXNwb25zZSxcbiAgVXBkYXRlT3JEZWxldGVEb21haW5UZW1wbGF0ZVJlc3VsdFxufSBmcm9tICcuLi8uLi9UeXBlcy9Eb21haW5zJztcbmltcG9ydCBOYXZpZ2F0aW9uVGhydVBhZ2VzIGZyb20gJy4uL2NvbW1vbi9OYXZpZ2F0aW9uVGhydVBhZ2VzJztcbmltcG9ydCB7IElEb21haW5UZW1wbGF0ZSwgSURvbWFpblRlbXBsYXRlc0NsaWVudCB9IGZyb20gJy4uLy4uL0ludGVyZmFjZXMvRG9tYWlucyc7XG5cbmV4cG9ydCBjbGFzcyBEb21haW5UZW1wbGF0ZUl0ZW0gaW1wbGVtZW50cyBJRG9tYWluVGVtcGxhdGUge1xuICBuYW1lIDogc3RyaW5nO1xuICBkZXNjcmlwdGlvbiA6IHN0cmluZztcbiAgY3JlYXRlZEF0IDogRGF0ZSB8ICcnO1xuICBjcmVhdGVkQnkgOiBzdHJpbmc7XG4gIGlkIDogc3RyaW5nO1xuICB2ZXJzaW9uPzogVGVtcGxhdGVWZXJzaW9uO1xuICB2ZXJzaW9ucz86IFNob3J0VGVtcGxhdGVWZXJzaW9uW107XG5cbiAgY29uc3RydWN0b3IoZG9tYWluVGVtcGxhdGVGcm9tQVBJOiBJRG9tYWluVGVtcGxhdGUpIHtcbiAgICB0aGlzLm5hbWUgPSBkb21haW5UZW1wbGF0ZUZyb21BUEkubmFtZTtcbiAgICB0aGlzLmRlc2NyaXB0aW9uID0gZG9tYWluVGVtcGxhdGVGcm9tQVBJLmRlc2NyaXB0aW9uO1xuICAgIHRoaXMuY3JlYXRlZEF0ID0gZG9tYWluVGVtcGxhdGVGcm9tQVBJLmNyZWF0ZWRBdCA/IG5ldyBEYXRlKGRvbWFpblRlbXBsYXRlRnJvbUFQSS5jcmVhdGVkQXQpIDogJyc7XG4gICAgdGhpcy5jcmVhdGVkQnkgPSBkb21haW5UZW1wbGF0ZUZyb21BUEkuY3JlYXRlZEJ5O1xuICAgIHRoaXMuaWQgPSBkb21haW5UZW1wbGF0ZUZyb21BUEkuaWQ7XG5cbiAgICBpZiAoZG9tYWluVGVtcGxhdGVGcm9tQVBJLnZlcnNpb24pIHtcbiAgICAgIHRoaXMudmVyc2lvbiA9IGRvbWFpblRlbXBsYXRlRnJvbUFQSS52ZXJzaW9uO1xuICAgICAgaWYgKGRvbWFpblRlbXBsYXRlRnJvbUFQSS52ZXJzaW9uLmNyZWF0ZWRBdCkge1xuICAgICAgICB0aGlzLnZlcnNpb24uY3JlYXRlZEF0ID0gbmV3IERhdGUoZG9tYWluVGVtcGxhdGVGcm9tQVBJLnZlcnNpb24uY3JlYXRlZEF0KTtcbiAgICAgIH1cbiAgICB9XG5cbiAgICBpZiAoZG9tYWluVGVtcGxhdGVGcm9tQVBJLnZlcnNpb25zICYmIGRvbWFpblRlbXBsYXRlRnJvbUFQSS52ZXJzaW9ucy5sZW5ndGgpIHtcbiAgICAgIHRoaXMudmVyc2lvbnMgPSBkb21haW5UZW1wbGF0ZUZyb21BUEkudmVyc2lvbnMubWFwKCh2ZXJzaW9uKSA9PiB7XG4gICAgICAgIGNvbnN0IHJlc3VsdCA9IHsgLi4udmVyc2lvbiB9O1xuICAgICAgICByZXN1bHQuY3JlYXRlZEF0ID0gbmV3IERhdGUodmVyc2lvbi5jcmVhdGVkQXQpO1xuICAgICAgICByZXR1cm4gcmVzdWx0O1xuICAgICAgfSk7XG4gICAgfVxuICB9XG59XG5cbmV4cG9ydCBkZWZhdWx0IGNsYXNzIERvbWFpblRlbXBsYXRlc0NsaWVudFxuICBleHRlbmRzIE5hdmlnYXRpb25UaHJ1UGFnZXM8TGlzdERvbWFpblRlbXBsYXRlc1Jlc3VsdD5cbiAgaW1wbGVtZW50cyBJRG9tYWluVGVtcGxhdGVzQ2xpZW50IHtcbiAgYmFzZVJvdXRlOiBzdHJpbmc7XG4gIHJlcXVlc3Q6IFJlcXVlc3Q7XG5cbiAgY29uc3RydWN0b3IocmVxdWVzdDogUmVxdWVzdCkge1xuICAgIHN1cGVyKHJlcXVlc3QpO1xuICAgIHRoaXMucmVxdWVzdCA9IHJlcXVlc3Q7XG4gICAgdGhpcy5iYXNlUm91dGUgPSAnL3YzLyc7XG4gIH1cblxuICBwcml2YXRlIHBhcnNlQ3JlYXRpb25SZXNwb25zZShkYXRhOiBDcmVhdGVEb21haW5UZW1wbGF0ZUFQSVJlc3BvbnNlKTogSURvbWFpblRlbXBsYXRlIHtcbiAgICByZXR1cm4gbmV3IERvbWFpblRlbXBsYXRlSXRlbShkYXRhLmJvZHkudGVtcGxhdGUpO1xuICB9XG5cbiAgcHJpdmF0ZSBwYXJzZUNyZWF0aW9uVmVyc2lvblJlc3BvbnNlKFxuICAgIGRhdGE6IENyZWF0ZURvbWFpblRlbXBsYXRlVmVyc2lvbkFQSVJlc3BvbnNlXG4gICk6IENyZWF0ZURvbWFpblRlbXBsYXRlVmVyc2lvblJlc3VsdCB7XG4gICAgY29uc3QgcmVzdWx0OiBDcmVhdGVEb21haW5UZW1wbGF0ZVZlcnNpb25SZXN1bHQgPSB7fSBhcyBDcmVhdGVEb21haW5UZW1wbGF0ZVZlcnNpb25SZXN1bHQ7XG4gICAgcmVzdWx0LnN0YXR1cyA9IGRhdGEuc3RhdHVzO1xuICAgIHJlc3VsdC5tZXNzYWdlID0gZGF0YS5ib2R5Lm1lc3NhZ2U7XG4gICAgaWYgKGRhdGEuYm9keSAmJiBkYXRhLmJvZHkudGVtcGxhdGUpIHtcbiAgICAgIHJlc3VsdC50ZW1wbGF0ZSA9IG5ldyBEb21haW5UZW1wbGF0ZUl0ZW0oZGF0YS5ib2R5LnRlbXBsYXRlKTtcbiAgICB9XG4gICAgcmV0dXJuIHJlc3VsdDtcbiAgfVxuXG4gIHByaXZhdGUgcGFyc2VNdXRhdGlvblJlc3BvbnNlKFxuICAgIGRhdGE6IFVwZGF0ZU9yRGVsZXRlRG9tYWluVGVtcGxhdGVBUElSZXNwb25zZVxuICApOiBVcGRhdGVPckRlbGV0ZURvbWFpblRlbXBsYXRlUmVzdWx0IHtcbiAgICBjb25zdCByZXN1bHQ6IFVwZGF0ZU9yRGVsZXRlRG9tYWluVGVtcGxhdGVSZXN1bHQgPSB7fSBhcyBVcGRhdGVPckRlbGV0ZURvbWFpblRlbXBsYXRlUmVzdWx0O1xuICAgIHJlc3VsdC5zdGF0dXMgPSBkYXRhLnN0YXR1cztcbiAgICByZXN1bHQubWVzc2FnZSA9IGRhdGEuYm9keS5tZXNzYWdlO1xuICAgIGlmIChkYXRhLmJvZHkgJiYgZGF0YS5ib2R5LnRlbXBsYXRlKSB7XG4gICAgICByZXN1bHQudGVtcGxhdGVOYW1lID0gZGF0YS5ib2R5LnRlbXBsYXRlLm5hbWU7XG4gICAgfVxuICAgIHJldHVybiByZXN1bHQ7XG4gIH1cblxuICBwcml2YXRlIHBhcnNlTm90aWZpY2F0aW9uUmVzcG9uc2UoZGF0YTogTm90aWZpY2F0aW9uQVBJUmVzcG9uc2UpOiBOb3RpZmljYXRpb25SZXN1bHQge1xuICAgIGNvbnN0IHJlc3VsdDogTm90aWZpY2F0aW9uUmVzdWx0ID0ge30gYXMgTm90aWZpY2F0aW9uUmVzdWx0O1xuICAgIHJlc3VsdC5zdGF0dXMgPSBkYXRhLnN0YXR1cztcbiAgICByZXN1bHQubWVzc2FnZSA9IGRhdGEuYm9keS5tZXNzYWdlO1xuICAgIHJldHVybiByZXN1bHQ7XG4gIH1cblxuICBwcml2YXRlIHBhcnNlTXV0YXRlVGVtcGxhdGVWZXJzaW9uUmVzcG9uc2UoXG4gICAgZGF0YTogTXV0YXRlRG9tYWluVGVtcGxhdGVWZXJzaW9uQVBJUmVzcG9uc2VcbiAgKTogTXV0YXRlRG9tYWluVGVtcGxhdGVWZXJzaW9uUmVzdWx0IHtcbiAgICBjb25zdCByZXN1bHQ6IE11dGF0ZURvbWFpblRlbXBsYXRlVmVyc2lvblJlc3VsdCA9IHt9IGFzIE11dGF0ZURvbWFpblRlbXBsYXRlVmVyc2lvblJlc3VsdDtcbiAgICByZXN1bHQuc3RhdHVzID0gZGF0YS5zdGF0dXM7XG4gICAgcmVzdWx0Lm1lc3NhZ2UgPSBkYXRhLmJvZHkubWVzc2FnZTtcbiAgICBpZiAoZGF0YS5ib2R5LnRlbXBsYXRlKSB7XG4gICAgICByZXN1bHQudGVtcGxhdGVOYW1lID0gZGF0YS5ib2R5LnRlbXBsYXRlLm5hbWU7XG4gICAgICByZXN1bHQudGVtcGxhdGVWZXJzaW9uID0geyB0YWc6IGRhdGEuYm9keS50ZW1wbGF0ZS52ZXJzaW9uLnRhZyB9O1xuICAgIH1cbiAgICByZXR1cm4gcmVzdWx0O1xuICB9XG5cbiAgcHJvdGVjdGVkIHBhcnNlTGlzdChyZXNwb25zZTogTGlzdERvbWFpblRlbXBsYXRlc0FQSVJlc3BvbnNlKTogTGlzdERvbWFpblRlbXBsYXRlc1Jlc3VsdCB7XG4gICAgY29uc3QgZGF0YSA9IHt9IGFzIExpc3REb21haW5UZW1wbGF0ZXNSZXN1bHQ7XG5cbiAgICBkYXRhLml0ZW1zID0gcmVzcG9uc2UuYm9keS5pdGVtcy5tYXAoKGQ6IElEb21haW5UZW1wbGF0ZSkgPT4gbmV3IERvbWFpblRlbXBsYXRlSXRlbShkKSk7XG5cbiAgICBkYXRhLnBhZ2VzID0gdGhpcy5wYXJzZVBhZ2VMaW5rcyhyZXNwb25zZSwgJz8nLCAncCcpO1xuICAgIGRhdGEuc3RhdHVzID0gcmVzcG9uc2Uuc3RhdHVzO1xuXG4gICAgcmV0dXJuIGRhdGE7XG4gIH1cblxuICBwcml2YXRlIHBhcnNlTGlzdFRlbXBsYXRlVmVyc2lvbnMoXG4gICAgcmVzcG9uc2U6IExpc3REb21haW5UZW1wbGF0ZVZlcnNpb25zQVBJUmVzcG9uc2VcbiAgKTogTGlzdERvbWFpblRlbXBsYXRlVmVyc2lvbnNSZXN1bHQge1xuICAgIGNvbnN0IGRhdGEgPSB7fSBhcyBMaXN0RG9tYWluVGVtcGxhdGVWZXJzaW9uc1Jlc3VsdDtcblxuICAgIGRhdGEudGVtcGxhdGUgPSBuZXcgRG9tYWluVGVtcGxhdGVJdGVtKHJlc3BvbnNlLmJvZHkudGVtcGxhdGUpO1xuXG4gICAgZGF0YS5wYWdlcyA9IHRoaXMucGFyc2VQYWdlTGlua3MocmVzcG9uc2UsICc/JywgJ3AnKTtcblxuICAgIHJldHVybiBkYXRhO1xuICB9XG5cbiAgYXN5bmMgbGlzdChkb21haW46IHN0cmluZywgcXVlcnk/OiBEb21haW5UZW1wbGF0ZXNRdWVyeSk6IFByb21pc2U8TGlzdERvbWFpblRlbXBsYXRlc1Jlc3VsdD4ge1xuICAgIHJldHVybiB0aGlzLnJlcXVlc3RMaXN0V2l0aFBhZ2VzKHVybGpvaW4odGhpcy5iYXNlUm91dGUsIGRvbWFpbiwgJy90ZW1wbGF0ZXMnKSwgcXVlcnkpO1xuICB9XG5cbiAgZ2V0KGRvbWFpbjogc3RyaW5nLCB0ZW1wbGF0ZU5hbWU6IHN0cmluZywgcXVlcnk/OiBUZW1wbGF0ZVF1ZXJ5KTogUHJvbWlzZTxJRG9tYWluVGVtcGxhdGU+IHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LmdldCh1cmxqb2luKHRoaXMuYmFzZVJvdXRlLCBkb21haW4sICcvdGVtcGxhdGVzLycsIHRlbXBsYXRlTmFtZSksIHF1ZXJ5KVxuICAgICAgLnRoZW4oXG4gICAgICAgIChyZXM6IEdldERvbWFpblRlbXBsYXRlQVBJUmVzcG9uc2UpID0+IG5ldyBEb21haW5UZW1wbGF0ZUl0ZW0ocmVzLmJvZHkudGVtcGxhdGUpXG4gICAgICApO1xuICB9XG5cbiAgY3JlYXRlKFxuICAgIGRvbWFpbjogc3RyaW5nLFxuICAgIGRhdGE6IERvbWFpblRlbXBsYXRlRGF0YVxuICApOiBQcm9taXNlPElEb21haW5UZW1wbGF0ZT4ge1xuICAgIHJldHVybiB0aGlzLnJlcXVlc3QucG9zdFdpdGhGRCh1cmxqb2luKHRoaXMuYmFzZVJvdXRlLCBkb21haW4sICcvdGVtcGxhdGVzJyksIGRhdGEpXG4gICAgICAudGhlbigocmVzOiBDcmVhdGVEb21haW5UZW1wbGF0ZUFQSVJlc3BvbnNlKSA9PiB0aGlzLnBhcnNlQ3JlYXRpb25SZXNwb25zZShyZXMpKTtcbiAgfVxuXG4gIHVwZGF0ZShcbiAgICBkb21haW46IHN0cmluZyxcbiAgICB0ZW1wbGF0ZU5hbWU6IHN0cmluZyxcbiAgICBkYXRhOiBEb21haW5UZW1wbGF0ZVVwZGF0ZURhdGFcbiAgKTogUHJvbWlzZTxVcGRhdGVPckRlbGV0ZURvbWFpblRlbXBsYXRlUmVzdWx0PiB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5wdXRXaXRoRkQodXJsam9pbih0aGlzLmJhc2VSb3V0ZSwgZG9tYWluLCAnL3RlbXBsYXRlcy8nLCB0ZW1wbGF0ZU5hbWUpLCBkYXRhKVxuICAgICAgLnRoZW4oKHJlczogVXBkYXRlT3JEZWxldGVEb21haW5UZW1wbGF0ZUFQSVJlc3BvbnNlKSA9PiB0aGlzLnBhcnNlTXV0YXRpb25SZXNwb25zZShyZXMpKTtcbiAgfVxuXG4gIGRlc3Ryb3koZG9tYWluOiBzdHJpbmcsIHRlbXBsYXRlTmFtZTogc3RyaW5nKTogUHJvbWlzZTxVcGRhdGVPckRlbGV0ZURvbWFpblRlbXBsYXRlUmVzdWx0PiB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5kZWxldGUodXJsam9pbih0aGlzLmJhc2VSb3V0ZSwgZG9tYWluLCAnL3RlbXBsYXRlcy8nLCB0ZW1wbGF0ZU5hbWUpKVxuICAgICAgLnRoZW4oKHJlczogVXBkYXRlT3JEZWxldGVEb21haW5UZW1wbGF0ZUFQSVJlc3BvbnNlKSA9PiB0aGlzLnBhcnNlTXV0YXRpb25SZXNwb25zZShyZXMpKTtcbiAgfVxuXG4gIGRlc3Ryb3lBbGwoZG9tYWluOiBzdHJpbmcpOiBQcm9taXNlPE5vdGlmaWNhdGlvblJlc3VsdD4ge1xuICAgIHJldHVybiB0aGlzLnJlcXVlc3QuZGVsZXRlKHVybGpvaW4odGhpcy5iYXNlUm91dGUsIGRvbWFpbiwgJy90ZW1wbGF0ZXMnKSlcbiAgICAgIC50aGVuKChyZXM6IE5vdGlmaWNhdGlvbkFQSVJlc3BvbnNlKSA9PiB0aGlzLnBhcnNlTm90aWZpY2F0aW9uUmVzcG9uc2UocmVzKSk7XG4gIH1cblxuICBjcmVhdGVWZXJzaW9uKFxuICAgIGRvbWFpbjogc3RyaW5nLFxuICAgIHRlbXBsYXRlTmFtZTogc3RyaW5nLFxuICAgIGRhdGE6IERvbWFpblRlbXBsYXRlVmVyc2lvbkRhdGFcbiAgKTogUHJvbWlzZTxDcmVhdGVEb21haW5UZW1wbGF0ZVZlcnNpb25SZXN1bHQ+IHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LnBvc3RXaXRoRkQodXJsam9pbih0aGlzLmJhc2VSb3V0ZSwgZG9tYWluLCAnL3RlbXBsYXRlcy8nLCB0ZW1wbGF0ZU5hbWUsICcvdmVyc2lvbnMnKSwgZGF0YSlcbiAgICAgIC50aGVuKFxuICAgICAgICAocmVzOiBDcmVhdGVEb21haW5UZW1wbGF0ZVZlcnNpb25BUElSZXNwb25zZSkgPT4gdGhpcy5wYXJzZUNyZWF0aW9uVmVyc2lvblJlc3BvbnNlKHJlcylcbiAgICAgICk7XG4gIH1cblxuICBnZXRWZXJzaW9uKGRvbWFpbjogc3RyaW5nLCB0ZW1wbGF0ZU5hbWU6IHN0cmluZywgdGFnOiBzdHJpbmcpOiBQcm9taXNlPElEb21haW5UZW1wbGF0ZT4ge1xuICAgIHJldHVybiB0aGlzLnJlcXVlc3QuZ2V0KHVybGpvaW4odGhpcy5iYXNlUm91dGUsIGRvbWFpbiwgJy90ZW1wbGF0ZXMvJywgdGVtcGxhdGVOYW1lLCAnL3ZlcnNpb25zLycsIHRhZykpXG4gICAgICAudGhlbihcbiAgICAgICAgKHJlczogR2V0RG9tYWluVGVtcGxhdGVBUElSZXNwb25zZSkgPT4gbmV3IERvbWFpblRlbXBsYXRlSXRlbShyZXMuYm9keS50ZW1wbGF0ZSlcbiAgICAgICk7XG4gIH1cblxuICB1cGRhdGVWZXJzaW9uKFxuICAgIGRvbWFpbjogc3RyaW5nLFxuICAgIHRlbXBsYXRlTmFtZTogc3RyaW5nLFxuICAgIHRhZzogc3RyaW5nLFxuICAgIGRhdGE6IERvbWFpblRlbXBsYXRlVXBkYXRlVmVyc2lvbkRhdGFcbiAgKTogUHJvbWlzZTxNdXRhdGVEb21haW5UZW1wbGF0ZVZlcnNpb25SZXN1bHQ+IHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LnB1dFdpdGhGRCh1cmxqb2luKHRoaXMuYmFzZVJvdXRlLCBkb21haW4sICcvdGVtcGxhdGVzLycsIHRlbXBsYXRlTmFtZSwgJy92ZXJzaW9ucy8nLCB0YWcpLCBkYXRhKVxuICAgICAgLnRoZW4oXG4gICAgICAgIC8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBtYXgtbGVuXG4gICAgICAgIChyZXM6IE11dGF0ZURvbWFpblRlbXBsYXRlVmVyc2lvbkFQSVJlc3BvbnNlKSA9PiB0aGlzLnBhcnNlTXV0YXRlVGVtcGxhdGVWZXJzaW9uUmVzcG9uc2UocmVzKVxuICAgICAgKTtcbiAgfVxuXG4gIGRlc3Ryb3lWZXJzaW9uKFxuICAgIGRvbWFpbjogc3RyaW5nLFxuICAgIHRlbXBsYXRlTmFtZTogc3RyaW5nLFxuICAgIHRhZzogc3RyaW5nXG4gICk6IFByb21pc2U8TXV0YXRlRG9tYWluVGVtcGxhdGVWZXJzaW9uUmVzdWx0PiB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5kZWxldGUodXJsam9pbih0aGlzLmJhc2VSb3V0ZSwgZG9tYWluLCAnL3RlbXBsYXRlcy8nLCB0ZW1wbGF0ZU5hbWUsICcvdmVyc2lvbnMvJywgdGFnKSlcbiAgICAgIC8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBtYXgtbGVuXG4gICAgICAudGhlbigocmVzOiBNdXRhdGVEb21haW5UZW1wbGF0ZVZlcnNpb25BUElSZXNwb25zZSkgPT4gdGhpcy5wYXJzZU11dGF0ZVRlbXBsYXRlVmVyc2lvblJlc3BvbnNlKHJlcykpO1xuICB9XG5cbiAgbGlzdFZlcnNpb25zKFxuICAgIGRvbWFpbjogc3RyaW5nLFxuICAgIHRlbXBsYXRlTmFtZTogc3RyaW5nLFxuICAgIHF1ZXJ5PzogRG9tYWluVGVtcGxhdGVzUXVlcnlcbiAgKTogUHJvbWlzZTxMaXN0RG9tYWluVGVtcGxhdGVWZXJzaW9uc1Jlc3VsdD4ge1xuICAgIHJldHVybiB0aGlzLnJlcXVlc3QuZ2V0KHVybGpvaW4odGhpcy5iYXNlUm91dGUsIGRvbWFpbiwgJy90ZW1wbGF0ZXMnLCB0ZW1wbGF0ZU5hbWUsICcvdmVyc2lvbnMnKSwgcXVlcnkpXG4gICAgICAudGhlbihcbiAgICAgICAgKHJlczogTGlzdERvbWFpblRlbXBsYXRlVmVyc2lvbnNBUElSZXNwb25zZSkgPT4gdGhpcy5wYXJzZUxpc3RUZW1wbGF0ZVZlcnNpb25zKHJlcylcbiAgICAgICk7XG4gIH1cbn1cbiIsImltcG9ydCB1cmxqb2luIGZyb20gJ3VybC1qb2luJztcbmltcG9ydCBOYXZpZ2F0aW9uVGhydVBhZ2VzIGZyb20gJy4vY29tbW9uL05hdmlnYXRpb25UaHJ1UGFnZXMnO1xuaW1wb3J0IHtcbiAgRXZlbnRzTGlzdCxcbiAgRXZlbnRzUXVlcnksXG4gIEV2ZW50c1Jlc3BvbnNlLFxufSBmcm9tICcuLi9UeXBlcy9FdmVudHMnO1xuXG5pbXBvcnQgUmVxdWVzdCBmcm9tICcuL2NvbW1vbi9SZXF1ZXN0JztcbmltcG9ydCB7IElFdmVudENsaWVudCB9IGZyb20gJy4uL0ludGVyZmFjZXMnO1xuXG5leHBvcnQgZGVmYXVsdCBjbGFzcyBFdmVudENsaWVudFxuICBleHRlbmRzIE5hdmlnYXRpb25UaHJ1UGFnZXM8RXZlbnRzTGlzdD5cbiAgaW1wbGVtZW50cyBJRXZlbnRDbGllbnQge1xuICByZXF1ZXN0OiBSZXF1ZXN0O1xuXG4gIGNvbnN0cnVjdG9yKHJlcXVlc3Q6IFJlcXVlc3QpIHtcbiAgICBzdXBlcihyZXF1ZXN0KTtcbiAgICB0aGlzLnJlcXVlc3QgPSByZXF1ZXN0O1xuICB9XG5cbiAgcHJvdGVjdGVkIHBhcnNlTGlzdChcbiAgICByZXNwb25zZTogRXZlbnRzUmVzcG9uc2UsXG4gICk6IEV2ZW50c0xpc3Qge1xuICAgIGNvbnN0IGRhdGEgPSB7fSBhcyBFdmVudHNMaXN0O1xuICAgIGRhdGEuaXRlbXMgPSByZXNwb25zZS5ib2R5Lml0ZW1zO1xuXG4gICAgZGF0YS5wYWdlcyA9IHRoaXMucGFyc2VQYWdlTGlua3MocmVzcG9uc2UsICcvJyk7XG4gICAgZGF0YS5zdGF0dXMgPSByZXNwb25zZS5zdGF0dXM7XG4gICAgcmV0dXJuIGRhdGE7XG4gIH1cblxuICBhc3luYyBnZXQoZG9tYWluOiBzdHJpbmcsIHF1ZXJ5PzogRXZlbnRzUXVlcnkpIDogUHJvbWlzZTxFdmVudHNMaXN0PiB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdExpc3RXaXRoUGFnZXModXJsam9pbignL3YzJywgZG9tYWluLCAnZXZlbnRzJyksIHF1ZXJ5KTtcbiAgfVxufVxuIiwiLyogZXNsaW50LWRpc2FibGUgY2FtZWxjYXNlICovXG5pbXBvcnQgUmVxdWVzdCBmcm9tICcuL2NvbW1vbi9SZXF1ZXN0JztcblxuaW1wb3J0IHtcbiAgSXBQb29sQ3JlYXRlRGF0YSxcbiAgSXBQb29sQ3JlYXRlUmVzcG9uc2UsXG4gIElwUG9vbENyZWF0ZVJlc3VsdCxcbiAgSXBQb29sRGVsZXRlRGF0YSxcbiAgSXBQb29sTGlzdFJlc3BvbnNlLFxuICBJcFBvb2xMaXN0UmVzdWx0LFxuICBJcFBvb2xNZXNzYWdlUmVzcG9uc2UsXG4gIElwUG9vbE1lc3NhZ2VSZXN1bHQsXG4gIElwUG9vbFVwZGF0ZURhdGEsXG59IGZyb20gJy4uL1R5cGVzL0lQUG9vbHMnO1xuaW1wb3J0IHsgSUlQUG9vbHNDbGllbnQgfSBmcm9tICcuLi9JbnRlcmZhY2VzJztcblxuZXhwb3J0IGRlZmF1bHQgY2xhc3MgSXBQb29sc0NsaWVudCBpbXBsZW1lbnRzIElJUFBvb2xzQ2xpZW50IHtcbiAgcmVxdWVzdDogUmVxdWVzdDtcblxuICBjb25zdHJ1Y3RvcihyZXF1ZXN0OiBSZXF1ZXN0KSB7XG4gICAgdGhpcy5yZXF1ZXN0ID0gcmVxdWVzdDtcbiAgfVxuXG4gIGxpc3QoKTogUHJvbWlzZTxJcFBvb2xMaXN0UmVzdWx0PiB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5nZXQoJy92MS9pcF9wb29scycpXG4gICAgICAudGhlbigocmVzcG9uc2U6IElwUG9vbExpc3RSZXNwb25zZSkgPT4gdGhpcy5wYXJzZUlwUG9vbHNSZXNwb25zZShyZXNwb25zZSkpO1xuICB9XG5cbiAgYXN5bmMgY3JlYXRlKGRhdGE6IElwUG9vbENyZWF0ZURhdGEpOiBQcm9taXNlPElwUG9vbENyZWF0ZVJlc3VsdD4ge1xuICAgIGNvbnN0IHJlc3BvbnNlOiBJcFBvb2xDcmVhdGVSZXNwb25zZSA9IGF3YWl0IHRoaXMucmVxdWVzdC5wb3N0V2l0aEZEKCcvdjEvaXBfcG9vbHMnLCBkYXRhKTtcbiAgICByZXR1cm4ge1xuICAgICAgc3RhdHVzOiByZXNwb25zZS5zdGF0dXMsXG4gICAgICAuLi5yZXNwb25zZS5ib2R5XG4gICAgfTtcbiAgfVxuXG4gIGFzeW5jIHVwZGF0ZShwb29sSWQ6IHN0cmluZywgZGF0YTogSXBQb29sVXBkYXRlRGF0YSk6IFByb21pc2U8SXBQb29sTWVzc2FnZVJlc3VsdD4ge1xuICAgIGNvbnN0IHJlc3BvbnNlOiBJcFBvb2xNZXNzYWdlUmVzcG9uc2UgPSBhd2FpdCB0aGlzLnJlcXVlc3QucGF0Y2hXaXRoRkQoYC92MS9pcF9wb29scy8ke3Bvb2xJZH1gLCBkYXRhKTtcbiAgICByZXR1cm4ge1xuICAgICAgc3RhdHVzOiByZXNwb25zZS5zdGF0dXMsXG4gICAgICAuLi5yZXNwb25zZS5ib2R5XG4gICAgfTtcbiAgfVxuXG4gIGFzeW5jIGRlbGV0ZShwb29sSWQ6IHN0cmluZywgZGF0YTogSXBQb29sRGVsZXRlRGF0YSk6IFByb21pc2U8SXBQb29sTWVzc2FnZVJlc3VsdD4ge1xuICAgIGNvbnN0IHJlc3BvbnNlOklwUG9vbE1lc3NhZ2VSZXNwb25zZSA9IGF3YWl0IHRoaXMucmVxdWVzdC5kZWxldGUoYC92MS9pcF9wb29scy8ke3Bvb2xJZH1gLCBkYXRhKTtcbiAgICByZXR1cm4ge1xuICAgICAgc3RhdHVzOiByZXNwb25zZS5zdGF0dXMsXG4gICAgICAuLi5yZXNwb25zZS5ib2R5XG4gICAgfTtcbiAgfVxuXG4gIHByaXZhdGUgcGFyc2VJcFBvb2xzUmVzcG9uc2UocmVzcG9uc2U6IElwUG9vbExpc3RSZXNwb25zZSk6IElwUG9vbExpc3RSZXN1bHQge1xuICAgIHJldHVybiB7XG4gICAgICBzdGF0dXM6IHJlc3BvbnNlLnN0YXR1cyxcbiAgICAgIC4uLnJlc3BvbnNlLmJvZHlcbiAgICB9O1xuICB9XG59XG4iLCJpbXBvcnQgTWdSZXF1ZXN0IGZyb20gJy4vY29tbW9uL1JlcXVlc3QnO1xuaW1wb3J0IHsgSXBEYXRhLCBJUHNMaXN0UXVlcnksIElwc0xpc3RSZXNwb25zZUJvZHkgfSBmcm9tICcuLi9UeXBlcy9JUHMnO1xuaW1wb3J0IHsgSUlQc0NsaWVudCB9IGZyb20gJy4uL0ludGVyZmFjZXMnO1xuXG5leHBvcnQgZGVmYXVsdCBjbGFzcyBJcHNDbGllbnQgaW1wbGVtZW50cyBJSVBzQ2xpZW50IHtcbiAgcmVxdWVzdDogTWdSZXF1ZXN0O1xuXG4gIGNvbnN0cnVjdG9yKHJlcXVlc3Q6IE1nUmVxdWVzdCkge1xuICAgIHRoaXMucmVxdWVzdCA9IHJlcXVlc3Q7XG4gIH1cblxuICBhc3luYyBsaXN0KHF1ZXJ5PzogSVBzTGlzdFF1ZXJ5KTogUHJvbWlzZTxJcHNMaXN0UmVzcG9uc2VCb2R5PiB7XG4gICAgY29uc3QgcmVzcG9uc2UgPSBhd2FpdCB0aGlzLnJlcXVlc3QuZ2V0KCcvdjMvaXBzJywgcXVlcnkpO1xuICAgIHJldHVybiB0aGlzLnBhcnNlSXBzUmVzcG9uc2U8SXBzTGlzdFJlc3BvbnNlQm9keT4ocmVzcG9uc2UpO1xuICB9XG5cbiAgYXN5bmMgZ2V0KGlwOiBzdHJpbmcpOiBQcm9taXNlPElwRGF0YT4ge1xuICAgIGNvbnN0IHJlc3BvbnNlID0gYXdhaXQgdGhpcy5yZXF1ZXN0LmdldChgL3YzL2lwcy8ke2lwfWApO1xuICAgIHJldHVybiB0aGlzLnBhcnNlSXBzUmVzcG9uc2U8SXBEYXRhPihyZXNwb25zZSk7XG4gIH1cblxuICBwcml2YXRlIHBhcnNlSXBzUmVzcG9uc2U8VD4ocmVzcG9uc2U6IHsgYm9keTogVCB9KTogVCB7XG4gICAgcmV0dXJuIHJlc3BvbnNlLmJvZHk7XG4gIH1cbn1cbiIsIi8qIGVzbGludC1kaXNhYmxlIGNhbWVsY2FzZSAqL1xuaW1wb3J0IFJlcXVlc3QgZnJvbSAnLi9jb21tb24vUmVxdWVzdCc7XG5pbXBvcnQgeyBNYWlsZ3VuQ2xpZW50T3B0aW9ucywgSW5wdXRGb3JtRGF0YSwgUmVxdWVzdE9wdGlvbnMgfSBmcm9tICcuLi9UeXBlcyc7XG5cbmltcG9ydCBEb21haW5zQ2xpZW50IGZyb20gJy4vRG9tYWlucy9kb21haW5zQ2xpZW50JztcbmltcG9ydCBFdmVudENsaWVudCBmcm9tICcuL0V2ZW50cyc7XG5pbXBvcnQgU3RhdHNDbGllbnQgZnJvbSAnLi9TdGF0cy9TdGF0c0NsaWVudCc7XG5pbXBvcnQgU3VwcHJlc3Npb25DbGllbnQgZnJvbSAnLi9TdXBwcmVzc2lvbnMvU3VwcHJlc3Npb25zQ2xpZW50JztcbmltcG9ydCBXZWJob29rc0NsaWVudCBmcm9tICcuL1dlYmhvb2tzJztcbmltcG9ydCBNZXNzYWdlc0NsaWVudCBmcm9tICcuL01lc3NhZ2VzJztcbmltcG9ydCBSb3V0ZXNDbGllbnQgZnJvbSAnLi9Sb3V0ZXMnO1xuaW1wb3J0IFZhbGlkYXRlQ2xpZW50IGZyb20gJy4vVmFsaWRhdGlvbnMvdmFsaWRhdGUnO1xuaW1wb3J0IElwc0NsaWVudCBmcm9tICcuL0lQcyc7XG5pbXBvcnQgSXBQb29sc0NsaWVudCBmcm9tICcuL0lQUG9vbHMnO1xuaW1wb3J0IE1haWxpbmdMaXN0c0NsaWVudCBmcm9tICcuL01haWxpbmdMaXN0cy9tYWlsaW5nTGlzdHMnO1xuaW1wb3J0IE1haWxMaXN0c01lbWJlcnMgZnJvbSAnLi9NYWlsaW5nTGlzdHMvbWFpbExpc3RNZW1iZXJzJztcbmltcG9ydCBEb21haW5DcmVkZW50aWFsc0NsaWVudCBmcm9tICcuL0RvbWFpbnMvZG9tYWluc0NyZWRlbnRpYWxzJztcbmltcG9ydCBNdWx0aXBsZVZhbGlkYXRpb25DbGllbnQgZnJvbSAnLi9WYWxpZGF0aW9ucy9tdWx0aXBsZVZhbGlkYXRpb24nO1xuaW1wb3J0IERvbWFpblRlbXBsYXRlc0NsaWVudCBmcm9tICcuL0RvbWFpbnMvZG9tYWluc1RlbXBsYXRlcyc7XG5pbXBvcnQgRG9tYWluVGFnc0NsaWVudCBmcm9tICcuL0RvbWFpbnMvZG9tYWluc1RhZ3MnO1xuaW1wb3J0IFN1YmFjY291bnRzQ2xpZW50IGZyb20gJy4vU3ViYWNjb3VudHMnO1xuXG5pbXBvcnQge1xuICBJRG9tYWluc0NsaWVudCxcbiAgSVdlYkhvb2tzQ2xpZW50LFxuICBJTWFpbGd1bkNsaWVudCxcbiAgSU1haWxpbmdMaXN0c0NsaWVudCxcbiAgSUV2ZW50Q2xpZW50LFxuICBJU3RhdHNDbGllbnQsXG4gIElTdXBwcmVzc2lvbkNsaWVudCxcbiAgSU1lc3NhZ2VzQ2xpZW50LFxuICBJUm91dGVzQ2xpZW50LFxuICBJVmFsaWRhdGlvbkNsaWVudCxcbiAgSUlQc0NsaWVudCxcbiAgSUlQUG9vbHNDbGllbnQsXG4gIElTdWJhY2NvdW50c0NsaWVudCxcbn0gZnJvbSAnLi4vSW50ZXJmYWNlcyc7XG5cbmV4cG9ydCBkZWZhdWx0IGNsYXNzIE1haWxndW5DbGllbnQgaW1wbGVtZW50cyBJTWFpbGd1bkNsaWVudCB7XG4gIHByaXZhdGUgcmVxdWVzdDtcblxuICBwdWJsaWMgZG9tYWluczogSURvbWFpbnNDbGllbnQ7XG4gIHB1YmxpYyB3ZWJob29rczogSVdlYkhvb2tzQ2xpZW50O1xuICBwdWJsaWMgZXZlbnRzOiBJRXZlbnRDbGllbnQ7XG4gIHB1YmxpYyBzdGF0czogSVN0YXRzQ2xpZW50O1xuICBwdWJsaWMgc3VwcHJlc3Npb25zOiBJU3VwcHJlc3Npb25DbGllbnQ7XG4gIHB1YmxpYyBtZXNzYWdlczogSU1lc3NhZ2VzQ2xpZW50O1xuICBwdWJsaWMgcm91dGVzOiBJUm91dGVzQ2xpZW50O1xuICBwdWJsaWMgdmFsaWRhdGU6IElWYWxpZGF0aW9uQ2xpZW50O1xuICBwdWJsaWMgaXBzOiBJSVBzQ2xpZW50O1xuICBwdWJsaWMgaXBfcG9vbHM6IElJUFBvb2xzQ2xpZW50O1xuICBwdWJsaWMgbGlzdHM6IElNYWlsaW5nTGlzdHNDbGllbnQ7XG4gIHB1YmxpYyBzdWJhY2NvdW50czogSVN1YmFjY291bnRzQ2xpZW50O1xuXG4gIGNvbnN0cnVjdG9yKG9wdGlvbnM6IE1haWxndW5DbGllbnRPcHRpb25zLCBmb3JtRGF0YTogSW5wdXRGb3JtRGF0YSkge1xuICAgIGNvbnN0IGNvbmZpZzogUmVxdWVzdE9wdGlvbnMgPSB7IC4uLm9wdGlvbnMgfSBhcyBSZXF1ZXN0T3B0aW9ucztcblxuICAgIGlmICghY29uZmlnLnVybCkge1xuICAgICAgY29uZmlnLnVybCA9ICdodHRwczovL2FwaS5tYWlsZ3VuLm5ldCc7XG4gICAgfVxuXG4gICAgaWYgKCFjb25maWcudXNlcm5hbWUpIHtcbiAgICAgIHRocm93IG5ldyBFcnJvcignUGFyYW1ldGVyIFwidXNlcm5hbWVcIiBpcyByZXF1aXJlZCcpO1xuICAgIH1cblxuICAgIGlmICghY29uZmlnLmtleSkge1xuICAgICAgdGhyb3cgbmV3IEVycm9yKCdQYXJhbWV0ZXIgXCJrZXlcIiBpcyByZXF1aXJlZCcpO1xuICAgIH1cblxuICAgIC8qKiBAaW50ZXJuYWwgKi9cbiAgICB0aGlzLnJlcXVlc3QgPSBuZXcgUmVxdWVzdChjb25maWcsIGZvcm1EYXRhKTtcbiAgICBjb25zdCBtYWlsTGlzdHNNZW1iZXJzID0gbmV3IE1haWxMaXN0c01lbWJlcnModGhpcy5yZXF1ZXN0KTtcbiAgICBjb25zdCBkb21haW5DcmVkZW50aWFsc0NsaWVudCA9IG5ldyBEb21haW5DcmVkZW50aWFsc0NsaWVudCh0aGlzLnJlcXVlc3QpO1xuICAgIGNvbnN0IGRvbWFpblRlbXBsYXRlc0NsaWVudCA9IG5ldyBEb21haW5UZW1wbGF0ZXNDbGllbnQodGhpcy5yZXF1ZXN0KTtcbiAgICBjb25zdCBkb21haW5UYWdzQ2xpZW50ID0gbmV3IERvbWFpblRhZ3NDbGllbnQodGhpcy5yZXF1ZXN0KTtcbiAgICBjb25zdCBtdWx0aXBsZVZhbGlkYXRpb25DbGllbnQgPSBuZXcgTXVsdGlwbGVWYWxpZGF0aW9uQ2xpZW50KHRoaXMucmVxdWVzdCk7XG5cbiAgICB0aGlzLmRvbWFpbnMgPSBuZXcgRG9tYWluc0NsaWVudChcbiAgICAgIHRoaXMucmVxdWVzdCxcbiAgICAgIGRvbWFpbkNyZWRlbnRpYWxzQ2xpZW50LFxuICAgICAgZG9tYWluVGVtcGxhdGVzQ2xpZW50LFxuICAgICAgZG9tYWluVGFnc0NsaWVudFxuICAgICk7XG4gICAgdGhpcy53ZWJob29rcyA9IG5ldyBXZWJob29rc0NsaWVudCh0aGlzLnJlcXVlc3QpO1xuICAgIHRoaXMuZXZlbnRzID0gbmV3IEV2ZW50Q2xpZW50KHRoaXMucmVxdWVzdCk7XG4gICAgdGhpcy5zdGF0cyA9IG5ldyBTdGF0c0NsaWVudCh0aGlzLnJlcXVlc3QpO1xuICAgIHRoaXMuc3VwcHJlc3Npb25zID0gbmV3IFN1cHByZXNzaW9uQ2xpZW50KHRoaXMucmVxdWVzdCk7XG4gICAgdGhpcy5tZXNzYWdlcyA9IG5ldyBNZXNzYWdlc0NsaWVudCh0aGlzLnJlcXVlc3QpO1xuICAgIHRoaXMucm91dGVzID0gbmV3IFJvdXRlc0NsaWVudCh0aGlzLnJlcXVlc3QpO1xuICAgIHRoaXMuaXBzID0gbmV3IElwc0NsaWVudCh0aGlzLnJlcXVlc3QpO1xuICAgIHRoaXMuaXBfcG9vbHMgPSBuZXcgSXBQb29sc0NsaWVudCh0aGlzLnJlcXVlc3QpO1xuICAgIHRoaXMubGlzdHMgPSBuZXcgTWFpbGluZ0xpc3RzQ2xpZW50KHRoaXMucmVxdWVzdCwgbWFpbExpc3RzTWVtYmVycyk7XG4gICAgdGhpcy52YWxpZGF0ZSA9IG5ldyBWYWxpZGF0ZUNsaWVudCh0aGlzLnJlcXVlc3QsIG11bHRpcGxlVmFsaWRhdGlvbkNsaWVudCk7XG4gICAgdGhpcy5zdWJhY2NvdW50cyA9IG5ldyBTdWJhY2NvdW50c0NsaWVudCh0aGlzLnJlcXVlc3QpO1xuICB9XG5cbiAgc2V0U3ViYWNjb3VudChzdWJhY2NvdW50SWQ6IHN0cmluZyk6IHZvaWQge1xuICAgIHRoaXMucmVxdWVzdD8uc2V0U3ViYWNjb3VudEhlYWRlcihzdWJhY2NvdW50SWQpO1xuICB9XG5cbiAgcmVzZXRTdWJhY2NvdW50KCk6IHZvaWQge1xuICAgIHRoaXMucmVxdWVzdD8ucmVzZXRTdWJhY2NvdW50SGVhZGVyKCk7XG4gIH1cbn1cbiIsImltcG9ydCBSZXF1ZXN0IGZyb20gJy4uL2NvbW1vbi9SZXF1ZXN0JztcbmltcG9ydCB7XG4gIE1haWxMaXN0TWVtYmVyc1F1ZXJ5LFxuICBDcmVhdGVVcGRhdGVNYWlsTGlzdE1lbWJlcnMsXG4gIE1haWxMaXN0TWVtYmVyLFxuICBNdWx0aXBsZU1lbWJlcnNEYXRhLFxuICBNdWx0aXBsZU1lbWJlcnNSZXFEYXRhLFxuICBEZWxldGVkTWVtYmVyLFxuICBDcmVhdGVVcGRhdGVNYWlsTGlzdE1lbWJlcnNSZXEsXG4gIE5ld011bHRpcGxlTWVtYmVyc1Jlc3BvbnNlLFxuICBNYWlsTGlzdE1lbWJlcnNSZXN1bHQsXG4gIE1haWxMaXN0TWVtYmVyc1Jlc3BvbnNlXG59IGZyb20gJy4uLy4uL1R5cGVzL01haWxpbmdMaXN0cyc7XG5pbXBvcnQgTmF2aWdhdGlvblRocnVQYWdlcyBmcm9tICcuLi9jb21tb24vTmF2aWdhdGlvblRocnVQYWdlcyc7XG5pbXBvcnQgeyBJTWFpbExpc3RzTWVtYmVycyB9IGZyb20gJy4uLy4uL0ludGVyZmFjZXMvTWFpbGluZ0xpc3RzJztcblxuZXhwb3J0IGRlZmF1bHQgY2xhc3MgTWFpbExpc3RzTWVtYmVyc1xuICBleHRlbmRzIE5hdmlnYXRpb25UaHJ1UGFnZXM8TWFpbExpc3RNZW1iZXJzUmVzdWx0PlxuICBpbXBsZW1lbnRzIElNYWlsTGlzdHNNZW1iZXJzIHtcbiAgYmFzZVJvdXRlOiBzdHJpbmc7XG4gIHJlcXVlc3Q6IFJlcXVlc3Q7XG5cbiAgY29uc3RydWN0b3IocmVxdWVzdDogUmVxdWVzdCkge1xuICAgIHN1cGVyKHJlcXVlc3QpO1xuICAgIHRoaXMucmVxdWVzdCA9IHJlcXVlc3Q7XG4gICAgdGhpcy5iYXNlUm91dGUgPSAnL3YzL2xpc3RzJztcbiAgfVxuXG4gIHByaXZhdGUgY2hlY2tBbmRVcGRhdGVEYXRhKGRhdGE6IENyZWF0ZVVwZGF0ZU1haWxMaXN0TWVtYmVycykge1xuICAgIGNvbnN0IG5ld0RhdGEgPSB7IC4uLmRhdGEgfTtcblxuICAgIGlmICh0eXBlb2YgZGF0YS52YXJzID09PSAnb2JqZWN0Jykge1xuICAgICAgbmV3RGF0YS52YXJzID0gSlNPTi5zdHJpbmdpZnkobmV3RGF0YS52YXJzKTtcbiAgICB9XG5cbiAgICBpZiAodHlwZW9mIGRhdGEuc3Vic2NyaWJlZCA9PT0gJ2Jvb2xlYW4nKSB7XG4gICAgICBuZXdEYXRhLnN1YnNjcmliZWQgPSBkYXRhLnN1YnNjcmliZWQgPyAneWVzJyA6ICdubyc7XG4gICAgfVxuXG4gICAgcmV0dXJuIG5ld0RhdGEgYXMgQ3JlYXRlVXBkYXRlTWFpbExpc3RNZW1iZXJzUmVxO1xuICB9XG5cbiAgcHJvdGVjdGVkIHBhcnNlTGlzdChcbiAgICByZXNwb25zZTogTWFpbExpc3RNZW1iZXJzUmVzcG9uc2UsXG4gICk6IE1haWxMaXN0TWVtYmVyc1Jlc3VsdCB7XG4gICAgY29uc3QgZGF0YSA9IHt9IGFzIE1haWxMaXN0TWVtYmVyc1Jlc3VsdDtcbiAgICBkYXRhLml0ZW1zID0gcmVzcG9uc2UuYm9keS5pdGVtcztcblxuICAgIGRhdGEucGFnZXMgPSB0aGlzLnBhcnNlUGFnZUxpbmtzKHJlc3BvbnNlLCAnPycsICdhZGRyZXNzJyk7XG4gICAgcmV0dXJuIGRhdGE7XG4gIH1cblxuICBhc3luYyBsaXN0TWVtYmVycyhcbiAgICBtYWlsTGlzdEFkZHJlc3M6IHN0cmluZyxcbiAgICBxdWVyeT86IE1haWxMaXN0TWVtYmVyc1F1ZXJ5XG4gICk6IFByb21pc2U8TWFpbExpc3RNZW1iZXJzUmVzdWx0PiB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdExpc3RXaXRoUGFnZXMoYCR7dGhpcy5iYXNlUm91dGV9LyR7bWFpbExpc3RBZGRyZXNzfS9tZW1iZXJzL3BhZ2VzYCwgcXVlcnkpO1xuICB9XG5cbiAgZ2V0TWVtYmVyKG1haWxMaXN0QWRkcmVzczogc3RyaW5nLCBtYWlsTGlzdE1lbWJlckFkZHJlc3M6IHN0cmluZyk6IFByb21pc2U8TWFpbExpc3RNZW1iZXI+IHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LmdldChgJHt0aGlzLmJhc2VSb3V0ZX0vJHttYWlsTGlzdEFkZHJlc3N9L21lbWJlcnMvJHttYWlsTGlzdE1lbWJlckFkZHJlc3N9YClcbiAgICAgIC50aGVuKChyZXNwb25zZSkgPT4gcmVzcG9uc2UuYm9keS5tZW1iZXIgYXMgTWFpbExpc3RNZW1iZXIpO1xuICB9XG5cbiAgY3JlYXRlTWVtYmVyKFxuICAgIG1haWxMaXN0QWRkcmVzczogc3RyaW5nLFxuICAgIGRhdGE6IENyZWF0ZVVwZGF0ZU1haWxMaXN0TWVtYmVyc1xuICApOiBQcm9taXNlPE1haWxMaXN0TWVtYmVyPiB7XG4gICAgY29uc3QgcmVxRGF0YSA9IHRoaXMuY2hlY2tBbmRVcGRhdGVEYXRhKGRhdGEpO1xuICAgIHJldHVybiB0aGlzLnJlcXVlc3QucG9zdFdpdGhGRChgJHt0aGlzLmJhc2VSb3V0ZX0vJHttYWlsTGlzdEFkZHJlc3N9L21lbWJlcnNgLCByZXFEYXRhKVxuICAgICAgLnRoZW4oKHJlc3BvbnNlKSA9PiByZXNwb25zZS5ib2R5Lm1lbWJlciBhcyBNYWlsTGlzdE1lbWJlcik7XG4gIH1cblxuICBjcmVhdGVNZW1iZXJzKFxuICAgIG1haWxMaXN0QWRkcmVzczogc3RyaW5nLFxuICAgIGRhdGE6IE11bHRpcGxlTWVtYmVyc0RhdGFcbiAgKTogUHJvbWlzZTxOZXdNdWx0aXBsZU1lbWJlcnNSZXNwb25zZT4ge1xuICAgIGNvbnN0IG5ld0RhdGE6IE11bHRpcGxlTWVtYmVyc1JlcURhdGEgPSB7XG4gICAgICBtZW1iZXJzOiBBcnJheS5pc0FycmF5KGRhdGEubWVtYmVycykgPyBKU09OLnN0cmluZ2lmeShkYXRhLm1lbWJlcnMpIDogZGF0YS5tZW1iZXJzLFxuICAgICAgdXBzZXJ0OiBkYXRhLnVwc2VydFxuICAgIH07XG5cbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LnBvc3RXaXRoRkQoYCR7dGhpcy5iYXNlUm91dGV9LyR7bWFpbExpc3RBZGRyZXNzfS9tZW1iZXJzLmpzb25gLCBuZXdEYXRhKVxuICAgICAgLnRoZW4oKHJlc3BvbnNlKSA9PiByZXNwb25zZS5ib2R5IGFzIE5ld011bHRpcGxlTWVtYmVyc1Jlc3BvbnNlKTtcbiAgfVxuXG4gIHVwZGF0ZU1lbWJlcihcbiAgICBtYWlsTGlzdEFkZHJlc3M6IHN0cmluZyxcbiAgICBtYWlsTGlzdE1lbWJlckFkZHJlc3M6IHN0cmluZyxcbiAgICBkYXRhOiBDcmVhdGVVcGRhdGVNYWlsTGlzdE1lbWJlcnNcbiAgKTogUHJvbWlzZTxNYWlsTGlzdE1lbWJlcj4ge1xuICAgIGNvbnN0IHJlcURhdGEgPSB0aGlzLmNoZWNrQW5kVXBkYXRlRGF0YShkYXRhKTtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LnB1dFdpdGhGRChgJHt0aGlzLmJhc2VSb3V0ZX0vJHttYWlsTGlzdEFkZHJlc3N9L21lbWJlcnMvJHttYWlsTGlzdE1lbWJlckFkZHJlc3N9YCwgcmVxRGF0YSlcbiAgICAgIC50aGVuKChyZXNwb25zZSkgPT4gcmVzcG9uc2UuYm9keS5tZW1iZXIgYXMgTWFpbExpc3RNZW1iZXIpO1xuICB9XG5cbiAgZGVzdHJveU1lbWJlcihtYWlsTGlzdEFkZHJlc3M6IHN0cmluZywgbWFpbExpc3RNZW1iZXJBZGRyZXNzOiBzdHJpbmcpIDogUHJvbWlzZTxEZWxldGVkTWVtYmVyPiB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5kZWxldGUoYCR7dGhpcy5iYXNlUm91dGV9LyR7bWFpbExpc3RBZGRyZXNzfS9tZW1iZXJzLyR7bWFpbExpc3RNZW1iZXJBZGRyZXNzfWApXG4gICAgICAudGhlbigocmVzcG9uc2UpID0+IHJlc3BvbnNlLmJvZHkgYXMgRGVsZXRlZE1lbWJlcik7XG4gIH1cbn1cbiIsImltcG9ydCBSZXF1ZXN0IGZyb20gJy4uL2NvbW1vbi9SZXF1ZXN0JztcbmltcG9ydCB7XG4gIExpc3RzUXVlcnksXG4gIENyZWF0ZVVwZGF0ZUxpc3QsXG4gIERlc3Ryb3llZExpc3QsXG4gIE1haWxpbmdMaXN0LFxuICBNYWlsaW5nTGlzdFZhbGlkYXRpb25BcGlSZXNwb25zZSxcbiAgU3RhcnRWYWxpZGF0aW9uUmVzdWx0LFxuICBNYWlsaW5nTGlzdFZhbGlkYXRpb25SZXN1bHQsXG4gIE1haWxpbmdMaXN0Q2FuY2VsVmFsaWRhdGlvblJlc3VsdCxcbiAgTWFpbGluZ0xpc3RSZXN1bHQsXG4gIE1haWxpbmdMaXN0QXBpUmVzcG9uc2Vcbn0gZnJvbSAnLi4vLi4vVHlwZXMvTWFpbGluZ0xpc3RzJztcbmltcG9ydCB7IElNYWlsTGlzdHNNZW1iZXJzIH0gZnJvbSAnLi4vLi4vSW50ZXJmYWNlcy9NYWlsaW5nTGlzdHMvTWFpbGluZ0xpc3RNZW1iZXJzJztcbmltcG9ydCBOYXZpZ2F0aW9uVGhydVBhZ2VzIGZyb20gJy4uL2NvbW1vbi9OYXZpZ2F0aW9uVGhydVBhZ2VzJztcbmltcG9ydCB7IElNYWlsaW5nTGlzdHNDbGllbnQgfSBmcm9tICcuLi8uLi9JbnRlcmZhY2VzJztcblxuZXhwb3J0IGRlZmF1bHQgY2xhc3MgTWFpbGluZ0xpc3RzQ2xpZW50XG4gIGV4dGVuZHMgTmF2aWdhdGlvblRocnVQYWdlczxNYWlsaW5nTGlzdFJlc3VsdD5cbiAgaW1wbGVtZW50cyBJTWFpbGluZ0xpc3RzQ2xpZW50IHtcbiAgYmFzZVJvdXRlOiBzdHJpbmc7XG4gIHJlcXVlc3Q6IFJlcXVlc3Q7XG4gIHB1YmxpYyBtZW1iZXJzOiBJTWFpbExpc3RzTWVtYmVycztcblxuICBjb25zdHJ1Y3RvcihyZXF1ZXN0OiBSZXF1ZXN0LCBtZW1iZXJzOiBJTWFpbExpc3RzTWVtYmVycykge1xuICAgIHN1cGVyKHJlcXVlc3QpO1xuICAgIHRoaXMucmVxdWVzdCA9IHJlcXVlc3Q7XG4gICAgdGhpcy5iYXNlUm91dGUgPSAnL3YzL2xpc3RzJztcbiAgICB0aGlzLm1lbWJlcnMgPSBtZW1iZXJzO1xuICB9XG5cbiAgcHJpdmF0ZSBwYXJzZVZhbGlkYXRpb25SZXN1bHQoXG4gICAgc3RhdHVzOiBudW1iZXIsXG4gICAgZGF0YTogTWFpbGluZ0xpc3RWYWxpZGF0aW9uQXBpUmVzcG9uc2VcbiAgKTogTWFpbGluZ0xpc3RWYWxpZGF0aW9uUmVzdWx0IHtcbiAgICByZXR1cm4ge1xuICAgICAgc3RhdHVzLFxuICAgICAgdmFsaWRhdGlvblJlc3VsdDoge1xuICAgICAgICAuLi5kYXRhLFxuICAgICAgICBjcmVhdGVkX2F0OiBuZXcgRGF0ZShkYXRhLmNyZWF0ZWRfYXQgKiAxMDAwKSAvLyBhZGQgbWlsbGlzZWNvbmQgdG8gVW5peCB0aW1lc3RhbXBcbiAgICAgIH1cbiAgICB9IGFzIE1haWxpbmdMaXN0VmFsaWRhdGlvblJlc3VsdDtcbiAgfVxuXG4gIHByb3RlY3RlZCBwYXJzZUxpc3QocmVzcG9uc2U6IE1haWxpbmdMaXN0QXBpUmVzcG9uc2UpOiBNYWlsaW5nTGlzdFJlc3VsdCB7XG4gICAgY29uc3QgZGF0YSA9IHt9IGFzIE1haWxpbmdMaXN0UmVzdWx0O1xuXG4gICAgZGF0YS5pdGVtcyA9IHJlc3BvbnNlLmJvZHkuaXRlbXM7XG5cbiAgICBkYXRhLnBhZ2VzID0gdGhpcy5wYXJzZVBhZ2VMaW5rcyhyZXNwb25zZSwgJz8nLCAnYWRkcmVzcycpO1xuICAgIGRhdGEuc3RhdHVzID0gcmVzcG9uc2Uuc3RhdHVzO1xuXG4gICAgcmV0dXJuIGRhdGE7XG4gIH1cblxuICBhc3luYyBsaXN0KHF1ZXJ5PzogTGlzdHNRdWVyeSk6IFByb21pc2U8TWFpbGluZ0xpc3RSZXN1bHQ+IHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0TGlzdFdpdGhQYWdlcyhgJHt0aGlzLmJhc2VSb3V0ZX0vcGFnZXNgLCBxdWVyeSk7XG4gIH1cblxuICBnZXQobWFpbExpc3RBZGRyZXNzOiBzdHJpbmcpOiBQcm9taXNlPE1haWxpbmdMaXN0PiB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5nZXQoYCR7dGhpcy5iYXNlUm91dGV9LyR7bWFpbExpc3RBZGRyZXNzfWApXG4gICAgICAudGhlbigocmVzcG9uc2UpID0+IHJlc3BvbnNlLmJvZHkubGlzdCBhcyBNYWlsaW5nTGlzdCk7XG4gIH1cblxuICBjcmVhdGUoZGF0YTogQ3JlYXRlVXBkYXRlTGlzdCk6IFByb21pc2U8TWFpbGluZ0xpc3Q+IHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LnBvc3RXaXRoRkQodGhpcy5iYXNlUm91dGUsIGRhdGEpXG4gICAgICAudGhlbigocmVzcG9uc2UpID0+IHJlc3BvbnNlLmJvZHkubGlzdCBhcyBNYWlsaW5nTGlzdCk7XG4gIH1cblxuICB1cGRhdGUobWFpbExpc3RBZGRyZXNzOiBzdHJpbmcsIGRhdGE6IENyZWF0ZVVwZGF0ZUxpc3QpOiBQcm9taXNlPE1haWxpbmdMaXN0PiB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5wdXRXaXRoRkQoYCR7dGhpcy5iYXNlUm91dGV9LyR7bWFpbExpc3RBZGRyZXNzfWAsIGRhdGEpXG4gICAgICAudGhlbigocmVzcG9uc2UpID0+IHJlc3BvbnNlLmJvZHkubGlzdCBhcyBNYWlsaW5nTGlzdCk7XG4gIH1cblxuICBkZXN0cm95KG1haWxMaXN0QWRkcmVzczogc3RyaW5nKTogUHJvbWlzZTxEZXN0cm95ZWRMaXN0PiB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5kZWxldGUoYCR7dGhpcy5iYXNlUm91dGV9LyR7bWFpbExpc3RBZGRyZXNzfWApXG4gICAgICAudGhlbigocmVzcG9uc2UpID0+IHJlc3BvbnNlLmJvZHkgYXMgRGVzdHJveWVkTGlzdCk7XG4gIH1cblxuICB2YWxpZGF0ZShtYWlsTGlzdEFkZHJlc3M6IHN0cmluZyk6IFByb21pc2U8U3RhcnRWYWxpZGF0aW9uUmVzdWx0PiB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5wb3N0KGAke3RoaXMuYmFzZVJvdXRlfS8ke21haWxMaXN0QWRkcmVzc30vdmFsaWRhdGVgLCB7fSlcbiAgICAgIC50aGVuKChyZXNwb25zZSkgPT4gKHtcbiAgICAgICAgc3RhdHVzOiByZXNwb25zZS5zdGF0dXMsXG4gICAgICAgIC4uLnJlc3BvbnNlLmJvZHlcbiAgICAgIH0pIGFzIFN0YXJ0VmFsaWRhdGlvblJlc3VsdCk7XG4gIH1cblxuICB2YWxpZGF0aW9uUmVzdWx0KG1haWxMaXN0QWRkcmVzczogc3RyaW5nKTogUHJvbWlzZTxNYWlsaW5nTGlzdFZhbGlkYXRpb25SZXN1bHQ+IHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LmdldChgJHt0aGlzLmJhc2VSb3V0ZX0vJHttYWlsTGlzdEFkZHJlc3N9L3ZhbGlkYXRlYClcbiAgICAgIC50aGVuKFxuICAgICAgICAocmVzcG9uc2UpID0+IHRoaXMucGFyc2VWYWxpZGF0aW9uUmVzdWx0KFxuICAgICAgICAgIHJlc3BvbnNlLnN0YXR1cyxcbiAgICAgICAgICAgcmVzcG9uc2UuYm9keSBhcyBNYWlsaW5nTGlzdFZhbGlkYXRpb25BcGlSZXNwb25zZVxuICAgICAgICApXG4gICAgICApO1xuICB9XG5cbiAgY2FuY2VsVmFsaWRhdGlvbihtYWlsTGlzdEFkZHJlc3M6IHN0cmluZyk6IFByb21pc2U8TWFpbGluZ0xpc3RDYW5jZWxWYWxpZGF0aW9uUmVzdWx0PiB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5kZWxldGUoYCR7dGhpcy5iYXNlUm91dGV9LyR7bWFpbExpc3RBZGRyZXNzfS92YWxpZGF0ZWApXG4gICAgICAudGhlbigocmVzcG9uc2UpID0+ICh7XG4gICAgICAgIHN0YXR1czogcmVzcG9uc2Uuc3RhdHVzLFxuICAgICAgICBtZXNzYWdlOiByZXNwb25zZS5ib2R5Lm1lc3NhZ2VcbiAgICAgIH0gYXMgTWFpbGluZ0xpc3RDYW5jZWxWYWxpZGF0aW9uUmVzdWx0KSk7XG4gIH1cbn1cbiIsImltcG9ydCBBUElFcnJvciBmcm9tICcuL2NvbW1vbi9FcnJvcic7XG5pbXBvcnQge1xuICBBUElFcnJvck9wdGlvbnMsXG4gIE1haWxndW5NZXNzYWdlRGF0YSxcbiAgTWVzc2FnZXNTZW5kQVBJUmVzcG9uc2UsXG4gIE1lc3NhZ2VzU2VuZFJlc3VsdFxufSBmcm9tICcuLi9UeXBlcyc7XG5pbXBvcnQgUmVxdWVzdCBmcm9tICcuL2NvbW1vbi9SZXF1ZXN0JztcbmltcG9ydCB7IElNZXNzYWdlc0NsaWVudCB9IGZyb20gJy4uL0ludGVyZmFjZXMnO1xuXG5leHBvcnQgZGVmYXVsdCBjbGFzcyBNZXNzYWdlc0NsaWVudCBpbXBsZW1lbnRzIElNZXNzYWdlc0NsaWVudCB7XG4gIHJlcXVlc3Q6IFJlcXVlc3Q7XG5cbiAgY29uc3RydWN0b3IocmVxdWVzdDogUmVxdWVzdCkge1xuICAgIHRoaXMucmVxdWVzdCA9IHJlcXVlc3Q7XG4gIH1cblxuICBwcml2YXRlIHByZXBhcmVCb29sZWFuVmFsdWVzKGRhdGE6IE1haWxndW5NZXNzYWdlRGF0YSk6IE1haWxndW5NZXNzYWdlRGF0YSB7XG4gICAgY29uc3QgeWVzTm9Qcm9wZXJ0aWVzID0gbmV3IFNldChbXG4gICAgICAnbzp0ZXN0bW9kZScsXG4gICAgICAndDp0ZXh0JyxcbiAgICAgICdvOmRraW0nLFxuICAgICAgJ286dHJhY2tpbmcnLFxuICAgICAgJ286dHJhY2tpbmctY2xpY2tzJyxcbiAgICAgICdvOnRyYWNraW5nLW9wZW5zJyxcbiAgICAgICdvOnJlcXVpcmUtdGxzJyxcbiAgICAgICdvOnNraXAtdmVyaWZpY2F0aW9uJ1xuICAgIF0pO1xuXG4gICAgaWYgKCFkYXRhIHx8IE9iamVjdC5rZXlzKGRhdGEpLmxlbmd0aCA9PT0gMCkge1xuICAgICAgdGhyb3cgbmV3IEFQSUVycm9yKHtcbiAgICAgICAgc3RhdHVzOiA0MDAsXG4gICAgICAgIG1lc3NhZ2U6ICdNZXNzYWdlIGRhdGEgb2JqZWN0IGNhbiBub3QgYmUgZW1wdHknXG4gICAgICB9IGFzIEFQSUVycm9yT3B0aW9ucyk7XG4gICAgfVxuICAgIHJldHVybiBPYmplY3Qua2V5cyhkYXRhKS5yZWR1Y2UoKGFjYywga2V5KSA9PiB7XG4gICAgICBpZiAoeWVzTm9Qcm9wZXJ0aWVzLmhhcyhrZXkpICYmIHR5cGVvZiBkYXRhW2tleV0gPT09ICdib29sZWFuJykge1xuICAgICAgICBhY2Nba2V5XSA9IGRhdGFba2V5XSA/ICd5ZXMnIDogJ25vJztcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIGFjY1trZXldID0gZGF0YVtrZXldO1xuICAgICAgfVxuICAgICAgcmV0dXJuIGFjYztcbiAgICB9LCB7fSBhcyBNYWlsZ3VuTWVzc2FnZURhdGEpO1xuICB9XG5cbiAgX3BhcnNlUmVzcG9uc2UocmVzcG9uc2U6IE1lc3NhZ2VzU2VuZEFQSVJlc3BvbnNlKTogTWVzc2FnZXNTZW5kUmVzdWx0IHtcbiAgICByZXR1cm4ge1xuICAgICAgc3RhdHVzOiByZXNwb25zZS5zdGF0dXMsXG4gICAgICAuLi5yZXNwb25zZS5ib2R5XG4gICAgfTtcbiAgfVxuXG4gIGNyZWF0ZShkb21haW46IHN0cmluZywgZGF0YTogTWFpbGd1bk1lc3NhZ2VEYXRhKTogUHJvbWlzZTxNZXNzYWdlc1NlbmRSZXN1bHQ+IHtcbiAgICBpZiAoZGF0YS5tZXNzYWdlKSB7XG4gICAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LnBvc3RXaXRoRkQoYC92My8ke2RvbWFpbn0vbWVzc2FnZXMubWltZWAsIGRhdGEpXG4gICAgICAgIC50aGVuKHRoaXMuX3BhcnNlUmVzcG9uc2UpO1xuICAgIH1cblxuICAgIGNvbnN0IG1vZGlmaWVkRGF0YSA9IHRoaXMucHJlcGFyZUJvb2xlYW5WYWx1ZXMoZGF0YSk7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5wb3N0V2l0aEZEKGAvdjMvJHtkb21haW59L21lc3NhZ2VzYCwgbW9kaWZpZWREYXRhKVxuICAgICAgLnRoZW4odGhpcy5fcGFyc2VSZXNwb25zZSk7XG4gIH1cbn1cbiIsImltcG9ydCB7IElSb3V0ZXNDbGllbnQgfSBmcm9tICcuLi9JbnRlcmZhY2VzJztcbmltcG9ydCB7XG4gIENyZWF0ZVVwZGF0ZVJvdXRlRGF0YSwgRGVzdHJveVJvdXRlUmVzcG9uc2UsIFJvdXRlLCBSb3V0ZXNMaXN0UXVlcnksIFVwZGF0ZVJvdXRlUmVzcG9uc2Vcbn0gZnJvbSAnLi4vVHlwZXMvUm91dGVzJztcbmltcG9ydCBSZXF1ZXN0IGZyb20gJy4vY29tbW9uL1JlcXVlc3QnO1xuXG5leHBvcnQgZGVmYXVsdCBjbGFzcyBSb3V0ZXNDbGllbnQgaW1wbGVtZW50cyBJUm91dGVzQ2xpZW50IHtcbiAgcmVxdWVzdDogUmVxdWVzdDtcblxuICBjb25zdHJ1Y3RvcihyZXF1ZXN0OiBSZXF1ZXN0KSB7XG4gICAgdGhpcy5yZXF1ZXN0ID0gcmVxdWVzdDtcbiAgfVxuXG4gIGxpc3QocXVlcnk6IFJvdXRlc0xpc3RRdWVyeSk6IFByb21pc2U8Um91dGVbXT4ge1xuICAgIHJldHVybiB0aGlzLnJlcXVlc3QuZ2V0KCcvdjMvcm91dGVzJywgcXVlcnkpXG4gICAgICAudGhlbigocmVzcG9uc2UpID0+IHJlc3BvbnNlLmJvZHkuaXRlbXMpO1xuICB9XG5cbiAgZ2V0KGlkOiBzdHJpbmcpOiBQcm9taXNlPFJvdXRlPiB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5nZXQoYC92My9yb3V0ZXMvJHtpZH1gKVxuICAgICAgLnRoZW4oKHJlc3BvbnNlKSA9PiByZXNwb25zZS5ib2R5LnJvdXRlKTtcbiAgfVxuXG4gIGNyZWF0ZShkYXRhOiBDcmVhdGVVcGRhdGVSb3V0ZURhdGEpOiBQcm9taXNlPFJvdXRlPiB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5wb3N0V2l0aEZEKCcvdjMvcm91dGVzJywgZGF0YSlcbiAgICAgIC50aGVuKChyZXNwb25zZSkgPT4gcmVzcG9uc2UuYm9keS5yb3V0ZSk7XG4gIH1cblxuICB1cGRhdGUoaWQ6IHN0cmluZywgZGF0YTogQ3JlYXRlVXBkYXRlUm91dGVEYXRhKTogUHJvbWlzZTxVcGRhdGVSb3V0ZVJlc3BvbnNlPiB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5wdXRXaXRoRkQoYC92My9yb3V0ZXMvJHtpZH1gLCBkYXRhKVxuICAgICAgLnRoZW4oKHJlc3BvbnNlKSA9PiByZXNwb25zZS5ib2R5KTtcbiAgfVxuXG4gIGRlc3Ryb3koaWQ6IHN0cmluZyk6IFByb21pc2U8RGVzdHJveVJvdXRlUmVzcG9uc2U+IHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LmRlbGV0ZShgL3YzL3JvdXRlcy8ke2lkfWApXG4gICAgICAudGhlbigocmVzcG9uc2UpID0+IHJlc3BvbnNlLmJvZHkpO1xuICB9XG59XG4iLCJpbXBvcnQgdXJsam9pbiBmcm9tICd1cmwtam9pbic7XG5pbXBvcnQgUmVxdWVzdCBmcm9tICcuLi9jb21tb24vUmVxdWVzdCc7XG5pbXBvcnQgeyBTdGF0c1F1ZXJ5LCBTdGF0c09wdGlvbnMgfSBmcm9tICcuLi8uLi9UeXBlcy9TdGF0cyc7XG5pbXBvcnQgeyBJTG9nZ2VyIH0gZnJvbSAnLi4vLi4vSW50ZXJmYWNlcy9Db21tb24nO1xuaW1wb3J0IFN0YXRzQ29udGFpbmVyIGZyb20gJy4vU3RhdHNDb250YWluZXInO1xuaW1wb3J0IHsgSVN0YXRzQ2xpZW50LCBJU3RhdHNDb250YWluZXIgfSBmcm9tICcuLi8uLi9JbnRlcmZhY2VzL1N0YXRzJztcblxuZXhwb3J0IGRlZmF1bHQgY2xhc3MgU3RhdHNDbGllbnQgaW1wbGVtZW50cyBJU3RhdHNDbGllbnQge1xuICByZXF1ZXN0OiBSZXF1ZXN0O1xuICBwcml2YXRlIGxvZ2dlcjogSUxvZ2dlcjtcblxuICBjb25zdHJ1Y3RvcihyZXF1ZXN0OiBSZXF1ZXN0LCBsb2dnZXI6IElMb2dnZXIgPSBjb25zb2xlKSB7XG4gICAgdGhpcy5yZXF1ZXN0ID0gcmVxdWVzdDtcbiAgICB0aGlzLmxvZ2dlciA9IGxvZ2dlcjtcbiAgfVxuXG4gIHByaXZhdGUgY29udmVydERhdGVUb1VUQyhrZXk6c3RyaW5nLCBpbnB1dERhdGU6IERhdGUpOiBBcnJheTxzdHJpbmc+IHtcbiAgICAvKlxuICAgICAgQmVjYXVzZSBcIm5ldyBEYXRlKCcyMDIyLTEyLTI1VDAwOjAwOjAwLjAwMFonKVwiIGJlY29tZXMgXCJTdW4gRGVjIDI1IDIwMjIgMDI6MDA6MDAgR01UKzAyMDBcIlxuICAgICAgKHBsdXMgMiBob3VycyBmcm9tIHRoZSB0aW1lem9uZSlcbiAgICAgIGFuZCBiZWNhdXNlIGZvciBBUEksIHdlIG5lZWQgdG8gcHJvdmlkZSB0aGUgZGF0ZSBpbiB0aGUgZXhwZWN0ZWQgZm9ybWF0XG4gICAgICBleDogJ1RodSwgMTMgT2N0IDIwMTEgMTg6MDI6MDAgKzAwMDAnLlxuICAgICAgSGVyZSB3ZSB0cnkgYXV0by1jb252ZXJ0IHRoZW0gdG8gVVRDXG4gICAgKi9cbiAgICB0aGlzLmxvZ2dlci53YXJuKGBEYXRlOlwiJHtpbnB1dERhdGV9XCIgd2FzIGF1dG8tY29udmVydGVkIHRvIFVUQyB0aW1lIHpvbmUuXG5WYWx1ZSBcIiR7aW5wdXREYXRlLnRvVVRDU3RyaW5nKCl9XCIgd2lsbCBiZSB1c2VkIGZvciByZXF1ZXN0LlxuQ29uc2lkZXIgdXNpbmcgc3RpbmcgdHlwZSBmb3IgcHJvcGVydHkgXCIke2tleX1cIiB0byBhdm9pZCBhdXRvLWNvbnZlcnRpbmdgKTtcbiAgICByZXR1cm4gW2tleSwgaW5wdXREYXRlLnRvVVRDU3RyaW5nKCldO1xuICB9XG5cbiAgcHJpdmF0ZSBwcmVwYXJlU2VhcmNoUGFyYW1zKHF1ZXJ5OiBTdGF0c1F1ZXJ5IHwgdW5kZWZpbmVkKTogQXJyYXk8QXJyYXk8c3RyaW5nPj4ge1xuICAgIGxldCBzZWFyY2hQYXJhbXMgPSBbXSBhcyBBcnJheTxBcnJheTxzdHJpbmc+PjtcbiAgICBpZiAodHlwZW9mIHF1ZXJ5ID09PSAnb2JqZWN0JyAmJiBPYmplY3Qua2V5cyhxdWVyeSkubGVuZ3RoKSB7XG4gICAgICBzZWFyY2hQYXJhbXMgPSBPYmplY3QuZW50cmllcyhxdWVyeSkucmVkdWNlKChhcnJheVdpdGhQYWlycywgY3VycmVudFBhaXIpID0+IHtcbiAgICAgICAgY29uc3QgW2tleSwgdmFsdWVdID0gY3VycmVudFBhaXI7XG5cbiAgICAgICAgaWYgKEFycmF5LmlzQXJyYXkodmFsdWUpICYmIHZhbHVlLmxlbmd0aCkgeyAvLyBldmVudDogWydkZWxpdmVyZWQnLCAnYWNjZXB0ZWQnXVxuICAgICAgICAgIGNvbnN0IHJlcGVhdGVkUHJvcGVydHkgPSB2YWx1ZS5tYXAoKGl0ZW0pID0+IFtrZXksIGl0ZW1dKTtcbiAgICAgICAgICByZXR1cm4gWy4uLmFycmF5V2l0aFBhaXJzLCAuLi5yZXBlYXRlZFByb3BlcnR5XTsgLy8gW1tldmVudCxkZWxpdmVyZWRdLCBbZXZlbnQsYWNjZXB0ZWRdXVxuICAgICAgICB9XG5cbiAgICAgICAgaWYgKHZhbHVlIGluc3RhbmNlb2YgRGF0ZSkge1xuICAgICAgICAgIGFycmF5V2l0aFBhaXJzLnB1c2godGhpcy5jb252ZXJ0RGF0ZVRvVVRDKGtleSwgdmFsdWUpKTtcbiAgICAgICAgICByZXR1cm4gYXJyYXlXaXRoUGFpcnM7XG4gICAgICAgIH1cblxuICAgICAgICBpZiAodHlwZW9mIHZhbHVlID09PSAnc3RyaW5nJykge1xuICAgICAgICAgIGFycmF5V2l0aFBhaXJzLnB1c2goW2tleSwgdmFsdWVdKTtcbiAgICAgICAgfVxuXG4gICAgICAgIHJldHVybiBhcnJheVdpdGhQYWlycztcbiAgICAgIH0sIFtdIGFzIEFycmF5PEFycmF5PHN0cmluZz4+KTtcbiAgICB9XG5cbiAgICByZXR1cm4gc2VhcmNoUGFyYW1zO1xuICB9XG5cbiAgcHJpdmF0ZSBwYXJzZVN0YXRzKHJlc3BvbnNlOiB7IGJvZHk6IFN0YXRzT3B0aW9ucyB9KTogSVN0YXRzQ29udGFpbmVyIHtcbiAgICByZXR1cm4gbmV3IFN0YXRzQ29udGFpbmVyKHJlc3BvbnNlLmJvZHkpO1xuICB9XG5cbiAgZ2V0RG9tYWluKGRvbWFpbjogc3RyaW5nLCBxdWVyeT86IFN0YXRzUXVlcnkpOiBQcm9taXNlPElTdGF0c0NvbnRhaW5lcj4ge1xuICAgIGNvbnN0IHNlYXJjaFBhcmFtcyA9IHRoaXMucHJlcGFyZVNlYXJjaFBhcmFtcyhxdWVyeSk7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5nZXQodXJsam9pbignL3YzJywgZG9tYWluLCAnc3RhdHMvdG90YWwnKSwgc2VhcmNoUGFyYW1zKVxuICAgICAgLnRoZW4odGhpcy5wYXJzZVN0YXRzKTtcbiAgfVxuXG4gIGdldEFjY291bnQocXVlcnk/OiBTdGF0c1F1ZXJ5KTogUHJvbWlzZTxJU3RhdHNDb250YWluZXI+IHtcbiAgICBjb25zdCBzZWFyY2hQYXJhbXMgPSB0aGlzLnByZXBhcmVTZWFyY2hQYXJhbXMocXVlcnkpO1xuICAgIHJldHVybiB0aGlzLnJlcXVlc3QuZ2V0KCcvdjMvc3RhdHMvdG90YWwnLCBzZWFyY2hQYXJhbXMpXG4gICAgICAudGhlbih0aGlzLnBhcnNlU3RhdHMpO1xuICB9XG59XG4iLCJpbXBvcnQgeyBJU3RhdHNDb250YWluZXIgfSBmcm9tICcuLi8uLi9JbnRlcmZhY2VzL1N0YXRzJztcbmltcG9ydCB7IFN0YXQsIFN0YXRzT3B0aW9ucyB9IGZyb20gJy4uLy4uL1R5cGVzL1N0YXRzJztcblxuZXhwb3J0IGRlZmF1bHQgY2xhc3MgU3RhdHNDb250YWluZXIgaW1wbGVtZW50cyBJU3RhdHNDb250YWluZXIge1xuICAgIHN0YXJ0OiBEYXRlO1xuICAgIGVuZDogRGF0ZTtcbiAgICByZXNvbHV0aW9uOiBzdHJpbmc7XG4gICAgc3RhdHM6IFN0YXRbXTtcbiAgICBjb25zdHJ1Y3RvcihkYXRhOiBTdGF0c09wdGlvbnMpIHtcbiAgICAgIHRoaXMuc3RhcnQgPSBuZXcgRGF0ZShkYXRhLnN0YXJ0KTtcbiAgICAgIHRoaXMuZW5kID0gbmV3IERhdGUoZGF0YS5lbmQpO1xuICAgICAgdGhpcy5yZXNvbHV0aW9uID0gZGF0YS5yZXNvbHV0aW9uO1xuICAgICAgdGhpcy5zdGF0cyA9IGRhdGEuc3RhdHMubWFwKGZ1bmN0aW9uIChzdGF0OiBTdGF0KSB7XG4gICAgICAgIGNvbnN0IHJlcyA9IHsgLi4uc3RhdCB9O1xuICAgICAgICByZXMudGltZSA9IG5ldyBEYXRlKHN0YXQudGltZSk7XG4gICAgICAgIHJldHVybiByZXM7XG4gICAgICB9KTtcbiAgICB9XG59XG4iLCJpbXBvcnQgUmVxdWVzdCBmcm9tICcuL2NvbW1vbi9SZXF1ZXN0JztcbmltcG9ydCB7IElTdWJhY2NvdW50c0NsaWVudCB9IGZyb20gJy4uL0ludGVyZmFjZXMnO1xuaW1wb3J0IHtcbiAgU3ViYWNjb3VudExpc3RSZXNwb25zZURhdGEsXG4gIFN1YmFjY291bnRSZXNwb25zZURhdGEsXG4gIFN1YmFjY291bnRzUXVlcnksXG59IGZyb20gJy4uL1R5cGVzJztcblxuZXhwb3J0IGRlZmF1bHQgY2xhc3MgU3ViYWNjb3VudHNDbGllbnQgaW1wbGVtZW50cyBJU3ViYWNjb3VudHNDbGllbnQge1xuICByZXF1ZXN0OiBSZXF1ZXN0O1xuICBzdGF0aWMgU1VCQUNDT1VOVF9IRUFERVIgPSAnWC1NYWlsZ3VuLU9uLUJlaGFsZi1PZic7XG5cbiAgY29uc3RydWN0b3IocmVxdWVzdDogUmVxdWVzdCkge1xuICAgIHRoaXMucmVxdWVzdCA9IHJlcXVlc3Q7XG4gIH1cblxuICBsaXN0KHF1ZXJ5PzogU3ViYWNjb3VudHNRdWVyeSk6IFByb21pc2U8U3ViYWNjb3VudExpc3RSZXNwb25zZURhdGE+IHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LmdldCgnL3Y1L2FjY291bnRzL3N1YmFjY291bnRzJywgcXVlcnkpXG4gICAgICAudGhlbigocmVzKSA9PiByZXMuYm9keSk7XG4gIH1cblxuICBnZXQoaWQ6c3RyaW5nKTogUHJvbWlzZTxTdWJhY2NvdW50UmVzcG9uc2VEYXRhPiB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5nZXQoYC92NS9hY2NvdW50cy9zdWJhY2NvdW50cy8ke2lkfWApXG4gICAgICAudGhlbigocmVzKSA9PiByZXMuYm9keSk7XG4gIH1cblxuICBjcmVhdGUobmFtZTpzdHJpbmcpOiBQcm9taXNlPFN1YmFjY291bnRSZXNwb25zZURhdGE+IHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LnBvc3RXaXRoRkQoJy92NS9hY2NvdW50cy9zdWJhY2NvdW50cycsIHsgbmFtZSB9KVxuICAgICAgLnRoZW4oKHJlcykgPT4gcmVzLmJvZHkpO1xuICB9XG5cbiAgZW5hYmxlKGlkOnN0cmluZyk6IFByb21pc2U8U3ViYWNjb3VudFJlc3BvbnNlRGF0YT4ge1xuICAgIHJldHVybiB0aGlzLnJlcXVlc3QucG9zdChgL3Y1L2FjY291bnRzL3N1YmFjY291bnRzLyR7aWR9L2VuYWJsZWApXG4gICAgICAudGhlbigocmVzKSA9PiByZXMuYm9keSk7XG4gIH1cblxuICBkaXNhYmxlKGlkOnN0cmluZyk6IFByb21pc2U8U3ViYWNjb3VudFJlc3BvbnNlRGF0YT4ge1xuICAgIHJldHVybiB0aGlzLnJlcXVlc3QucG9zdChgL3Y1L2FjY291bnRzL3N1YmFjY291bnRzLyR7aWR9L2Rpc2FibGVgKVxuICAgICAgLnRoZW4oKHJlcykgPT4gcmVzLmJvZHkpO1xuICB9XG59XG4iLCJpbXBvcnQgeyBTdXBwcmVzc2lvbk1vZGVscyB9IGZyb20gJy4uLy4uL0VudW1zJztcbmltcG9ydCB7IElCb3VuY2UgfSBmcm9tICcuLi8uLi9JbnRlcmZhY2VzL1N1cHByZXNzaW9ucyc7XG5pbXBvcnQgeyBCb3VuY2VEYXRhIH0gZnJvbSAnLi4vLi4vVHlwZXMvU3VwcHJlc3Npb25zJztcbmltcG9ydCBTdXBwcmVzc2lvbiBmcm9tICcuL1N1cHByZXNzaW9uJztcblxuZXhwb3J0IGRlZmF1bHQgY2xhc3MgQm91bmNlIGV4dGVuZHMgU3VwcHJlc3Npb24gaW1wbGVtZW50cyBJQm91bmNlIHtcbiAgICBhZGRyZXNzOiBzdHJpbmc7XG4gICAgY29kZTogbnVtYmVyO1xuICAgIGVycm9yOiBzdHJpbmc7XG4gICAgLyogZXNsaW50LWRpc2FibGUgY2FtZWxjYXNlICovXG4gICAgY3JlYXRlZF9hdDogRGF0ZTtcblxuICAgIGNvbnN0cnVjdG9yKGRhdGE6IEJvdW5jZURhdGEpIHtcbiAgICAgIHN1cGVyKFN1cHByZXNzaW9uTW9kZWxzLkJPVU5DRVMpO1xuICAgICAgdGhpcy5hZGRyZXNzID0gZGF0YS5hZGRyZXNzO1xuICAgICAgdGhpcy5jb2RlID0gK2RhdGEuY29kZTtcbiAgICAgIHRoaXMuZXJyb3IgPSBkYXRhLmVycm9yO1xuICAgICAgdGhpcy5jcmVhdGVkX2F0ID0gbmV3IERhdGUoZGF0YS5jcmVhdGVkX2F0KTtcbiAgICB9XG59XG4iLCJpbXBvcnQgeyBTdXBwcmVzc2lvbk1vZGVscyB9IGZyb20gJy4uLy4uL0VudW1zJztcbmltcG9ydCB7IElDb21wbGFpbnQgfSBmcm9tICcuLi8uLi9JbnRlcmZhY2VzL1N1cHByZXNzaW9ucyc7XG5pbXBvcnQgeyBDb21wbGFpbnREYXRhIH0gZnJvbSAnLi4vLi4vVHlwZXMvU3VwcHJlc3Npb25zJztcbmltcG9ydCBTdXBwcmVzc2lvbiBmcm9tICcuL1N1cHByZXNzaW9uJztcblxuZXhwb3J0IGRlZmF1bHQgY2xhc3MgQ29tcGxhaW50IGV4dGVuZHMgU3VwcHJlc3Npb24gaW1wbGVtZW50cyBJQ29tcGxhaW50IHtcbiAgICBhZGRyZXNzOiBzdHJpbmc7XG4gICAgLyogZXNsaW50LWRpc2FibGUgY2FtZWxjYXNlICovXG4gICAgY3JlYXRlZF9hdDogRGF0ZTtcbiAgICBjb25zdHJ1Y3RvcihkYXRhOiBDb21wbGFpbnREYXRhKSB7XG4gICAgICBzdXBlcihTdXBwcmVzc2lvbk1vZGVscy5DT01QTEFJTlRTKTtcbiAgICAgIHRoaXMuYWRkcmVzcyA9IGRhdGEuYWRkcmVzcztcbiAgICAgIHRoaXMuY3JlYXRlZF9hdCA9IG5ldyBEYXRlKGRhdGEuY3JlYXRlZF9hdCk7XG4gICAgfVxufVxuIiwiaW1wb3J0IHsgU3VwcHJlc3Npb25Nb2RlbHMgfSBmcm9tICcuLi8uLi9FbnVtcyc7XG5cbmV4cG9ydCBkZWZhdWx0IGNsYXNzIFN1cHByZXNzaW9uIHtcbiAgICB0eXBlOiBzdHJpbmc7XG4gICAgY29uc3RydWN0b3IodHlwZTogU3VwcHJlc3Npb25Nb2RlbHMpIHtcbiAgICAgIHRoaXMudHlwZSA9IHR5cGU7XG4gICAgfVxufVxuIiwiaW1wb3J0IHVybGpvaW4gZnJvbSAndXJsLWpvaW4nO1xuXG4vKiBlc2xpbnQtZGlzYWJsZSBjYW1lbGNhc2UgKi9cblxuaW1wb3J0IFJlcXVlc3QgZnJvbSAnLi4vY29tbW9uL1JlcXVlc3QnO1xuXG5pbXBvcnQgQVBJRXJyb3IgZnJvbSAnLi4vY29tbW9uL0Vycm9yJztcbmltcG9ydCBOYXZpZ2F0aW9uVGhydVBhZ2VzIGZyb20gJy4uL2NvbW1vbi9OYXZpZ2F0aW9uVGhydVBhZ2VzJztcbmltcG9ydCBCb3VuY2UgZnJvbSAnLi9Cb3VuY2UnO1xuaW1wb3J0IENvbXBsYWludCBmcm9tICcuL0NvbXBsYWludCc7XG5pbXBvcnQgVW5zdWJzY3JpYmUgZnJvbSAnLi9VbnN1YnNjcmliZSc7XG5pbXBvcnQgV2hpdGVMaXN0IGZyb20gJy4vV2hpdGVMaXN0JztcbmltcG9ydCBTdXBwcmVzc2lvbiBmcm9tICcuL1N1cHByZXNzaW9uJztcbmltcG9ydCB7XG4gIElCb3VuY2UsXG4gIElDb21wbGFpbnQsXG4gIElTdXBwcmVzc2lvbkNsaWVudCxcbiAgSVVuc3Vic2NyaWJlLFxuICBJV2hpdGVMaXN0XG59IGZyb20gJy4uLy4uL0ludGVyZmFjZXMvU3VwcHJlc3Npb25zJztcbmltcG9ydCB7XG4gIFN1cHByZXNzaW9uTGlzdCxcbiAgU3VwcHJlc3Npb25MaXN0UmVzcG9uc2UsXG4gIFN1cHByZXNzaW9uRGF0YVR5cGUsXG4gIFN1cHByZXNzaW9uQ3JlYXRpb25EYXRhLFxuICBTdXBwcmVzc2lvbkNyZWF0aW9uUmVzdWx0LFxuICBTdXBwcmVzc2lvbkNyZWF0aW9uUmVzcG9uc2UsXG4gIFN1cHByZXNzaW9uTGlzdFF1ZXJ5LFxuICBTdXBwcmVzc2lvblJlc3BvbnNlLFxuICBTdXBwcmVzc2lvbkRlc3Ryb3lSZXN1bHQsXG4gIFN1cHByZXNzaW9uRGVzdHJveVJlc3BvbnNlXG59IGZyb20gJy4uLy4uL1R5cGVzL1N1cHByZXNzaW9ucyc7XG5pbXBvcnQgeyBBUElFcnJvck9wdGlvbnMgfSBmcm9tICcuLi8uLi9UeXBlcy9Db21tb24nO1xuXG5jb25zdCBjcmVhdGVPcHRpb25zID0ge1xuICBoZWFkZXJzOiB7ICdDb250ZW50LVR5cGUnOiAnYXBwbGljYXRpb24vanNvbicgfVxufTtcblxuZXhwb3J0IGRlZmF1bHQgY2xhc3MgU3VwcHJlc3Npb25DbGllbnRcbiAgZXh0ZW5kcyBOYXZpZ2F0aW9uVGhydVBhZ2VzPFN1cHByZXNzaW9uTGlzdD5cbiAgaW1wbGVtZW50cyBJU3VwcHJlc3Npb25DbGllbnQge1xuICByZXF1ZXN0OiBSZXF1ZXN0O1xuICBtb2RlbHM6IG9iamVjdDtcblxuICBjb25zdHJ1Y3RvcihyZXF1ZXN0OiBSZXF1ZXN0KSB7XG4gICAgc3VwZXIocmVxdWVzdCk7XG4gICAgdGhpcy5yZXF1ZXN0ID0gcmVxdWVzdDtcbiAgICB0aGlzLm1vZGVscyA9IHtcbiAgICAgIGJvdW5jZXM6IEJvdW5jZSxcbiAgICAgIGNvbXBsYWludHM6IENvbXBsYWludCxcbiAgICAgIHVuc3Vic2NyaWJlczogVW5zdWJzY3JpYmUsXG4gICAgICB3aGl0ZWxpc3RzOiBXaGl0ZUxpc3QsXG4gICAgfTtcbiAgfVxuXG4gIHByb3RlY3RlZCBwYXJzZUxpc3QoXG4gICAgcmVzcG9uc2U6IFN1cHByZXNzaW9uTGlzdFJlc3BvbnNlLFxuICAgIE1vZGVsOiB7XG4gICAgICBuZXcoZGF0YTogU3VwcHJlc3Npb25EYXRhVHlwZSk6XG4gICAgICBJQm91bmNlIHwgSUNvbXBsYWludCB8IElVbnN1YnNjcmliZSB8IElXaGl0ZUxpc3RcbiAgICB9XG4gICk6IFN1cHByZXNzaW9uTGlzdCB7XG4gICAgY29uc3QgZGF0YSA9IHt9IGFzIFN1cHByZXNzaW9uTGlzdDtcbiAgICBkYXRhLml0ZW1zID0gcmVzcG9uc2UuYm9keS5pdGVtcz8ubWFwKChpdGVtKSA9PiBuZXcgTW9kZWwoaXRlbSkpIHx8IFtdO1xuXG4gICAgZGF0YS5wYWdlcyA9IHRoaXMucGFyc2VQYWdlTGlua3MocmVzcG9uc2UsICc/JywgJ2FkZHJlc3MnKTtcbiAgICBkYXRhLnN0YXR1cyA9IHJlc3BvbnNlLnN0YXR1cztcbiAgICByZXR1cm4gZGF0YTtcbiAgfVxuXG4gIF9wYXJzZUl0ZW08VCBleHRlbmRzIFN1cHByZXNzaW9uPihcbiAgICBkYXRhIDogU3VwcHJlc3Npb25EYXRhVHlwZSxcbiAgICBNb2RlbDoge1xuICAgICAgbmV3KGRhdGFUeXBlOiBTdXBwcmVzc2lvbkRhdGFUeXBlKTpUXG4gICAgfVxuICApOiBUIHtcbiAgICByZXR1cm4gbmV3IE1vZGVsKGRhdGEpO1xuICB9XG5cbiAgcHJpdmF0ZSBjcmVhdGVXaGl0ZUxpc3QoXG4gICAgZG9tYWluOiBzdHJpbmcsXG4gICAgZGF0YTogU3VwcHJlc3Npb25DcmVhdGlvbkRhdGEgfCBTdXBwcmVzc2lvbkNyZWF0aW9uRGF0YVtdLFxuICAgIGlzRGF0YUFycmF5OiBib29sZWFuXG4gICk6IFByb21pc2U8U3VwcHJlc3Npb25DcmVhdGlvblJlc3VsdD4ge1xuICAgIGlmIChpc0RhdGFBcnJheSkge1xuICAgICAgdGhyb3cgbmV3IEFQSUVycm9yKHtcbiAgICAgICAgc3RhdHVzOiA0MDAsXG4gICAgICAgIHN0YXR1c1RleHQ6ICdEYXRhIHByb3BlcnR5IHNob3VsZCBiZSBhbiBvYmplY3QnLFxuICAgICAgICBib2R5OiB7XG4gICAgICAgICAgbWVzc2FnZTogJ1doaXRlbGlzdFxcJ3MgY3JlYXRpb24gcHJvY2VzcyBkb2VzIG5vdCBzdXBwb3J0IG11bHRpcGxlIGNyZWF0aW9ucy4gRGF0YSBwcm9wZXJ0eSBzaG91bGQgYmUgYW4gb2JqZWN0J1xuICAgICAgICB9XG4gICAgICB9IGFzIEFQSUVycm9yT3B0aW9ucyk7XG4gICAgfVxuICAgIHJldHVybiB0aGlzLnJlcXVlc3RcbiAgICAgIC5wb3N0V2l0aEZEKHVybGpvaW4oJ3YzJywgZG9tYWluLCAnd2hpdGVsaXN0cycpLCBkYXRhKVxuICAgICAgLnRoZW4odGhpcy5wcmVwYXJlUmVzcG9uc2UpO1xuICB9XG5cbiAgcHJpdmF0ZSBjcmVhdGVVbnN1YnNjcmliZShcbiAgICBkb21haW46IHN0cmluZyxcbiAgICBkYXRhOiBTdXBwcmVzc2lvbkNyZWF0aW9uRGF0YSB8IFN1cHByZXNzaW9uQ3JlYXRpb25EYXRhW11cbiAgKTogUHJvbWlzZTxTdXBwcmVzc2lvbkNyZWF0aW9uUmVzdWx0PiB7XG4gICAgaWYgKEFycmF5LmlzQXJyYXkoZGF0YSkpIHsgLy8gVXNlciBwcm92aWRlZCBhbiBhcnJheVxuICAgICAgY29uc3QgaXNDb250YWluc1RhZyA9IGRhdGEuc29tZSgodW5zdWJzY3JpYmU6IFN1cHByZXNzaW9uQ3JlYXRpb25EYXRhKSA9PiB1bnN1YnNjcmliZS50YWcpO1xuICAgICAgaWYgKGlzQ29udGFpbnNUYWcpIHtcbiAgICAgICAgdGhyb3cgbmV3IEFQSUVycm9yKHtcbiAgICAgICAgICBzdGF0dXM6IDQwMCxcbiAgICAgICAgICBzdGF0dXNUZXh0OiAnVGFnIHByb3BlcnR5IHNob3VsZCBub3QgYmUgdXNlZCBmb3IgY3JlYXRpbmcgbXVsdGlwbGUgdW5zdWJzY3JpYmVzLicsXG4gICAgICAgICAgYm9keToge1xuICAgICAgICAgICAgbWVzc2FnZTogJ1RhZyBwcm9wZXJ0eSBjYW4gYmUgdXNlZCBvbmx5IGlmIG9uZSB1bnN1YnNjcmliZSBwcm92aWRlZCBhcyBzZWNvbmQgYXJndW1lbnQgb2YgY3JlYXRlIG1ldGhvZC4gUGxlYXNlIHVzZSB0YWdzIGluc3RlYWQuJ1xuICAgICAgICAgIH1cbiAgICAgICAgfSBhcyBBUElFcnJvck9wdGlvbnMpO1xuICAgICAgfVxuICAgICAgcmV0dXJuIHRoaXMucmVxdWVzdFxuICAgICAgICAucG9zdCh1cmxqb2luKCd2MycsIGRvbWFpbiwgJ3Vuc3Vic2NyaWJlcycpLCBKU09OLnN0cmluZ2lmeShkYXRhKSwgY3JlYXRlT3B0aW9ucylcbiAgICAgICAgLnRoZW4odGhpcy5wcmVwYXJlUmVzcG9uc2UpO1xuICAgIH1cblxuICAgIGlmIChkYXRhPy50YWdzKSB7XG4gICAgICB0aHJvdyBuZXcgQVBJRXJyb3Ioe1xuICAgICAgICBzdGF0dXM6IDQwMCxcbiAgICAgICAgc3RhdHVzVGV4dDogJ1RhZ3MgcHJvcGVydHkgc2hvdWxkIG5vdCBiZSB1c2VkIGZvciBjcmVhdGluZyBvbmUgdW5zdWJzY3JpYmUuJyxcbiAgICAgICAgYm9keToge1xuICAgICAgICAgIG1lc3NhZ2U6ICdUYWdzIHByb3BlcnR5IGNhbiBiZSB1c2VkIGlmIHlvdSBwcm92aWRlcyBhbiBhcnJheSBvZiB1bnN1YnNjcmliZXMgYXMgc2Vjb25kIGFyZ3VtZW50IG9mIGNyZWF0ZSBtZXRob2QuIFBsZWFzZSB1c2UgdGFnIGluc3RlYWQnXG4gICAgICAgIH1cbiAgICAgIH0gYXMgQVBJRXJyb3JPcHRpb25zKTtcbiAgICB9XG4gICAgaWYgKEFycmF5LmlzQXJyYXkoZGF0YS50YWcpKSB7XG4gICAgICB0aHJvdyBuZXcgQVBJRXJyb3Ioe1xuICAgICAgICBzdGF0dXM6IDQwMCxcbiAgICAgICAgc3RhdHVzVGV4dDogJ1RhZyBwcm9wZXJ0eSBjYW4gbm90IGJlIGFuIGFycmF5JyxcbiAgICAgICAgYm9keToge1xuICAgICAgICAgIG1lc3NhZ2U6ICdQbGVhc2UgdXNlIGFycmF5IG9mIHVuc3Vic2NyaWJlcyBhcyBzZWNvbmQgYXJndW1lbnQgb2YgY3JlYXRlIG1ldGhvZCB0byBiZSBhYmxlIHRvIHByb3ZpZGUgZmV3IHRhZ3MnXG4gICAgICAgIH1cbiAgICAgIH0gYXMgQVBJRXJyb3JPcHRpb25zKTtcbiAgICB9XG4gICAgLyogV2UgbmVlZCBGb3JtIERhdGEgZm9yIHVuc3Vic2NyaWJlcyBpZiB3ZSB3YW50IHRvIHN1cHBvcnQgdGhlIFwidGFnXCIgcHJvcGVydHkgKi9cbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0XG4gICAgICAucG9zdFdpdGhGRCh1cmxqb2luKCd2MycsIGRvbWFpbiwgJ3Vuc3Vic2NyaWJlcycpLCBkYXRhKVxuICAgICAgLnRoZW4odGhpcy5wcmVwYXJlUmVzcG9uc2UpO1xuICB9XG5cbiAgcHJpdmF0ZSBnZXRNb2RlbCh0eXBlOiBzdHJpbmcpIHtcbiAgICBpZiAodHlwZSBpbiB0aGlzLm1vZGVscykge1xuICAgICAgcmV0dXJuIHRoaXMubW9kZWxzW3R5cGUgYXMga2V5b2YgdHlwZW9mIHRoaXMubW9kZWxzXTtcbiAgICB9XG4gICAgdGhyb3cgbmV3IEFQSUVycm9yKHtcbiAgICAgIHN0YXR1czogNDAwLFxuICAgICAgc3RhdHVzVGV4dDogJ1Vua25vd24gdHlwZSB2YWx1ZScsXG4gICAgICBib2R5OiB7IG1lc3NhZ2U6ICdUeXBlIG1heSBiZSBvbmx5IG9uZSBvZiBbYm91bmNlcywgY29tcGxhaW50cywgdW5zdWJzY3JpYmVzLCB3aGl0ZWxpc3RzXScgfVxuICAgIH0gYXMgQVBJRXJyb3JPcHRpb25zKTtcbiAgfVxuXG4gIHByaXZhdGUgcHJlcGFyZVJlc3BvbnNlKHJlc3BvbnNlOiBTdXBwcmVzc2lvbkNyZWF0aW9uUmVzcG9uc2UpOiBTdXBwcmVzc2lvbkNyZWF0aW9uUmVzdWx0IHtcbiAgICByZXR1cm4ge1xuICAgICAgbWVzc2FnZTogcmVzcG9uc2UuYm9keS5tZXNzYWdlLFxuICAgICAgdHlwZTogcmVzcG9uc2UuYm9keS50eXBlIHx8ICcnLFxuICAgICAgdmFsdWU6IHJlc3BvbnNlLmJvZHkudmFsdWUgfHwgJycsXG4gICAgICBzdGF0dXM6IHJlc3BvbnNlLnN0YXR1c1xuICAgIH07XG4gIH1cblxuICBhc3luYyBsaXN0KFxuICAgIGRvbWFpbjogc3RyaW5nLFxuICAgIHR5cGU6IHN0cmluZyxcbiAgICBxdWVyeT86IFN1cHByZXNzaW9uTGlzdFF1ZXJ5XG4gICk6IFByb21pc2U8U3VwcHJlc3Npb25MaXN0PiB7XG4gICAgY29uc3QgbW9kZWwgPSB0aGlzLmdldE1vZGVsKHR5cGUpO1xuICAgIHJldHVybiB0aGlzLnJlcXVlc3RMaXN0V2l0aFBhZ2VzKHVybGpvaW4oJ3YzJywgZG9tYWluLCB0eXBlKSwgcXVlcnksIG1vZGVsKTtcbiAgfVxuXG4gIGdldChcbiAgICBkb21haW46IHN0cmluZyxcbiAgICB0eXBlOiBzdHJpbmcsXG4gICAgYWRkcmVzczogc3RyaW5nXG4gICk6IFByb21pc2U8SUJvdW5jZSB8IElDb21wbGFpbnQgfCBJVW5zdWJzY3JpYmUgfCBJV2hpdGVMaXN0PiB7XG4gICAgY29uc3QgbW9kZWwgPSB0aGlzLmdldE1vZGVsKHR5cGUpO1xuICAgIHJldHVybiB0aGlzLnJlcXVlc3RcbiAgICAgIC5nZXQodXJsam9pbigndjMnLCBkb21haW4sIHR5cGUsIGVuY29kZVVSSUNvbXBvbmVudChhZGRyZXNzKSkpXG4gICAgICAudGhlbigocmVzcG9uc2U6IFN1cHByZXNzaW9uUmVzcG9uc2UpID0+IHRoaXMuX3BhcnNlSXRlbTx0eXBlb2YgbW9kZWw+KHJlc3BvbnNlLmJvZHksIG1vZGVsKSk7XG4gIH1cblxuICBjcmVhdGUoXG4gICAgZG9tYWluOiBzdHJpbmcsXG4gICAgdHlwZTogc3RyaW5nLFxuICAgIGRhdGE6IFN1cHByZXNzaW9uQ3JlYXRpb25EYXRhIHwgU3VwcHJlc3Npb25DcmVhdGlvbkRhdGFbXVxuICApOiBQcm9taXNlPFN1cHByZXNzaW9uQ3JlYXRpb25SZXN1bHQ+IHtcbiAgICB0aGlzLmdldE1vZGVsKHR5cGUpO1xuICAgIC8vIHN1cHBvcnRzIGFkZGluZyBtdWx0aXBsZSBzdXBwcmVzc2lvbnMgYnkgZGVmYXVsdFxuICAgIGxldCBwb3N0RGF0YTtcbiAgICBjb25zdCBpc0RhdGFBcnJheSA9IEFycmF5LmlzQXJyYXkoZGF0YSk7XG5cbiAgICBpZiAodHlwZSA9PT0gJ3doaXRlbGlzdHMnKSB7XG4gICAgICByZXR1cm4gdGhpcy5jcmVhdGVXaGl0ZUxpc3QoZG9tYWluLCBkYXRhLCBpc0RhdGFBcnJheSk7XG4gICAgfVxuXG4gICAgaWYgKHR5cGUgPT09ICd1bnN1YnNjcmliZXMnKSB7XG4gICAgICByZXR1cm4gdGhpcy5jcmVhdGVVbnN1YnNjcmliZShkb21haW4sIGRhdGEpO1xuICAgIH1cblxuICAgIGlmICghaXNEYXRhQXJyYXkpIHtcbiAgICAgIHBvc3REYXRhID0gW2RhdGFdO1xuICAgIH0gZWxzZSB7XG4gICAgICBwb3N0RGF0YSA9IFsuLi5kYXRhXTtcbiAgICB9XG5cbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0XG4gICAgICAucG9zdCh1cmxqb2luKCd2MycsIGRvbWFpbiwgdHlwZSksIEpTT04uc3RyaW5naWZ5KHBvc3REYXRhKSwgY3JlYXRlT3B0aW9ucylcbiAgICAgIC50aGVuKHRoaXMucHJlcGFyZVJlc3BvbnNlKTtcbiAgfVxuXG4gIGRlc3Ryb3koXG4gICAgZG9tYWluOiBzdHJpbmcsXG4gICAgdHlwZTogc3RyaW5nLFxuICAgIGFkZHJlc3M6IHN0cmluZ1xuICApOiBQcm9taXNlPFN1cHByZXNzaW9uRGVzdHJveVJlc3VsdD4ge1xuICAgIHRoaXMuZ2V0TW9kZWwodHlwZSk7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdFxuICAgICAgLmRlbGV0ZSh1cmxqb2luKCd2MycsIGRvbWFpbiwgdHlwZSwgZW5jb2RlVVJJQ29tcG9uZW50KGFkZHJlc3MpKSlcbiAgICAgIC50aGVuKChyZXNwb25zZTogU3VwcHJlc3Npb25EZXN0cm95UmVzcG9uc2UpID0+ICh7XG4gICAgICAgIG1lc3NhZ2U6IHJlc3BvbnNlLmJvZHkubWVzc2FnZSxcbiAgICAgICAgdmFsdWU6IHJlc3BvbnNlLmJvZHkudmFsdWUgfHwgJycsXG4gICAgICAgIGFkZHJlc3M6IHJlc3BvbnNlLmJvZHkuYWRkcmVzcyB8fCAnJyxcbiAgICAgICAgc3RhdHVzOiByZXNwb25zZS5zdGF0dXNcbiAgICAgIH0pKTtcbiAgfVxufVxuXG5tb2R1bGUuZXhwb3J0cyA9IFN1cHByZXNzaW9uQ2xpZW50O1xuIiwiaW1wb3J0IHsgU3VwcHJlc3Npb25Nb2RlbHMgfSBmcm9tICcuLi8uLi9FbnVtcyc7XG5pbXBvcnQgeyBJVW5zdWJzY3JpYmUgfSBmcm9tICcuLi8uLi9JbnRlcmZhY2VzL1N1cHByZXNzaW9ucyc7XG5pbXBvcnQgeyBVbnN1YnNjcmliZURhdGEgfSBmcm9tICcuLi8uLi9UeXBlcy9TdXBwcmVzc2lvbnMnO1xuXG5pbXBvcnQgU3VwcHJlc3Npb24gZnJvbSAnLi9TdXBwcmVzc2lvbic7XG5cbmV4cG9ydCBkZWZhdWx0IGNsYXNzIFVuc3Vic2NyaWJlIGV4dGVuZHMgU3VwcHJlc3Npb24gaW1wbGVtZW50cyBJVW5zdWJzY3JpYmUge1xuICAgIGFkZHJlc3M6IHN0cmluZztcbiAgICB0YWdzOiBzdHJpbmdbXTtcbiAgICAvKiBlc2xpbnQtZGlzYWJsZSBjYW1lbGNhc2UgKi9cbiAgICBjcmVhdGVkX2F0OiBEYXRlO1xuXG4gICAgY29uc3RydWN0b3IoZGF0YTogVW5zdWJzY3JpYmVEYXRhKSB7XG4gICAgICBzdXBlcihTdXBwcmVzc2lvbk1vZGVscy5VTlNVQlNDUklCRVMpO1xuICAgICAgdGhpcy5hZGRyZXNzID0gZGF0YS5hZGRyZXNzO1xuICAgICAgdGhpcy50YWdzID0gZGF0YS50YWdzO1xuICAgICAgdGhpcy5jcmVhdGVkX2F0ID0gbmV3IERhdGUoZGF0YS5jcmVhdGVkX2F0KTtcbiAgICB9XG59XG4iLCJpbXBvcnQgeyBTdXBwcmVzc2lvbk1vZGVscyB9IGZyb20gJy4uLy4uL0VudW1zJztcbmltcG9ydCB7IElXaGl0ZUxpc3QgfSBmcm9tICcuLi8uLi9JbnRlcmZhY2VzL1N1cHByZXNzaW9ucyc7XG5pbXBvcnQgeyBXaGl0ZUxpc3REYXRhIH0gZnJvbSAnLi4vLi4vVHlwZXMvU3VwcHJlc3Npb25zJztcbmltcG9ydCBTdXBwcmVzc2lvbiBmcm9tICcuL1N1cHByZXNzaW9uJztcblxuZXhwb3J0IGRlZmF1bHQgY2xhc3MgV2hpdGVMaXN0IGV4dGVuZHMgU3VwcHJlc3Npb24gaW1wbGVtZW50cyBJV2hpdGVMaXN0IHtcbiAgICB2YWx1ZTogc3RyaW5nO1xuICAgIHJlYXNvbjogc3RyaW5nO1xuICAgIGNyZWF0ZWRBdDogRGF0ZTtcblxuICAgIGNvbnN0cnVjdG9yKGRhdGE6IFdoaXRlTGlzdERhdGEpIHtcbiAgICAgIHN1cGVyKFN1cHByZXNzaW9uTW9kZWxzLldISVRFTElTVFMpO1xuICAgICAgdGhpcy52YWx1ZSA9IGRhdGEudmFsdWU7XG4gICAgICB0aGlzLnJlYXNvbiA9IGRhdGEucmVhc29uO1xuICAgICAgdGhpcy5jcmVhdGVkQXQgPSBuZXcgRGF0ZShkYXRhLmNyZWF0ZWRBdCk7XG4gICAgfVxufVxuIiwiaW1wb3J0IE5hdmlnYXRpb25UaHJ1UGFnZXMgZnJvbSAnLi4vY29tbW9uL05hdmlnYXRpb25UaHJ1UGFnZXMnO1xuaW1wb3J0IHsgQVBJUmVzcG9uc2UgfSBmcm9tICcuLi8uLi9UeXBlcy9Db21tb24vQXBpUmVzcG9uc2UnO1xuXG5pbXBvcnQgUmVxdWVzdCBmcm9tICcuLi9jb21tb24vUmVxdWVzdCc7XG5pbXBvcnQgeyBJTXVsdGlwbGVWYWxpZGF0aW9uQ2xpZW50IH0gZnJvbSAnLi4vLi4vSW50ZXJmYWNlcy9WYWxpZGF0aW9ucyc7XG5pbXBvcnQge1xuICBNdWx0aXBsZVZhbGlkYXRpb25Kb2JSZXN1bHQsXG4gIE11bHRpcGxlVmFsaWRhdGlvbkpvYkRhdGEsXG4gIE11bHRpcGxlVmFsaWRhdGlvbkpvYnNMaXN0UmVzdWx0LFxuICBNdWx0aXBsZVZhbGlkYXRpb25Kb2JzTGlzdFJlc3BvbnNlLFxuICBNdWx0aXBsZVZhbGlkYXRpb25Kb2JzTGlzdFF1ZXJ5LFxuICBNdWx0aXBsZVZhbGlkYXRpb25DcmVhdGlvbkRhdGEsXG4gIENyZWF0ZWRNdWx0aXBsZVZhbGlkYXRpb25Kb2IsXG4gIE11bHRpcGxlVmFsaWRhdGlvbkNyZWF0aW9uRGF0YVVwZGF0ZWQsXG4gIENhbmNlbGVkTXVsdGlwbGVWYWxpZGF0aW9uSm9iXG59IGZyb20gJy4uLy4uL1R5cGVzL1ZhbGlkYXRpb25zL011bHRpcGxlVmFsaWRhdGlvbic7XG5cbmV4cG9ydCBjbGFzcyBNdWx0aXBsZVZhbGlkYXRpb25Kb2IgaW1wbGVtZW50cyBNdWx0aXBsZVZhbGlkYXRpb25Kb2JSZXN1bHQge1xuICBjcmVhdGVkQXQ6IERhdGU7XG4gIGlkOiBzdHJpbmc7XG4gIHF1YW50aXR5OiBudW1iZXJcbiAgcmVjb3Jkc1Byb2Nlc3NlZDogbnVtYmVyIHwgbnVsbDtcbiAgc3RhdHVzOiBzdHJpbmc7XG4gIGRvd25sb2FkVXJsPzoge1xuICAgIGNzdjogc3RyaW5nO1xuICAgIGpzb246IHN0cmluZztcbiAgfTtcblxuICByZXNwb25zZVN0YXR1c0NvZGU6IG51bWJlcjtcbiAgc3VtbWFyeT86IHtcbiAgICAgIHJlc3VsdDoge1xuICAgICAgICAgIGNhdGNoQWxsOiBudW1iZXI7XG4gICAgICAgICAgZGVsaXZlcmFibGU6IG51bWJlcjtcbiAgICAgICAgICBkb05vdFNlbmQ6IG51bWJlcjtcbiAgICAgICAgICB1bmRlbGl2ZXJhYmxlOiBudW1iZXI7XG4gICAgICAgICAgdW5rbm93bjogbnVtYmVyO1xuICAgICAgfTtcbiAgICAgIHJpc2s6IHtcbiAgICAgICAgICBoaWdoOiBudW1iZXI7XG4gICAgICAgICAgbG93OiBudW1iZXI7XG4gICAgICAgICAgbWVkaXVtOiBudW1iZXI7XG4gICAgICAgICAgdW5rbm93bjogbnVtYmVyO1xuICAgICAgfVxuICB9XG5cbiAgY29uc3RydWN0b3IoZGF0YTogTXVsdGlwbGVWYWxpZGF0aW9uSm9iRGF0YSwgcmVzcG9uc2VTdGF0dXNDb2RlOiBudW1iZXIpIHtcbiAgICB0aGlzLmNyZWF0ZWRBdCA9IG5ldyBEYXRlKGRhdGEuY3JlYXRlZF9hdCk7XG4gICAgdGhpcy5pZCA9IGRhdGEuaWQ7XG4gICAgdGhpcy5xdWFudGl0eSA9IGRhdGEucXVhbnRpdHk7XG4gICAgdGhpcy5yZWNvcmRzUHJvY2Vzc2VkID0gZGF0YS5yZWNvcmRzX3Byb2Nlc3NlZDtcbiAgICB0aGlzLnN0YXR1cyA9IGRhdGEuc3RhdHVzO1xuICAgIHRoaXMucmVzcG9uc2VTdGF0dXNDb2RlID0gcmVzcG9uc2VTdGF0dXNDb2RlO1xuICAgIGlmIChkYXRhLmRvd25sb2FkX3VybCkge1xuICAgICAgdGhpcy5kb3dubG9hZFVybCA9IHtcbiAgICAgICAgY3N2OiBkYXRhLmRvd25sb2FkX3VybD8uY3N2LFxuICAgICAgICBqc29uOiBkYXRhLmRvd25sb2FkX3VybD8uanNvblxuICAgICAgfTtcbiAgICB9XG4gICAgaWYgKGRhdGEuc3VtbWFyeSkge1xuICAgICAgdGhpcy5zdW1tYXJ5ID0ge1xuICAgICAgICByZXN1bHQ6IHtcbiAgICAgICAgICBjYXRjaEFsbDogZGF0YS5zdW1tYXJ5LnJlc3VsdC5jYXRjaF9hbGwsXG4gICAgICAgICAgZGVsaXZlcmFibGU6IGRhdGEuc3VtbWFyeS5yZXN1bHQuZGVsaXZlcmFibGUsXG4gICAgICAgICAgZG9Ob3RTZW5kOiBkYXRhLnN1bW1hcnkucmVzdWx0LmRvX25vdF9zZW5kLFxuICAgICAgICAgIHVuZGVsaXZlcmFibGU6IGRhdGEuc3VtbWFyeS5yZXN1bHQudW5kZWxpdmVyYWJsZSxcbiAgICAgICAgICB1bmtub3duOiBkYXRhLnN1bW1hcnkucmVzdWx0LnVua25vd25cbiAgICAgICAgfSxcbiAgICAgICAgcmlzazoge1xuICAgICAgICAgIGhpZ2g6IGRhdGEuc3VtbWFyeS5yaXNrLmhpZ2gsXG4gICAgICAgICAgbG93OiBkYXRhLnN1bW1hcnkucmlzay5sb3csXG4gICAgICAgICAgbWVkaXVtOiBkYXRhLnN1bW1hcnkucmlzay5tZWRpdW0sXG4gICAgICAgICAgdW5rbm93bjogZGF0YS5zdW1tYXJ5LnJpc2sudW5rbm93blxuICAgICAgICB9XG4gICAgICB9O1xuICAgIH1cbiAgfVxufVxuXG5leHBvcnQgZGVmYXVsdCBjbGFzcyBNdWx0aXBsZVZhbGlkYXRpb25DbGllbnRcbiAgZXh0ZW5kcyBOYXZpZ2F0aW9uVGhydVBhZ2VzPE11bHRpcGxlVmFsaWRhdGlvbkpvYnNMaXN0UmVzdWx0PlxuICBpbXBsZW1lbnRzIElNdWx0aXBsZVZhbGlkYXRpb25DbGllbnQge1xuICByZXF1ZXN0OiBSZXF1ZXN0O1xuXG4gIGNvbnN0cnVjdG9yKHJlcXVlc3Q6IFJlcXVlc3QpIHtcbiAgICBzdXBlcigpO1xuICAgIHRoaXMucmVxdWVzdCA9IHJlcXVlc3Q7XG4gIH1cblxuICBwcml2YXRlIGhhbmRsZVJlc3BvbnNlPFQ+KHJlc3BvbnNlOiBBUElSZXNwb25zZSk6IFQge1xuICAgIHJldHVybiB7XG4gICAgICBzdGF0dXM6IHJlc3BvbnNlLnN0YXR1cyxcbiAgICAgIC4uLnJlc3BvbnNlPy5ib2R5XG4gICAgfSBhcyBUO1xuICB9XG5cbiAgcHJvdGVjdGVkIHBhcnNlTGlzdChyZXNwb25zZTogTXVsdGlwbGVWYWxpZGF0aW9uSm9ic0xpc3RSZXNwb25zZSlcbiAgICA6IE11bHRpcGxlVmFsaWRhdGlvbkpvYnNMaXN0UmVzdWx0IHtcbiAgICBjb25zdCBkYXRhID0ge30gYXMgTXVsdGlwbGVWYWxpZGF0aW9uSm9ic0xpc3RSZXN1bHQ7XG5cbiAgICBkYXRhLmpvYnMgPSByZXNwb25zZS5ib2R5LmpvYnMubWFwKChqb2IpID0+IG5ldyBNdWx0aXBsZVZhbGlkYXRpb25Kb2Ioam9iLCByZXNwb25zZS5zdGF0dXMpKTtcblxuICAgIGRhdGEucGFnZXMgPSB0aGlzLnBhcnNlUGFnZUxpbmtzKHJlc3BvbnNlLCAnPycsICdwaXZvdCcpO1xuICAgIGRhdGEudG90YWwgPSByZXNwb25zZS5ib2R5LnRvdGFsO1xuICAgIGRhdGEuc3RhdHVzID0gcmVzcG9uc2Uuc3RhdHVzO1xuXG4gICAgcmV0dXJuIGRhdGE7XG4gIH1cblxuICBhc3luYyBsaXN0KHF1ZXJ5PzogTXVsdGlwbGVWYWxpZGF0aW9uSm9ic0xpc3RRdWVyeSk6IFByb21pc2U8TXVsdGlwbGVWYWxpZGF0aW9uSm9ic0xpc3RSZXN1bHQ+IHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0TGlzdFdpdGhQYWdlcygnL3Y0L2FkZHJlc3MvdmFsaWRhdGUvYnVsaycsIHF1ZXJ5KTtcbiAgfVxuXG4gIGFzeW5jIGdldChsaXN0SWQ6IHN0cmluZyk6IFByb21pc2U8TXVsdGlwbGVWYWxpZGF0aW9uSm9iPiB7XG4gICAgY29uc3QgcmVzcG9uc2UgPSBhd2FpdCB0aGlzLnJlcXVlc3QuZ2V0KGAvdjQvYWRkcmVzcy92YWxpZGF0ZS9idWxrLyR7bGlzdElkfWApO1xuICAgIHJldHVybiBuZXcgTXVsdGlwbGVWYWxpZGF0aW9uSm9iKHJlc3BvbnNlLmJvZHksIHJlc3BvbnNlLnN0YXR1cyk7XG4gIH1cblxuICBhc3luYyBjcmVhdGUoXG4gICAgbGlzdElkOiBzdHJpbmcsXG4gICAgZGF0YTogTXVsdGlwbGVWYWxpZGF0aW9uQ3JlYXRpb25EYXRhXG4gICk6IFByb21pc2U8Q3JlYXRlZE11bHRpcGxlVmFsaWRhdGlvbkpvYj4ge1xuICAgIGNvbnN0IG11bHRpcGxlVmFsaWRhdGlvbkRhdGE6IE11bHRpcGxlVmFsaWRhdGlvbkNyZWF0aW9uRGF0YVVwZGF0ZWQgPSB7XG4gICAgICBtdWx0aXBsZVZhbGlkYXRpb25GaWxlOiB7XG4gICAgICAgIC4uLmRhdGE/LmZpbGVcbiAgICAgIH0sXG4gICAgICAuLi5kYXRhXG4gICAgfTtcbiAgICBkZWxldGUgbXVsdGlwbGVWYWxpZGF0aW9uRGF0YS5maWxlO1xuICAgIGNvbnN0IHJlc3BvbnNlID0gYXdhaXQgdGhpcy5yZXF1ZXN0LnBvc3RXaXRoRkQoYC92NC9hZGRyZXNzL3ZhbGlkYXRlL2J1bGsvJHtsaXN0SWR9YCwgbXVsdGlwbGVWYWxpZGF0aW9uRGF0YSk7XG4gICAgcmV0dXJuIHRoaXMuaGFuZGxlUmVzcG9uc2U8Q3JlYXRlZE11bHRpcGxlVmFsaWRhdGlvbkpvYj4ocmVzcG9uc2UpO1xuICB9XG5cbiAgYXN5bmMgZGVzdHJveShsaXN0SWQ6IHN0cmluZyk6IFByb21pc2U8Q2FuY2VsZWRNdWx0aXBsZVZhbGlkYXRpb25Kb2I+IHtcbiAgICBjb25zdCByZXNwb25zZSA9IGF3YWl0IHRoaXMucmVxdWVzdC5kZWxldGUoYC92NC9hZGRyZXNzL3ZhbGlkYXRlL2J1bGsvJHtsaXN0SWR9YCk7XG4gICAgcmV0dXJuIHRoaXMuaGFuZGxlUmVzcG9uc2U8Q2FuY2VsZWRNdWx0aXBsZVZhbGlkYXRpb25Kb2I+KHJlc3BvbnNlKTtcbiAgfVxufVxuIiwiaW1wb3J0IHsgSVZhbGlkYXRpb25DbGllbnQsIElNdWx0aXBsZVZhbGlkYXRpb25DbGllbnQgfSBmcm9tICcuLi8uLi9JbnRlcmZhY2VzL1ZhbGlkYXRpb25zJztcbmltcG9ydCB7IFZhbGlkYXRpb25RdWVyeSwgVmFsaWRhdGlvblJlc3VsdCwgVmFsaWRhdGlvblJlc3BvbnNlIH0gZnJvbSAnLi4vLi4vVHlwZXMvVmFsaWRhdGlvbnMnO1xuaW1wb3J0IFJlcXVlc3QgZnJvbSAnLi4vY29tbW9uL1JlcXVlc3QnO1xuXG5leHBvcnQgZGVmYXVsdCBjbGFzcyBWYWxpZGF0ZUNsaWVudCBpbXBsZW1lbnRzIElWYWxpZGF0aW9uQ2xpZW50IHtcbiAgcHVibGljIG11bHRpcGxlVmFsaWRhdGlvbjtcbiAgcmVxdWVzdDogUmVxdWVzdDtcblxuICBjb25zdHJ1Y3RvcihyZXF1ZXN0OiBSZXF1ZXN0LCBtdWx0aXBsZVZhbGlkYXRpb25DbGllbnQ6IElNdWx0aXBsZVZhbGlkYXRpb25DbGllbnQpIHtcbiAgICB0aGlzLnJlcXVlc3QgPSByZXF1ZXN0O1xuICAgIHRoaXMubXVsdGlwbGVWYWxpZGF0aW9uID0gbXVsdGlwbGVWYWxpZGF0aW9uQ2xpZW50O1xuICB9XG5cbiAgYXN5bmMgZ2V0KGFkZHJlc3M6IHN0cmluZyk6IFByb21pc2U8VmFsaWRhdGlvblJlc3VsdD4ge1xuICAgIGNvbnN0IHF1ZXJ5OiBWYWxpZGF0aW9uUXVlcnkgPSB7IGFkZHJlc3MgfTtcbiAgICBjb25zdCByZXN1bHQ6IFZhbGlkYXRpb25SZXNwb25zZSA9IGF3YWl0IHRoaXMucmVxdWVzdC5nZXQoJy92NC9hZGRyZXNzL3ZhbGlkYXRlJywgcXVlcnkpO1xuICAgIHJldHVybiByZXN1bHQuYm9keSBhcyBWYWxpZGF0aW9uUmVzdWx0O1xuICB9XG59XG4iLCJpbXBvcnQgdXJsam9pbiBmcm9tICd1cmwtam9pbic7XG5pbXBvcnQgeyBXZWJob29rc0lkcyB9IGZyb20gJy4uL0VudW1zJztcbmltcG9ydCB7IElXZWJIb29rc0NsaWVudCB9IGZyb20gJy4uL0ludGVyZmFjZXMvV2ViaG9va3MnO1xuXG5pbXBvcnQge1xuICBXZWJob29rVmFsaWRhdGlvblJlc3BvbnNlLFxuICBXZWJob29rTGlzdCxcbiAgV2ViaG9va1Jlc3BvbnNlLFxuICBXZWJob29rc1F1ZXJ5LFxuICBXZWJob29rUmVzdWx0XG59IGZyb20gJy4uL1R5cGVzL1dlYmhvb2tzJztcbmltcG9ydCBSZXF1ZXN0IGZyb20gJy4vY29tbW9uL1JlcXVlc3QnO1xuXG5leHBvcnQgY2xhc3MgV2ViaG9vayBpbXBsZW1lbnRzIFdlYmhvb2tSZXN1bHQge1xuICBpZDogc3RyaW5nO1xuICB1cmw6IHN0cmluZyB8IHVuZGVmaW5lZDtcbiAgdXJsczogc3RyaW5nW107XG5cbiAgY29uc3RydWN0b3IoaWQ6IHN0cmluZywgdXJsOiBzdHJpbmcgfCB1bmRlZmluZWQsIHVybHM6IHN0cmluZ1tdKSB7XG4gICAgdGhpcy5pZCA9IGlkO1xuICAgIHRoaXMudXJsID0gdXJsO1xuICAgIHRoaXMudXJscyA9IHVybHM7XG4gIH1cbn1cblxuZXhwb3J0IGRlZmF1bHQgY2xhc3MgV2ViaG9va3NDbGllbnQgaW1wbGVtZW50cyBJV2ViSG9va3NDbGllbnQge1xuICByZXF1ZXN0OiBSZXF1ZXN0O1xuXG4gIGNvbnN0cnVjdG9yKHJlcXVlc3Q6IFJlcXVlc3QpIHtcbiAgICB0aGlzLnJlcXVlc3QgPSByZXF1ZXN0O1xuICB9XG5cbiAgcHJpdmF0ZSBfcGFyc2VXZWJob29rTGlzdChyZXNwb25zZTogeyBib2R5OiB7IHdlYmhvb2tzOiBXZWJob29rTGlzdCB9IH0pOiBXZWJob29rTGlzdCB7XG4gICAgcmV0dXJuIHJlc3BvbnNlLmJvZHkud2ViaG9va3M7XG4gIH1cblxuICBfcGFyc2VXZWJob29rV2l0aElEKGlkOiBzdHJpbmcpIHtcbiAgICByZXR1cm4gZnVuY3Rpb24gKHJlc3BvbnNlOiBXZWJob29rUmVzcG9uc2UpOiBXZWJob29rUmVzdWx0IHtcbiAgICAgIGNvbnN0IHdlYmhvb2tSZXNwb25zZSA9IHJlc3BvbnNlPy5ib2R5Py53ZWJob29rO1xuICAgICAgbGV0IHVybCA9IHdlYmhvb2tSZXNwb25zZT8udXJsO1xuICAgICAgbGV0IHVybHMgPSB3ZWJob29rUmVzcG9uc2U/LnVybHM7XG4gICAgICBpZiAoIXVybCkge1xuICAgICAgICB1cmwgPSB1cmxzICYmIHVybHMubGVuZ3RoXG4gICAgICAgICAgPyB1cmxzWzBdXG4gICAgICAgICAgOiB1bmRlZmluZWQ7XG4gICAgICB9XG4gICAgICBpZiAoKCF1cmxzIHx8IHVybHMubGVuZ3RoID09PSAwKSAmJiB1cmwpIHtcbiAgICAgICAgdXJscyA9IFt1cmxdO1xuICAgICAgfVxuICAgICAgcmV0dXJuIG5ldyBXZWJob29rKGlkLCB1cmwsIHVybHMgYXMgc3RyaW5nW10pO1xuICAgIH07XG4gIH1cblxuICBwcml2YXRlIF9wYXJzZVdlYmhvb2tUZXN0KHJlc3BvbnNlOiB7IGJvZHk6IHsgY29kZTogbnVtYmVyLCBtZXNzYWdlOiBzdHJpbmcgfSB9KVxuICA6IHtjb2RlOiBudW1iZXIsIG1lc3NhZ2U6c3RyaW5nfSB7XG4gICAgcmV0dXJuIHtcbiAgICAgIGNvZGU6IHJlc3BvbnNlLmJvZHkuY29kZSxcbiAgICAgIG1lc3NhZ2U6IHJlc3BvbnNlLmJvZHkubWVzc2FnZVxuICAgIH0gYXMgV2ViaG9va1ZhbGlkYXRpb25SZXNwb25zZTtcbiAgfVxuXG4gIGxpc3QoZG9tYWluOiBzdHJpbmcsIHF1ZXJ5OiBXZWJob29rc1F1ZXJ5KTogUHJvbWlzZTxXZWJob29rTGlzdD4ge1xuICAgIHJldHVybiB0aGlzLnJlcXVlc3QuZ2V0KHVybGpvaW4oJy92My9kb21haW5zJywgZG9tYWluLCAnd2ViaG9va3MnKSwgcXVlcnkpXG4gICAgICAudGhlbih0aGlzLl9wYXJzZVdlYmhvb2tMaXN0KTtcbiAgfVxuXG4gIGdldChkb21haW46IHN0cmluZywgaWQ6IFdlYmhvb2tzSWRzKTogUHJvbWlzZTxXZWJob29rUmVzdWx0PiB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5nZXQodXJsam9pbignL3YzL2RvbWFpbnMnLCBkb21haW4sICd3ZWJob29rcycsIGlkKSlcbiAgICAgIC50aGVuKHRoaXMuX3BhcnNlV2ViaG9va1dpdGhJRChpZCkpO1xuICB9XG5cbiAgY3JlYXRlKGRvbWFpbjogc3RyaW5nLFxuICAgIGlkOiBzdHJpbmcsXG4gICAgdXJsOiBzdHJpbmcsXG4gICAgdGVzdCA9IGZhbHNlKTogUHJvbWlzZTxXZWJob29rUmVzdWx0IHwgV2ViaG9va1ZhbGlkYXRpb25SZXNwb25zZT4ge1xuICAgIGlmICh0ZXN0KSB7XG4gICAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LnB1dFdpdGhGRCh1cmxqb2luKCcvdjMvZG9tYWlucycsIGRvbWFpbiwgJ3dlYmhvb2tzJywgaWQsICd0ZXN0JyksIHsgdXJsIH0pXG4gICAgICAgIC50aGVuKHRoaXMuX3BhcnNlV2ViaG9va1Rlc3QpO1xuICAgIH1cblxuICAgIHJldHVybiB0aGlzLnJlcXVlc3QucG9zdFdpdGhGRCh1cmxqb2luKCcvdjMvZG9tYWlucycsIGRvbWFpbiwgJ3dlYmhvb2tzJyksIHsgaWQsIHVybCB9KVxuICAgICAgLnRoZW4odGhpcy5fcGFyc2VXZWJob29rV2l0aElEKGlkKSk7XG4gIH1cblxuICB1cGRhdGUoZG9tYWluOiBzdHJpbmcsIGlkOiBzdHJpbmcsIHVybFZhbHVlczogc3RyaW5nIHwgc3RyaW5nW10pOiBQcm9taXNlPFdlYmhvb2tSZXN1bHQ+IHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LnB1dFdpdGhGRCh1cmxqb2luKCcvdjMvZG9tYWlucycsIGRvbWFpbiwgJ3dlYmhvb2tzJywgaWQpLCB7IHVybDogdXJsVmFsdWVzIH0pXG4gICAgICAudGhlbih0aGlzLl9wYXJzZVdlYmhvb2tXaXRoSUQoaWQpKTtcbiAgfVxuXG4gIGRlc3Ryb3koZG9tYWluOiBzdHJpbmcsIGlkOiBzdHJpbmcpIDogUHJvbWlzZTxXZWJob29rUmVzdWx0PiB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5kZWxldGUodXJsam9pbignL3YzL2RvbWFpbnMnLCBkb21haW4sICd3ZWJob29rcycsIGlkKSlcbiAgICAgIC50aGVuKHRoaXMuX3BhcnNlV2ViaG9va1dpdGhJRChpZCkpO1xuICB9XG59XG4iLCJpbXBvcnQgeyBBUElFcnJvck9wdGlvbnMsIEFQSUVycm9yVHlwZSB9IGZyb20gJy4uLy4uL1R5cGVzL0NvbW1vbic7XG5cbmV4cG9ydCBkZWZhdWx0IGNsYXNzIEFQSUVycm9yIGV4dGVuZHMgRXJyb3IgaW1wbGVtZW50cyBBUElFcnJvclR5cGUge1xuICBwdWJsaWMgc3RhdHVzOiBudW1iZXIgO1xuICBwdWJsaWMgc3RhY2s6IHN0cmluZztcbiAgcHVibGljIGRldGFpbHM6IHN0cmluZztcbiAgcHVibGljIHR5cGU6IHN0cmluZztcblxuICBjb25zdHJ1Y3Rvcih7XG4gICAgc3RhdHVzLFxuICAgIHN0YXR1c1RleHQsXG4gICAgbWVzc2FnZSxcbiAgICBib2R5ID0ge31cbiAgfTogQVBJRXJyb3JPcHRpb25zKSB7XG4gICAgbGV0IGJvZHlNZXNzYWdlID0gJyc7XG4gICAgbGV0IGVycm9yID0gJyc7XG4gICAgaWYgKHR5cGVvZiBib2R5ID09PSAnc3RyaW5nJykge1xuICAgICAgYm9keU1lc3NhZ2UgPSBib2R5O1xuICAgIH0gZWxzZSB7XG4gICAgICBib2R5TWVzc2FnZSA9IGJvZHk/Lm1lc3NhZ2UgfHwgJyc7XG4gICAgICBlcnJvciA9IGJvZHk/LmVycm9yIHx8ICcnO1xuICAgIH1cbiAgICBzdXBlcigpO1xuXG4gICAgdGhpcy5zdGFjayA9ICcnO1xuICAgIHRoaXMuc3RhdHVzID0gc3RhdHVzO1xuICAgIHRoaXMubWVzc2FnZSA9IG1lc3NhZ2UgfHwgZXJyb3IgfHwgc3RhdHVzVGV4dCB8fCAnJztcbiAgICB0aGlzLmRldGFpbHMgPSBib2R5TWVzc2FnZTtcbiAgICB0aGlzLnR5cGUgPSAnTWFpbGd1bkFQSUVycm9yJztcbiAgfVxufVxuIiwiaW1wb3J0ICogYXMgTm9kZUZvcm1EYXRhIGZyb20gJ2Zvcm0tZGF0YSc7XG5pbXBvcnQgeyBBUElFcnJvck9wdGlvbnMsIElucHV0Rm9ybURhdGEgfSBmcm9tICcuLi8uLi9UeXBlcy9Db21tb24nO1xuaW1wb3J0IEFQSUVycm9yIGZyb20gJy4vRXJyb3InO1xuXG5jbGFzcyBGb3JtRGF0YUJ1aWxkZXIge1xuICBwcml2YXRlIEZvcm1EYXRhQ29uc3RydWN0b3I6IElucHV0Rm9ybURhdGE7XG4gIGNvbnN0cnVjdG9yKEZvcm1EYXRhQ29uc3RydWN0b3I6IElucHV0Rm9ybURhdGEpIHtcbiAgICB0aGlzLkZvcm1EYXRhQ29uc3RydWN0b3IgPSBGb3JtRGF0YUNvbnN0cnVjdG9yO1xuICB9XG5cbiAgcHVibGljIGNyZWF0ZUZvcm1EYXRhKGRhdGE6IGFueSk6IE5vZGVGb3JtRGF0YSB8IEZvcm1EYXRhIHtcbiAgICBpZiAoIWRhdGEpIHtcbiAgICAgIHRocm93IG5ldyBFcnJvcignUGxlYXNlIHByb3ZpZGUgZGF0YSBvYmplY3QnKTtcbiAgICB9XG4gICAgY29uc3QgZm9ybURhdGE6IE5vZGVGb3JtRGF0YSB8IEZvcm1EYXRhID0gT2JqZWN0LmtleXMoZGF0YSlcbiAgICAgIC5maWx0ZXIoZnVuY3Rpb24gKGtleSkgeyByZXR1cm4gZGF0YVtrZXldOyB9KVxuICAgICAgLnJlZHVjZSgoZm9ybURhdGFBY2M6IE5vZGVGb3JtRGF0YSB8IEZvcm1EYXRhLCBrZXkpID0+IHtcbiAgICAgICAgY29uc3QgZmlsZUtleXMgPSBbJ2F0dGFjaG1lbnQnLCAnaW5saW5lJywgJ211bHRpcGxlVmFsaWRhdGlvbkZpbGUnXTtcbiAgICAgICAgaWYgKGZpbGVLZXlzLmluY2x1ZGVzKGtleSkpIHtcbiAgICAgICAgICB0aGlzLmFkZEZpbGVzVG9GRChrZXksIGRhdGFba2V5XSwgZm9ybURhdGFBY2MpO1xuICAgICAgICAgIHJldHVybiBmb3JtRGF0YUFjYztcbiAgICAgICAgfVxuXG4gICAgICAgIGlmIChrZXkgPT09ICdtZXNzYWdlJykgeyAvLyBtaW1lIG1lc3NhZ2VcbiAgICAgICAgICB0aGlzLmFkZE1pbWVEYXRhVG9GRChrZXksIGRhdGFba2V5XSwgZm9ybURhdGFBY2MpO1xuICAgICAgICAgIHJldHVybiBmb3JtRGF0YUFjYztcbiAgICAgICAgfVxuXG4gICAgICAgIHRoaXMuYWRkQ29tbW9uUHJvcGVydHlUb0ZEKGtleSwgZGF0YVtrZXldLCBmb3JtRGF0YUFjYyk7XG4gICAgICAgIHJldHVybiBmb3JtRGF0YUFjYztcbiAgICAgIH0sIG5ldyB0aGlzLkZvcm1EYXRhQ29uc3RydWN0b3IoKSk7XG4gICAgcmV0dXJuIGZvcm1EYXRhO1xuICB9XG5cbiAgcHJpdmF0ZSBpc0Zvcm1EYXRhUGFja2FnZShmb3JtRGF0YUluc3RhbmNlOiBOb2RlRm9ybURhdGEgfCBGb3JtRGF0YSlcbiAgOiBib29sZWFuIHtcbiAgICByZXR1cm4gKDxOb2RlRm9ybURhdGE+Zm9ybURhdGFJbnN0YW5jZSkuZ2V0SGVhZGVycyAhPT0gdW5kZWZpbmVkO1xuICB9XG5cbiAgcHJpdmF0ZSBnZXRBdHRhY2htZW50T3B0aW9ucyhpdGVtOiB7XG4gICAgZmlsZW5hbWU/OiBzdHJpbmc7XG4gICAgY29udGVudFR5cGU/IDogc3RyaW5nO1xuICAgIGtub3duTGVuZ3RoPzogbnVtYmVyO1xuICB9KToge1xuICAgIGZpbGVuYW1lPzogc3RyaW5nLFxuICAgIGNvbnRlbnRUeXBlPzogc3RyaW5nLFxuICAgIGtub3duTGVuZ3RoPzogbnVtYmVyXG4gIH0ge1xuICAgIGlmICh0eXBlb2YgaXRlbSAhPT0gJ29iamVjdCcgfHwgdGhpcy5pc1N0cmVhbShpdGVtKSkgcmV0dXJuIHt9O1xuICAgIGNvbnN0IHtcbiAgICAgIGZpbGVuYW1lLFxuICAgICAgY29udGVudFR5cGUsXG4gICAgICBrbm93bkxlbmd0aFxuICAgIH0gPSBpdGVtO1xuICAgIHJldHVybiB7XG4gICAgICAuLi4oZmlsZW5hbWUgPyB7IGZpbGVuYW1lIH0gOiB7IGZpbGVuYW1lOiAnZmlsZScgfSksXG4gICAgICAuLi4oY29udGVudFR5cGUgJiYgeyBjb250ZW50VHlwZSB9KSxcbiAgICAgIC4uLihrbm93bkxlbmd0aCAmJiB7IGtub3duTGVuZ3RoIH0pXG4gICAgfTtcbiAgfVxuXG4gIHByaXZhdGUgYWRkTWltZURhdGFUb0ZEKFxuICAgIGtleTogc3RyaW5nLFxuICAgIGRhdGE6IEJ1ZmZlciB8IEJsb2IgfCBzdHJpbmcsXG4gICAgZm9ybURhdGFJbnN0YW5jZTogTm9kZUZvcm1EYXRhIHwgRm9ybURhdGFcbiAgKTogdm9pZCB7XG4gICAgaWYgKHR5cGVvZiBkYXRhID09PSAnc3RyaW5nJykgeyAvLyBpZiBzdHJpbmcgb25seSB0d28gcGFyYW1ldGVycyBzaG91bGQgYmUgdXNlZC5cbiAgICAgIGZvcm1EYXRhSW5zdGFuY2UuYXBwZW5kKGtleSwgZGF0YSBhcyBzdHJpbmcpO1xuICAgICAgcmV0dXJuO1xuICAgIH1cblxuICAgIGlmICh0aGlzLmlzRm9ybURhdGFQYWNrYWdlKGZvcm1EYXRhSW5zdGFuY2UpKSB7IC8vIGZvcm0tZGF0YSBwYWNrYWdlIGlzIHVzZWRcbiAgICAgIGNvbnN0IG5vZGVGb3JtRGF0YSA9IGZvcm1EYXRhSW5zdGFuY2UgYXMgTm9kZUZvcm1EYXRhO1xuICAgICAgbm9kZUZvcm1EYXRhLmFwcGVuZChrZXksIGRhdGEsIHsgZmlsZW5hbWU6ICdNaW1lTWVzc2FnZScgfSk7XG4gICAgICByZXR1cm47XG4gICAgfVxuXG4gICAgaWYgKHR5cGVvZiBCbG9iICE9PSB1bmRlZmluZWQpIHsgLy8gZWl0aGVyIG5vZGUgPiAxOCBvciBicm93c2VyXG4gICAgICBjb25zdCBicm93c2VyRm9ybURhdGEgPSBmb3JtRGF0YUluc3RhbmNlIGFzIEZvcm1EYXRhOyAvLyBCcm93c2VyIGNvbXBsaWFudCBGb3JtRGF0YVxuICAgICAgaWYgKGRhdGEgaW5zdGFuY2VvZiBCbG9iKSB7XG4gICAgICAgIGJyb3dzZXJGb3JtRGF0YS5hcHBlbmQoa2V5LCBkYXRhLCAnTWltZU1lc3NhZ2UnKTtcbiAgICAgICAgcmV0dXJuO1xuICAgICAgfVxuICAgICAgaWYgKHR5cGVvZiBCdWZmZXIgIT09ICd1bmRlZmluZWQnKSB7IC8vIG5vZGUgZW52aXJvbm1lbnRcbiAgICAgICAgaWYgKEJ1ZmZlci5pc0J1ZmZlcihkYXRhKSkge1xuICAgICAgICAgIGNvbnN0IGJsb2JJbnN0YW5jZSA9IG5ldyBCbG9iKFtkYXRhXSk7XG4gICAgICAgICAgYnJvd3NlckZvcm1EYXRhLmFwcGVuZChrZXksIGJsb2JJbnN0YW5jZSwgJ01pbWVNZXNzYWdlJyk7XG4gICAgICAgICAgcmV0dXJuO1xuICAgICAgICB9XG4gICAgICB9XG4gICAgfVxuXG4gICAgdGhyb3cgbmV3IEFQSUVycm9yKHtcbiAgICAgIHN0YXR1czogNDAwLFxuICAgICAgc3RhdHVzVGV4dDogYFVua25vd24gZGF0YSB0eXBlIGZvciAke2tleX0gcHJvcGVydHlgLFxuICAgICAgYm9keTogJ1RoZSBtaW1lIGRhdGEgc2hvdWxkIGhhdmUgdHlwZSBvZiBCdWZmZXIsIFN0cmluZyBvciBCbG9iJ1xuICAgIH0gYXMgQVBJRXJyb3JPcHRpb25zKTtcbiAgfVxuXG4gIHByaXZhdGUgYWRkRmlsZXNUb0ZEKFxuICAgIHByb3BlcnR5TmFtZTogc3RyaW5nLFxuICAgIHZhbHVlOiBhbnksXG4gICAgZm9ybURhdGFJbnN0YW5jZTogTm9kZUZvcm1EYXRhIHwgRm9ybURhdGFcbiAgKTogdm9pZCB7XG4gICAgY29uc3QgYXBwZW5kRmlsZVRvRkQgPSAoXG4gICAgICBvcmlnaW5hbEtleTogc3RyaW5nLFxuICAgICAgb2JqOiBhbnksXG4gICAgICBmb3JtRGF0YTogTm9kZUZvcm1EYXRhIHwgRm9ybURhdGFcbiAgICApOiB2b2lkID0+IHtcbiAgICAgIGNvbnN0IGtleSA9IG9yaWdpbmFsS2V5ID09PSAnbXVsdGlwbGVWYWxpZGF0aW9uRmlsZScgPyAnZmlsZScgOiBvcmlnaW5hbEtleTtcbiAgICAgIGNvbnN0IGlzU3RyZWFtRGF0YSA9IHRoaXMuaXNTdHJlYW0ob2JqKTtcbiAgICAgIGNvbnN0IG9iakRhdGEgPSBpc1N0cmVhbURhdGEgPyBvYmogOiBvYmouZGF0YTtcbiAgICAgIC8vIGdldEF0dGFjaG1lbnRPcHRpb25zIHNob3VsZCBiZSBjYWxsZWQgd2l0aCBvYmogcGFyYW1ldGVyIHRvIHByZXZlbnQgbG9vc2luZyBmaWxlbmFtZVxuICAgICAgY29uc3Qgb3B0aW9ucyA9IHRoaXMuZ2V0QXR0YWNobWVudE9wdGlvbnMob2JqKTtcblxuICAgICAgaWYgKHRoaXMuaXNGb3JtRGF0YVBhY2thZ2UoZm9ybURhdGEpKSB7XG4gICAgICAgIGNvbnN0IGZkID0gZm9ybURhdGEgYXMgTm9kZUZvcm1EYXRhO1xuICAgICAgICBjb25zdCBkYXRhID0gdHlwZW9mIG9iakRhdGEgPT09ICdzdHJpbmcnID8gQnVmZmVyLmZyb20ob2JqRGF0YSkgOiBvYmpEYXRhO1xuICAgICAgICBmZC5hcHBlbmQoa2V5LCBkYXRhLCBvcHRpb25zKTtcbiAgICAgICAgcmV0dXJuO1xuICAgICAgfVxuXG4gICAgICBpZiAodHlwZW9mIEJsb2IgIT09IHVuZGVmaW5lZCkgeyAvLyBlaXRoZXIgbm9kZSA+IDE4IG9yIGJyb3dzZXJcbiAgICAgICAgY29uc3QgYnJvd3NlckZvcm1EYXRhID0gZm9ybURhdGFJbnN0YW5jZSBhcyBGb3JtRGF0YTsgLy8gQnJvd3NlciBjb21wbGlhbnQgRm9ybURhdGFcbiAgICAgICAgaWYgKHR5cGVvZiBvYmpEYXRhID09PSAnc3RyaW5nJykge1xuICAgICAgICAgIGNvbnN0IGJsb2JJbnN0YW5jZSA9IG5ldyBCbG9iKFtvYmpEYXRhXSk7XG4gICAgICAgICAgYnJvd3NlckZvcm1EYXRhLmFwcGVuZChrZXksIGJsb2JJbnN0YW5jZSwgb3B0aW9ucy5maWxlbmFtZSk7XG4gICAgICAgICAgcmV0dXJuO1xuICAgICAgICB9XG4gICAgICAgIGlmIChvYmpEYXRhIGluc3RhbmNlb2YgQmxvYikge1xuICAgICAgICAgIGJyb3dzZXJGb3JtRGF0YS5hcHBlbmQoa2V5LCBvYmpEYXRhLCBvcHRpb25zLmZpbGVuYW1lKTtcbiAgICAgICAgICByZXR1cm47XG4gICAgICAgIH1cbiAgICAgICAgaWYgKHR5cGVvZiBCdWZmZXIgIT09ICd1bmRlZmluZWQnKSB7IC8vIG5vZGUgZW52aXJvbm1lbnRcbiAgICAgICAgICBpZiAoQnVmZmVyLmlzQnVmZmVyKG9iakRhdGEpKSB7XG4gICAgICAgICAgICBjb25zdCBibG9iSW5zdGFuY2UgPSBuZXcgQmxvYihbb2JqRGF0YV0pO1xuICAgICAgICAgICAgYnJvd3NlckZvcm1EYXRhLmFwcGVuZChrZXksIGJsb2JJbnN0YW5jZSwgb3B0aW9ucy5maWxlbmFtZSk7XG4gICAgICAgICAgfVxuICAgICAgICB9XG4gICAgICB9XG4gICAgfTtcblxuICAgIGlmIChBcnJheS5pc0FycmF5KHZhbHVlKSkge1xuICAgICAgdmFsdWUuZm9yRWFjaChmdW5jdGlvbiAoaXRlbSkge1xuICAgICAgICBhcHBlbmRGaWxlVG9GRChwcm9wZXJ0eU5hbWUsIGl0ZW0sIGZvcm1EYXRhSW5zdGFuY2UpO1xuICAgICAgfSk7XG4gICAgfSBlbHNlIHtcbiAgICAgIGFwcGVuZEZpbGVUb0ZEKHByb3BlcnR5TmFtZSwgdmFsdWUsIGZvcm1EYXRhSW5zdGFuY2UpO1xuICAgIH1cbiAgfVxuXG4gIHByaXZhdGUgaXNTdHJlYW0oZGF0YTogYW55KSB7XG4gICAgcmV0dXJuIHR5cGVvZiBkYXRhID09PSAnb2JqZWN0JyAmJiB0eXBlb2YgZGF0YS5waXBlID09PSAnZnVuY3Rpb24nO1xuICB9XG5cbiAgcHJpdmF0ZSBhZGRDb21tb25Qcm9wZXJ0eVRvRkQoXG4gICAga2V5OiBzdHJpbmcsXG4gICAgdmFsdWU6IGFueSxcbiAgICBmb3JtRGF0YUFjYzogTm9kZUZvcm1EYXRhIHwgRm9ybURhdGFcbiAgKTogdm9pZCB7XG4gICAgaWYgKEFycmF5LmlzQXJyYXkodmFsdWUpKSB7XG4gICAgICB2YWx1ZS5mb3JFYWNoKGZ1bmN0aW9uIChpdGVtOiBhbnkpIHtcbiAgICAgICAgZm9ybURhdGFBY2MuYXBwZW5kKGtleSwgaXRlbSk7XG4gICAgICB9KTtcbiAgICB9IGVsc2UgaWYgKHZhbHVlICE9IG51bGwpIHtcbiAgICAgIGZvcm1EYXRhQWNjLmFwcGVuZChrZXksIHZhbHVlKTtcbiAgICB9XG4gIH1cbn1cbmV4cG9ydCBkZWZhdWx0IEZvcm1EYXRhQnVpbGRlcjtcbiIsImltcG9ydCB1cmxqb2luIGZyb20gJ3VybC1qb2luJztcbmltcG9ydCBBUElFcnJvciBmcm9tICcuL0Vycm9yJztcblxuaW1wb3J0IHtcbiAgUGFnZXNMaXN0QWNjdW11bGF0b3IsXG4gIFBhcnNlZFBhZ2UsXG4gIFBhcnNlZFBhZ2VzTGlzdCxcbiAgUXVlcnlXaXRoUGFnZSxcbiAgUmVzcG9uc2VXaXRoUGFnaW5nLFxuICBVcGRhdGVkVXJsQW5kUXVlcnksXG4gIEFQSUVycm9yT3B0aW9uc1xufSBmcm9tICcuLi8uLi9UeXBlcy9Db21tb24nO1xuaW1wb3J0IHtcbiAgSUJvdW5jZSxcbiAgSUNvbXBsYWludCxcbiAgSVVuc3Vic2NyaWJlLFxuICBJV2hpdGVMaXN0XG59IGZyb20gJy4uLy4uL0ludGVyZmFjZXMvU3VwcHJlc3Npb25zJztcbmltcG9ydCBSZXF1ZXN0IGZyb20gJy4vUmVxdWVzdCc7XG5pbXBvcnQge1xuICBTdXBwcmVzc2lvbkRhdGFUeXBlXG59IGZyb20gJy4uLy4uL1R5cGVzL1N1cHByZXNzaW9ucyc7XG5cbmV4cG9ydCBkZWZhdWx0IGFic3RyYWN0IGNsYXNzIE5hdmlnYXRpb25UaHJ1UGFnZXMgPFQ+IHtcbiAgcmVxdWVzdD86IFJlcXVlc3Q7XG4gIGNvbnN0cnVjdG9yKHJlcXVlc3Q/OiBSZXF1ZXN0KSB7XG4gICAgaWYgKHJlcXVlc3QpIHtcbiAgICAgIHRoaXMucmVxdWVzdCA9IHJlcXVlc3Q7XG4gICAgfVxuICB9XG5cbiAgcHJvdGVjdGVkIHBhcnNlUGFnZShcbiAgICBpZDogc3RyaW5nLFxuICAgIHBhZ2VVcmw6IHN0cmluZyxcbiAgICB1cmxTZXBhcmF0b3I6IHN0cmluZyxcbiAgICBpdGVyYXRvck5hbWU6IHN0cmluZyB8IHVuZGVmaW5lZFxuICApIDogUGFyc2VkUGFnZSB7XG4gICAgY29uc3QgcGFyc2VkVXJsID0gbmV3IFVSTChwYWdlVXJsKTtcbiAgICBjb25zdCB7IHNlYXJjaFBhcmFtcyB9ID0gcGFyc2VkVXJsO1xuXG4gICAgY29uc3QgcGFnZVZhbHVlID0gcGFnZVVybCAmJiB0eXBlb2YgcGFnZVVybCA9PT0gJ3N0cmluZycgPyBwYWdlVXJsLnNwbGl0KHVybFNlcGFyYXRvcikucG9wKCkgfHwgJycgOiAnJztcbiAgICBsZXQgaXRlcmF0b3JQb3NpdGlvbiA9IG51bGw7XG4gICAgaWYgKGl0ZXJhdG9yTmFtZSkge1xuICAgICAgaXRlcmF0b3JQb3NpdGlvbiA9IHNlYXJjaFBhcmFtcy5oYXMoaXRlcmF0b3JOYW1lKVxuICAgICAgICA/IHNlYXJjaFBhcmFtcy5nZXQoaXRlcmF0b3JOYW1lKVxuICAgICAgICA6IHVuZGVmaW5lZDtcbiAgICB9XG4gICAgcmV0dXJuIHtcbiAgICAgIGlkLFxuICAgICAgcGFnZTogdXJsU2VwYXJhdG9yID09PSAnPycgPyBgPyR7cGFnZVZhbHVlfWAgOiBwYWdlVmFsdWUsXG4gICAgICBpdGVyYXRvclBvc2l0aW9uLFxuICAgICAgdXJsOiBwYWdlVXJsXG4gICAgfSBhcyBQYXJzZWRQYWdlO1xuICB9XG5cbiAgcHJvdGVjdGVkIHBhcnNlUGFnZUxpbmtzKFxuICAgIHJlc3BvbnNlOiBSZXNwb25zZVdpdGhQYWdpbmcsXG4gICAgdXJsU2VwYXJhdG9yOiBzdHJpbmcsXG4gICAgaXRlcmF0b3JOYW1lPzogc3RyaW5nXG4gICk6IFBhcnNlZFBhZ2VzTGlzdCB7XG4gICAgY29uc3QgcGFnZXMgPSBPYmplY3QuZW50cmllcyhyZXNwb25zZS5ib2R5LnBhZ2luZyk7XG4gICAgcmV0dXJuIHBhZ2VzLnJlZHVjZShcbiAgICAgIChhY2M6IFBhZ2VzTGlzdEFjY3VtdWxhdG9yLCBbaWQsIHBhZ2VVcmxdOiBbIGlkOiBzdHJpbmcsIHBhZ2VVcmw6IHN0cmluZ10pID0+IHtcbiAgICAgICAgYWNjW2lkXSA9IHRoaXMucGFyc2VQYWdlKGlkLCBwYWdlVXJsLCB1cmxTZXBhcmF0b3IsIGl0ZXJhdG9yTmFtZSk7XG4gICAgICAgIHJldHVybiBhY2M7XG4gICAgICB9LCB7fVxuICAgICkgYXMgdW5rbm93biBhcyBQYXJzZWRQYWdlc0xpc3Q7XG4gIH1cblxuICBwcml2YXRlIHVwZGF0ZVVybEFuZFF1ZXJ5KGNsaWVudFVybDogc3RyaW5nLCBxdWVyeT86IFF1ZXJ5V2l0aFBhZ2UpOiBVcGRhdGVkVXJsQW5kUXVlcnkge1xuICAgIGxldCB1cmwgPSBjbGllbnRVcmw7XG4gICAgY29uc3QgcXVlcnlDb3B5ID0geyAuLi5xdWVyeSB9O1xuICAgIGlmIChxdWVyeUNvcHkucGFnZSkge1xuICAgICAgdXJsID0gdXJsam9pbihjbGllbnRVcmwsIHF1ZXJ5Q29weS5wYWdlKTtcbiAgICAgIGRlbGV0ZSBxdWVyeUNvcHkucGFnZTtcbiAgICB9XG4gICAgcmV0dXJuIHtcbiAgICAgIHVybCxcbiAgICAgIHVwZGF0ZWRRdWVyeTogcXVlcnlDb3B5XG4gICAgfTtcbiAgfVxuXG4gIHByb3RlY3RlZCBhc3luYyByZXF1ZXN0TGlzdFdpdGhQYWdlcyhjbGllbnRVcmw6c3RyaW5nLCBxdWVyeT86IFF1ZXJ5V2l0aFBhZ2UsIE1vZGVsPzoge1xuICAgIG5ldyhkYXRhOiBTdXBwcmVzc2lvbkRhdGFUeXBlKTpcbiAgICBJQm91bmNlIHwgSUNvbXBsYWludCB8IElVbnN1YnNjcmliZSB8IElXaGl0ZUxpc3RcbiAgfSk6IFByb21pc2U8VD4ge1xuICAgIGNvbnN0IHsgdXJsLCB1cGRhdGVkUXVlcnkgfSA9IHRoaXMudXBkYXRlVXJsQW5kUXVlcnkoY2xpZW50VXJsLCBxdWVyeSk7XG4gICAgaWYgKHRoaXMucmVxdWVzdCkge1xuICAgICAgY29uc3QgcmVzcG9uc2U6IFJlc3BvbnNlV2l0aFBhZ2luZyA9IGF3YWl0IHRoaXMucmVxdWVzdC5nZXQodXJsLCB1cGRhdGVkUXVlcnkpO1xuICAgICAgLy8gTW9kZWwgaGVyZSBpcyB1c3VhbGx5IHVuZGVmaW5lZCBleGNlcHQgZm9yIFN1cHByZXNzaW9uIENsaWVudFxuICAgICAgcmV0dXJuIHRoaXMucGFyc2VMaXN0KHJlc3BvbnNlLCBNb2RlbCk7XG4gICAgfVxuICAgIHRocm93IG5ldyBBUElFcnJvcih7XG4gICAgICBzdGF0dXM6IDUwMCxcbiAgICAgIHN0YXR1c1RleHQ6ICdSZXF1ZXN0IHByb3BlcnR5IGlzIGVtcHR5JyxcbiAgICAgIGJvZHk6IHsgbWVzc2FnZTogJycgfVxuICAgIH0gYXMgQVBJRXJyb3JPcHRpb25zKTtcbiAgfVxuXG4gIHByb3RlY3RlZCBhYnN0cmFjdCBwYXJzZUxpc3QocmVzcG9uc2U6IFJlc3BvbnNlV2l0aFBhZ2luZywgTW9kZWw/OiB7XG4gICAgbmV3KGRhdGE6IFN1cHByZXNzaW9uRGF0YVR5cGUpOlxuICAgIElCb3VuY2UgfCBJQ29tcGxhaW50IHwgSVVuc3Vic2NyaWJlIHwgSVdoaXRlTGlzdFxuICB9KTogVDtcbn1cbiIsImltcG9ydCAqIGFzIGJhc2U2NCBmcm9tICdiYXNlLTY0JztcbmltcG9ydCB1cmxqb2luIGZyb20gJ3VybC1qb2luJztcbmltcG9ydCBheGlvcywge1xuICBBeGlvc0Vycm9yLFxuICBBeGlvc1Jlc3BvbnNlLFxuICBBeGlvc0hlYWRlcnMsXG4gIFJhd0F4aW9zUmVxdWVzdEhlYWRlcnMsXG4gIEF4aW9zUHJveHlDb25maWcsXG59IGZyb20gJ2F4aW9zJztcbmltcG9ydCAqIGFzIE5vZGVGb3JtRGF0YSBmcm9tICdmb3JtLWRhdGEnO1xuaW1wb3J0IEFQSUVycm9yIGZyb20gJy4vRXJyb3InO1xuaW1wb3J0IHtcbiAgT25DYWxsUmVxdWVzdE9wdGlvbnMsXG4gIFJlcXVlc3RPcHRpb25zLFxuICBBUElFcnJvck9wdGlvbnMsXG4gIElucHV0Rm9ybURhdGEsXG4gIEFQSVJlc3BvbnNlLFxuICBJcFBvb2xEZWxldGVEYXRhXG59IGZyb20gJy4uLy4uL1R5cGVzJztcblxuaW1wb3J0IEZvcm1EYXRhQnVpbGRlciBmcm9tICcuL0Zvcm1EYXRhQnVpbGRlcic7XG5pbXBvcnQgU3ViYWNjb3VudHNDbGllbnQgZnJvbSAnLi4vU3ViYWNjb3VudHMnO1xuXG5jbGFzcyBSZXF1ZXN0IHtcbiAgcHJpdmF0ZSB1c2VybmFtZTogc3RyaW5nO1xuICBwcml2YXRlIGtleTogc3RyaW5nO1xuICBwcml2YXRlIHVybDogc3RyaW5nO1xuICBwcml2YXRlIHRpbWVvdXQ6IG51bWJlcjtcbiAgcHJpdmF0ZSBoZWFkZXJzOiBBeGlvc0hlYWRlcnM7XG4gIHByaXZhdGUgZm9ybURhdGFCdWlsZGVyOiBGb3JtRGF0YUJ1aWxkZXI7XG4gIHByaXZhdGUgbWF4Qm9keUxlbmd0aDogbnVtYmVyO1xuICBwcml2YXRlIHByb3h5OiBBeGlvc1Byb3h5Q29uZmlnIHwgdW5kZWZpbmVkO1xuXG4gIGNvbnN0cnVjdG9yKG9wdGlvbnM6IFJlcXVlc3RPcHRpb25zLCBmb3JtRGF0YTogSW5wdXRGb3JtRGF0YSkge1xuICAgIHRoaXMudXNlcm5hbWUgPSBvcHRpb25zLnVzZXJuYW1lO1xuICAgIHRoaXMua2V5ID0gb3B0aW9ucy5rZXk7XG4gICAgdGhpcy51cmwgPSBvcHRpb25zLnVybCBhcyBzdHJpbmc7XG4gICAgdGhpcy50aW1lb3V0ID0gb3B0aW9ucy50aW1lb3V0O1xuICAgIHRoaXMuaGVhZGVycyA9IHRoaXMubWFrZUhlYWRlcnNGcm9tT2JqZWN0KG9wdGlvbnMuaGVhZGVycyk7XG4gICAgdGhpcy5mb3JtRGF0YUJ1aWxkZXIgPSBuZXcgRm9ybURhdGFCdWlsZGVyKGZvcm1EYXRhKTtcbiAgICB0aGlzLm1heEJvZHlMZW5ndGggPSA1MjQyODgwMDsgLy8gNTAgTUJcbiAgICB0aGlzLnByb3h5ID0gb3B0aW9ucz8ucHJveHk7XG4gIH1cblxuICBhc3luYyByZXF1ZXN0KFxuICAgIG1ldGhvZDogc3RyaW5nLFxuICAgIHVybDogc3RyaW5nLFxuICAgIG9uQ2FsbE9wdGlvbnM/OiBSZWNvcmQ8c3RyaW5nLCB1bmtub3duIHwgUmVjb3JkPHN0cmluZywgdW5rbm93bj4gPlxuICApOiBQcm9taXNlPEFQSVJlc3BvbnNlPiB7XG4gICAgY29uc3Qgb3B0aW9uczogT25DYWxsUmVxdWVzdE9wdGlvbnMgPSB7IC4uLm9uQ2FsbE9wdGlvbnMgfTtcbiAgICBkZWxldGUgb3B0aW9ucz8uaGVhZGVycztcbiAgICBjb25zdCByZXF1ZXN0SGVhZGVycyA9IHRoaXMuam9pbkFuZFRyYW5zZm9ybUhlYWRlcnMob25DYWxsT3B0aW9ucyk7XG4gICAgY29uc3QgcGFyYW1zID0geyAuLi5vcHRpb25zIH07XG5cbiAgICBpZiAob3B0aW9ucz8ucXVlcnkgJiYgT2JqZWN0LmdldE93blByb3BlcnR5TmFtZXMob3B0aW9ucz8ucXVlcnkpLmxlbmd0aCA+IDApIHtcbiAgICAgIHBhcmFtcy5wYXJhbXMgPSBuZXcgVVJMU2VhcmNoUGFyYW1zKG9wdGlvbnMucXVlcnkpO1xuICAgICAgZGVsZXRlIHBhcmFtcy5xdWVyeTtcbiAgICB9XG5cbiAgICBpZiAob3B0aW9ucz8uYm9keSkge1xuICAgICAgY29uc3QgYm9keSA9IG9wdGlvbnM/LmJvZHk7XG4gICAgICBwYXJhbXMuZGF0YSA9IGJvZHk7XG4gICAgICBkZWxldGUgcGFyYW1zLmJvZHk7XG4gICAgfVxuICAgIGxldCByZXNwb25zZTogQXhpb3NSZXNwb25zZTtcbiAgICBjb25zdCB1cmxWYWx1ZSA9IHVybGpvaW4odGhpcy51cmwsIHVybCk7XG5cbiAgICB0cnkge1xuICAgICAgcmVzcG9uc2UgPSBhd2FpdCBheGlvcy5yZXF1ZXN0KHtcbiAgICAgICAgbWV0aG9kOiBtZXRob2QudG9Mb2NhbGVVcHBlckNhc2UoKSxcbiAgICAgICAgdGltZW91dDogdGhpcy50aW1lb3V0LFxuICAgICAgICB1cmw6IHVybFZhbHVlLFxuICAgICAgICBoZWFkZXJzOiByZXF1ZXN0SGVhZGVycyxcbiAgICAgICAgLi4ucGFyYW1zLFxuICAgICAgICBtYXhCb2R5TGVuZ3RoOiB0aGlzLm1heEJvZHlMZW5ndGgsXG4gICAgICAgIHByb3h5OiB0aGlzLnByb3h5LFxuICAgICAgfSk7XG4gICAgfSBjYXRjaCAoZXJyOiB1bmtub3duKSB7XG4gICAgICBjb25zdCBlcnJvclJlc3BvbnNlID0gZXJyIGFzIEF4aW9zRXJyb3I7XG5cbiAgICAgIHRocm93IG5ldyBBUElFcnJvcih7XG4gICAgICAgIHN0YXR1czogZXJyb3JSZXNwb25zZT8ucmVzcG9uc2U/LnN0YXR1cyB8fCA0MDAsXG4gICAgICAgIHN0YXR1c1RleHQ6IGVycm9yUmVzcG9uc2U/LnJlc3BvbnNlPy5zdGF0dXNUZXh0IHx8IGVycm9yUmVzcG9uc2UuY29kZSxcbiAgICAgICAgYm9keTogZXJyb3JSZXNwb25zZT8ucmVzcG9uc2U/LmRhdGEgfHwgZXJyb3JSZXNwb25zZS5tZXNzYWdlXG4gICAgICB9IGFzIEFQSUVycm9yT3B0aW9ucyk7XG4gICAgfVxuXG4gICAgY29uc3QgcmVzID0gYXdhaXQgdGhpcy5nZXRSZXNwb25zZUJvZHkocmVzcG9uc2UpO1xuICAgIHJldHVybiByZXMgYXMgQVBJUmVzcG9uc2U7XG4gIH1cblxuICBwcml2YXRlIGFzeW5jIGdldFJlc3BvbnNlQm9keShyZXNwb25zZTogQXhpb3NSZXNwb25zZSk6IFByb21pc2U8QVBJUmVzcG9uc2U+IHtcbiAgICBjb25zdCByZXMgPSB7XG4gICAgICBib2R5OiB7fSxcbiAgICAgIHN0YXR1czogcmVzcG9uc2U/LnN0YXR1c1xuICAgIH0gYXMgQVBJUmVzcG9uc2U7XG5cbiAgICBpZiAodHlwZW9mIHJlc3BvbnNlLmRhdGEgPT09ICdzdHJpbmcnKSB7XG4gICAgICBpZiAocmVzcG9uc2UuZGF0YSA9PT0gJ01haWxndW4gTWFnbmlmaWNlbnQgQVBJJykge1xuICAgICAgICB0aHJvdyBuZXcgQVBJRXJyb3Ioe1xuICAgICAgICAgIHN0YXR1czogNDAwLFxuICAgICAgICAgIHN0YXR1c1RleHQ6ICdJbmNvcnJlY3QgdXJsJyxcbiAgICAgICAgICBib2R5OiByZXNwb25zZS5kYXRhXG4gICAgICAgIH0gYXMgQVBJRXJyb3JPcHRpb25zKTtcbiAgICAgIH1cbiAgICAgIHJlcy5ib2R5ID0ge1xuICAgICAgICBtZXNzYWdlOiByZXNwb25zZS5kYXRhXG4gICAgICB9O1xuICAgIH0gZWxzZSB7XG4gICAgICByZXMuYm9keSA9IHJlc3BvbnNlLmRhdGE7XG4gICAgfVxuICAgIHJldHVybiByZXM7XG4gIH1cblxuICBwcml2YXRlIGpvaW5BbmRUcmFuc2Zvcm1IZWFkZXJzKFxuICAgIG9uQ2FsbE9wdGlvbnM/OiBPbkNhbGxSZXF1ZXN0T3B0aW9uc1xuICApOiBBeGlvc0hlYWRlcnMge1xuICAgIGNvbnN0IHJlcXVlc3RIZWFkZXJzID0gbmV3IEF4aW9zSGVhZGVycygpO1xuXG4gICAgY29uc3QgYmFzaWMgPSBiYXNlNjQuZW5jb2RlKGAke3RoaXMudXNlcm5hbWV9OiR7dGhpcy5rZXl9YCk7XG4gICAgcmVxdWVzdEhlYWRlcnMuc2V0QXV0aG9yaXphdGlvbihgQmFzaWMgJHtiYXNpY31gKTtcbiAgICByZXF1ZXN0SGVhZGVycy5zZXQodGhpcy5oZWFkZXJzKTtcblxuICAgIGNvbnN0IHJlY2VpdmVkT25DYWxsSGVhZGVycyA9IG9uQ2FsbE9wdGlvbnMgJiYgb25DYWxsT3B0aW9ucy5oZWFkZXJzO1xuICAgIGNvbnN0IG9uQ2FsbEhlYWRlcnMgPSB0aGlzLm1ha2VIZWFkZXJzRnJvbU9iamVjdChyZWNlaXZlZE9uQ2FsbEhlYWRlcnMpO1xuICAgIHJlcXVlc3RIZWFkZXJzLnNldChvbkNhbGxIZWFkZXJzKTtcbiAgICByZXR1cm4gcmVxdWVzdEhlYWRlcnM7XG4gIH1cblxuICBwcml2YXRlIG1ha2VIZWFkZXJzRnJvbU9iamVjdChcbiAgICBoZWFkZXJzT2JqZWN0OiBSYXdBeGlvc1JlcXVlc3RIZWFkZXJzID0ge31cbiAgKTogQXhpb3NIZWFkZXJzIHtcbiAgICBsZXQgcmVxdWVzdEhlYWRlcnMgPSBuZXcgQXhpb3NIZWFkZXJzKCk7XG4gICAgcmVxdWVzdEhlYWRlcnMgPSBPYmplY3QuZW50cmllcyhoZWFkZXJzT2JqZWN0KS5yZWR1Y2UoXG4gICAgICAoaGVhZGVyc0FjY3VtdWxhdG9yOiBBeGlvc0hlYWRlcnMsIGN1cnJlbnRQYWlyKSA9PiB7XG4gICAgICAgIGNvbnN0IFtrZXksIHZhbHVlXSA9IGN1cnJlbnRQYWlyO1xuICAgICAgICBoZWFkZXJzQWNjdW11bGF0b3Iuc2V0KGtleSwgdmFsdWUpO1xuICAgICAgICByZXR1cm4gaGVhZGVyc0FjY3VtdWxhdG9yO1xuICAgICAgfSwgcmVxdWVzdEhlYWRlcnNcbiAgICApO1xuICAgIHJldHVybiByZXF1ZXN0SGVhZGVycztcbiAgfVxuXG4gIHNldFN1YmFjY291bnRIZWFkZXIoc3ViYWNjb3VudElkOiBzdHJpbmcpOiB2b2lkIHtcbiAgICBjb25zdCBoZWFkZXJzID0gdGhpcy5tYWtlSGVhZGVyc0Zyb21PYmplY3Qoe1xuICAgICAgLi4udGhpcy5oZWFkZXJzLFxuICAgICAgW1N1YmFjY291bnRzQ2xpZW50LlNVQkFDQ09VTlRfSEVBREVSXTogc3ViYWNjb3VudElkXG4gICAgfSk7XG4gICAgdGhpcy5oZWFkZXJzLnNldChoZWFkZXJzKTtcbiAgfVxuXG4gIHJlc2V0U3ViYWNjb3VudEhlYWRlcigpOiB2b2lkIHtcbiAgICB0aGlzLmhlYWRlcnMuZGVsZXRlKFN1YmFjY291bnRzQ2xpZW50LlNVQkFDQ09VTlRfSEVBREVSKTtcbiAgfVxuXG4gIHF1ZXJ5KFxuICAgIG1ldGhvZDogc3RyaW5nLFxuICAgIHVybDogc3RyaW5nLFxuICAgIHF1ZXJ5PzogUmVjb3JkPHN0cmluZywgdW5rbm93bj4gfCBBcnJheTxBcnJheTxzdHJpbmc+PixcbiAgICBvcHRpb25zPzogUmVjb3JkPHN0cmluZywgdW5rbm93bj5cbiAgKTogUHJvbWlzZTxBUElSZXNwb25zZT4ge1xuICAgIHJldHVybiB0aGlzLnJlcXVlc3QobWV0aG9kLCB1cmwsIHsgcXVlcnksIC4uLm9wdGlvbnMgfSk7XG4gIH1cblxuICBjb21tYW5kKFxuICAgIG1ldGhvZDogc3RyaW5nLFxuICAgIHVybDogc3RyaW5nLFxuICAgIGRhdGE/OiBSZWNvcmQ8c3RyaW5nLCB1bmtub3duPiB8IFJlY29yZDxzdHJpbmcsIHVua25vd24+W10gfCBzdHJpbmcgfCBOb2RlRm9ybURhdGEgfCBGb3JtRGF0YSxcbiAgICBvcHRpb25zPzogUmVjb3JkPHN0cmluZywgdW5rbm93bj4sXG4gICAgYWRkRGVmYXVsdEhlYWRlcnMgPSB0cnVlXG4gICk6IFByb21pc2U8QVBJUmVzcG9uc2U+IHtcbiAgICBsZXQgaGVhZGVycyA9IHt9O1xuICAgIGlmIChhZGREZWZhdWx0SGVhZGVycykge1xuICAgICAgaGVhZGVycyA9IHsgJ0NvbnRlbnQtVHlwZSc6ICdhcHBsaWNhdGlvbi94LXd3dy1mb3JtLXVybGVuY29kZWQnIH07XG4gICAgfVxuICAgIGNvbnN0IHJlcXVlc3RPcHRpb25zID0ge1xuICAgICAgLi4uaGVhZGVycyxcbiAgICAgIGJvZHk6IGRhdGEsXG4gICAgICAuLi5vcHRpb25zXG4gICAgfTtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0KFxuICAgICAgbWV0aG9kLFxuICAgICAgdXJsLFxuICAgICAgcmVxdWVzdE9wdGlvbnNcbiAgICApO1xuICB9XG5cbiAgZ2V0KFxuICAgIHVybDogc3RyaW5nLFxuICAgIHF1ZXJ5PzogUmVjb3JkPHN0cmluZywgdW5rbm93bj4gfCBBcnJheTxBcnJheTxzdHJpbmc+PixcbiAgICBvcHRpb25zPzogUmVjb3JkPHN0cmluZywgdW5rbm93bj5cbiAgKTogUHJvbWlzZTxBUElSZXNwb25zZT4ge1xuICAgIHJldHVybiB0aGlzLnF1ZXJ5KCdnZXQnLCB1cmwsIHF1ZXJ5LCBvcHRpb25zKTtcbiAgfVxuXG4gIHBvc3QoXG4gICAgdXJsOiBzdHJpbmcsXG4gICAgZGF0YT86IFJlY29yZDxzdHJpbmcsIHVua25vd24+IHwgc3RyaW5nLFxuICAgIG9wdGlvbnM/OiBSZWNvcmQ8c3RyaW5nLCB1bmtub3duPlxuICApOiBQcm9taXNlPEFQSVJlc3BvbnNlPiB7XG4gICAgcmV0dXJuIHRoaXMuY29tbWFuZCgncG9zdCcsIHVybCwgZGF0YSwgb3B0aW9ucyk7XG4gIH1cblxuICBwb3N0V2l0aEZEKFxuICAgIHVybDogc3RyaW5nLFxuICAgIGRhdGE6IFJlY29yZDxzdHJpbmcsIHVua25vd24+IHwgUmVjb3JkPHN0cmluZywgdW5rbm93bj5bXVxuICApOiBQcm9taXNlPEFQSVJlc3BvbnNlPiB7XG4gICAgY29uc3QgZm9ybURhdGEgPSB0aGlzLmZvcm1EYXRhQnVpbGRlci5jcmVhdGVGb3JtRGF0YShkYXRhKTtcbiAgICByZXR1cm4gdGhpcy5jb21tYW5kKCdwb3N0JywgdXJsLCBmb3JtRGF0YSwge1xuICAgICAgaGVhZGVyczogeyAnQ29udGVudC1UeXBlJzogJ211bHRpcGFydC9mb3JtLWRhdGEnIH1cbiAgICB9LCBmYWxzZSk7XG4gIH1cblxuICBwdXRXaXRoRkQodXJsOiBzdHJpbmcsIGRhdGE6IFJlY29yZDxzdHJpbmcsIHVua25vd24+KTogUHJvbWlzZTxBUElSZXNwb25zZT4ge1xuICAgIGNvbnN0IGZvcm1EYXRhID0gdGhpcy5mb3JtRGF0YUJ1aWxkZXIuY3JlYXRlRm9ybURhdGEoZGF0YSk7XG4gICAgcmV0dXJuIHRoaXMuY29tbWFuZCgncHV0JywgdXJsLCBmb3JtRGF0YSwge1xuICAgICAgaGVhZGVyczogeyAnQ29udGVudC1UeXBlJzogJ211bHRpcGFydC9mb3JtLWRhdGEnIH1cbiAgICB9LCBmYWxzZSk7XG4gIH1cblxuICBwYXRjaFdpdGhGRCh1cmw6IHN0cmluZywgZGF0YTogUmVjb3JkPHN0cmluZywgdW5rbm93bj4pOiBQcm9taXNlPEFQSVJlc3BvbnNlPiB7XG4gICAgY29uc3QgZm9ybURhdGEgPSB0aGlzLmZvcm1EYXRhQnVpbGRlci5jcmVhdGVGb3JtRGF0YShkYXRhKTtcbiAgICByZXR1cm4gdGhpcy5jb21tYW5kKCdwYXRjaCcsIHVybCwgZm9ybURhdGEsIHtcbiAgICAgIGhlYWRlcnM6IHsgJ0NvbnRlbnQtVHlwZSc6ICdtdWx0aXBhcnQvZm9ybS1kYXRhJyB9XG4gICAgfSwgZmFsc2UpO1xuICB9XG5cbiAgcHV0KHVybDogc3RyaW5nLCBkYXRhPzogUmVjb3JkPHN0cmluZywgdW5rbm93bj4gfCBzdHJpbmcsIG9wdGlvbnM/OiBSZWNvcmQ8c3RyaW5nLCB1bmtub3duPilcbiAgOiBQcm9taXNlPEFQSVJlc3BvbnNlPiB7XG4gICAgcmV0dXJuIHRoaXMuY29tbWFuZCgncHV0JywgdXJsLCBkYXRhLCBvcHRpb25zKTtcbiAgfVxuXG4gIGRlbGV0ZSh1cmw6IHN0cmluZywgZGF0YT86IElwUG9vbERlbGV0ZURhdGEpOiBQcm9taXNlPEFQSVJlc3BvbnNlPiB7XG4gICAgcmV0dXJuIHRoaXMuY29tbWFuZCgnZGVsZXRlJywgdXJsLCBkYXRhKTtcbiAgfVxufVxuXG5leHBvcnQgZGVmYXVsdCBSZXF1ZXN0O1xuIiwiZXhwb3J0IGVudW0gUmVzb2x1dGlvbiB7XG4gICAgSE9VUiA9ICdob3VyJyxcbiAgICBEQVkgPSAnZGF5JyxcbiAgICBNT05USCA9ICdtb250aCdcbn1cblxuZXhwb3J0IGVudW0gU3VwcHJlc3Npb25Nb2RlbHMge1xuICAgIEJPVU5DRVMgPSAnYm91bmNlcycsXG4gICAgQ09NUExBSU5UUyA9ICdjb21wbGFpbnRzJyxcbiAgICBVTlNVQlNDUklCRVMgPSAndW5zdWJzY3JpYmVzJyxcbiAgICBXSElURUxJU1RTID0gJ3doaXRlbGlzdHMnXG59XG5cbmV4cG9ydCBlbnVtIFdlYmhvb2tzSWRzIHtcbiAgICBDTElDS0VEID0gJ2NsaWNrZWQnLFxuICAgIENPTVBMQUlORUQgPSAnY29tcGxhaW5lZCcsXG4gICAgREVMSVZFUkVEID0gJ2RlbGl2ZXJlZCcsXG4gICAgT1BFTkVEID0gJ29wZW5lZCcsXG4gICAgUEVSTUFORU5UX0ZBSUwgPSAncGVybWFuZW50X2ZhaWwnLFxuICAgIFRFTVBPUkFSWV9GQUlMID0gJ3RlbXBvcmFyeV9mYWlsJyxcbiAgICBVTlNVQlNDUklCRUQgPSAndW5zdWJzY3JpYmUnLFxufVxuXG5leHBvcnQgZW51bSBZZXNObyB7XG4gICAgWUVTID0gJ3llcycsXG4gICAgTk8gPSAnbm8nXG59XG4iLCJleHBvcnQgKiBmcm9tICcuL0xvZ2dlcic7XG4iLCJleHBvcnQgKiBmcm9tICcuL0RvbWFpbkNyZWRlbnRpYWxzJztcbmV4cG9ydCAqIGZyb20gJy4vRG9tYWluVGFncyc7XG5leHBvcnQgKiBmcm9tICcuL0RvbWFpblRlbXBsYXRlcyc7XG5leHBvcnQgKiBmcm9tICcuL0RvbWFpbnNDbGllbnQnO1xuIiwiZXhwb3J0ICogZnJvbSAnLi9JRXZlbnRDbGllbnQnO1xuIiwiZXhwb3J0ICogZnJvbSAnLi9JSVBQb29sc0NsaWVudCc7XG4iLCJleHBvcnQgKiBmcm9tICcuL0lJUHNDbGllbnQnO1xuIiwiZXhwb3J0ICogZnJvbSAnLi9JTWFpbGd1bkNsaWVudCc7XG4iLCJleHBvcnQgKiBmcm9tICcuL01haWxpbmdMaXN0TWVtYmVycyc7XG5leHBvcnQgKiBmcm9tICcuL01haWxpbmdMaXN0c0NsaWVudCc7XG4iLCJleHBvcnQgKiBmcm9tICcuL0lNZXNzYWdlc0NsaWVudCc7XG4iLCJleHBvcnQgKiBmcm9tICcuL0lSb3V0ZXNDbGllbnQnO1xuIiwiZXhwb3J0ICogZnJvbSAnLi9TdGF0c0NsaWVudCc7XG5leHBvcnQgKiBmcm9tICcuL1N0YXRzQ29udGFpbmVyJztcbiIsImV4cG9ydCAqIGZyb20gJy4vSVN1YmFjY291bnRzQ2xpZW50JztcbiIsImV4cG9ydCAqIGZyb20gJy4vQm91bmNlJztcbmV4cG9ydCAqIGZyb20gJy4vQ29tcGxhaW50JztcbmV4cG9ydCAqIGZyb20gJy4vVW5zdWJzY3JpYmUnO1xuZXhwb3J0ICogZnJvbSAnLi9XaGl0ZUxpc3QnO1xuZXhwb3J0ICogZnJvbSAnLi9JU3VwcHJlc3Npb25zQ2xpZW50JztcbiIsImV4cG9ydCAqIGZyb20gJy4vTXVsdGlwbGVWYWxpZGF0aW9uJztcbmV4cG9ydCAqIGZyb20gJy4vVmFsaWRhdGlvbic7XG4iLCJleHBvcnQgKiBmcm9tICcuL0lXZWJIb29rc0NsaWVudCc7XG4iLCJleHBvcnQgKiBmcm9tICcuL0NvbW1vbic7XG5leHBvcnQgKiBmcm9tICcuL0RvbWFpbnMnO1xuZXhwb3J0ICogZnJvbSAnLi9NYWlsZ3VuQ2xpZW50JztcbmV4cG9ydCAqIGZyb20gJy4vTWFpbGluZ0xpc3RzJztcbmV4cG9ydCAqIGZyb20gJy4vU3RhdHMnO1xuZXhwb3J0ICogZnJvbSAnLi9TdXBwcmVzc2lvbnMnO1xuZXhwb3J0ICogZnJvbSAnLi9WYWxpZGF0aW9ucyc7XG5leHBvcnQgKiBmcm9tICcuL0V2ZW50Q2xpZW50JztcbmV4cG9ydCAqIGZyb20gJy4vV2ViaG9va3MnO1xuZXhwb3J0ICogZnJvbSAnLi9NZXNzYWdlcyc7XG5leHBvcnQgKiBmcm9tICcuL1JvdXRlcyc7XG5leHBvcnQgKiBmcm9tICcuL0lQcyc7XG5leHBvcnQgKiBmcm9tICcuL0lQUG9vbHMnO1xuZXhwb3J0ICogZnJvbSAnLi9TdWJhY2NvdW50cyc7XG4iLCJleHBvcnQgKiBmcm9tICcuL0Vycm9yJztcbmV4cG9ydCAqIGZyb20gJy4vQXBpUmVzcG9uc2UnO1xuZXhwb3J0ICogZnJvbSAnLi9Gb3JtRGF0YSc7XG5leHBvcnQgKiBmcm9tICcuL05hdmlnYXRpb25UaHJ1UGFnZXMnO1xuZXhwb3J0ICogZnJvbSAnLi9SZXF1ZXN0T3B0aW9ucyc7XG4iLCJleHBvcnQgKiBmcm9tICcuL0RvbWFpbkNyZWRlbnRpYWxzJztcbmV4cG9ydCAqIGZyb20gJy4vRG9tYWlucyc7XG5leHBvcnQgKiBmcm9tICcuL0RvbWFpblRhZ3MnO1xuZXhwb3J0ICogZnJvbSAnLi9Eb21haW5UZW1wbGF0ZXMnO1xuZXhwb3J0ICogZnJvbSAnLi9Eb21haW5UcmFja2luZyc7XG4iLCJleHBvcnQgKiBmcm9tICcuL0V2ZW50cyc7XG4iLCJleHBvcnQgKiBmcm9tICcuL0lwUG9vbHMnO1xuIiwiZXhwb3J0ICogZnJvbSAnLi9JUHMnO1xuIiwiZXhwb3J0ICogZnJvbSAnLi9NYWlsZ3VuQ2xpZW50T3B0aW9ucyc7XG4iLCJleHBvcnQgKiBmcm9tICcuL01haWxpbmdMaXN0TWVtYmVycyc7XG5leHBvcnQgKiBmcm9tICcuL01haWxpbmdMaXN0cyc7XG4iLCJleHBvcnQgKiBmcm9tICcuL01lc3NhZ2VzJztcbiIsImV4cG9ydCAqIGZyb20gJy4vUm91dGVzJztcbiIsImV4cG9ydCAqIGZyb20gJy4vU3RhdHMnO1xuIiwiZXhwb3J0ICogZnJvbSAnLi9TdWJhY2NvdW50cyc7XG4iLCJleHBvcnQgKiBmcm9tICcuL0JvdW5jZSc7XG5leHBvcnQgKiBmcm9tICcuL0NvbXBsYWludCc7XG5leHBvcnQgKiBmcm9tICcuL1N1cHByZXNzaW9ucyc7XG5leHBvcnQgKiBmcm9tICcuL1Vuc3Vic2NyaWJlJztcbmV4cG9ydCAqIGZyb20gJy4vV2hpdGVMaXN0JztcbiIsImV4cG9ydCAqIGZyb20gJy4vTXVsdGlwbGVWYWxpZGF0aW9uJztcbmV4cG9ydCAqIGZyb20gJy4vVmFsaWRhdGlvbic7XG4iLCJleHBvcnQgKiBmcm9tICcuL1dlYmhvb2tzJztcbiIsImV4cG9ydCAqIGZyb20gJy4vQ29tbW9uJztcbmV4cG9ydCAqIGZyb20gJy4vRG9tYWlucyc7XG5leHBvcnQgKiBmcm9tICcuL0V2ZW50cyc7XG5leHBvcnQgKiBmcm9tICcuL0lQUG9vbHMnO1xuZXhwb3J0ICogZnJvbSAnLi9JUHMnO1xuZXhwb3J0ICogZnJvbSAnLi9NYWlsZ3VuQ2xpZW50JztcbmV4cG9ydCAqIGZyb20gJy4vTWFpbGluZ0xpc3RzJztcbmV4cG9ydCAqIGZyb20gJy4vTWVzc2FnZXMnO1xuZXhwb3J0ICogZnJvbSAnLi9Sb3V0ZXMnO1xuZXhwb3J0ICogZnJvbSAnLi9TdGF0cyc7XG5leHBvcnQgKiBmcm9tICcuL1N1YmFjY291bnRzJztcbmV4cG9ydCAqIGZyb20gJy4vU3VwcHJlc3Npb25zJztcbmV4cG9ydCAqIGZyb20gJy4vVmFsaWRhdGlvbnMnO1xuZXhwb3J0ICogZnJvbSAnLi9XZWJob29rcyc7XG4iLCJpbXBvcnQgTWFpbGd1bkNsaWVudCBmcm9tICcuL0NsYXNzZXMvTWFpbGd1bkNsaWVudCc7XG5pbXBvcnQgeyBJTWFpbGd1bkNsaWVudCB9IGZyb20gJy4vSW50ZXJmYWNlcyc7XG5pbXBvcnQgeyBJbnB1dEZvcm1EYXRhLCBNYWlsZ3VuQ2xpZW50T3B0aW9ucyB9IGZyb20gJy4vVHlwZXMnO1xuXG5leHBvcnQgKiBhcyBFbnVtcyBmcm9tICcuL0VudW1zJztcbmV4cG9ydCAqIGZyb20gJy4vVHlwZXMnO1xuZXhwb3J0ICogYXMgSW50ZXJmYWNlcyBmcm9tICcuL0ludGVyZmFjZXMnO1xuXG5leHBvcnQgZGVmYXVsdCBjbGFzcyBNYWlsZ3VuIHtcbiAgc3RhdGljIGdldCBkZWZhdWx0KCk6IHR5cGVvZiBNYWlsZ3VuIHsgcmV0dXJuIHRoaXM7IH1cbiAgcHJpdmF0ZSBmb3JtRGF0YTogSW5wdXRGb3JtRGF0YVxuXG4gIGNvbnN0cnVjdG9yKEZvcm1EYXRhOiBJbnB1dEZvcm1EYXRhKSB7XG4gICAgdGhpcy5mb3JtRGF0YSA9IEZvcm1EYXRhO1xuICB9XG5cbiAgY2xpZW50KG9wdGlvbnM6IE1haWxndW5DbGllbnRPcHRpb25zKSA6IElNYWlsZ3VuQ2xpZW50IHtcbiAgICByZXR1cm4gbmV3IE1haWxndW5DbGllbnQob3B0aW9ucywgdGhpcy5mb3JtRGF0YSk7XG4gIH1cbn1cbiIsIi8qISBodHRwczovL210aHMuYmUvYmFzZTY0IHYxLjAuMCBieSBAbWF0aGlhcyB8IE1JVCBsaWNlbnNlICovXG47KGZ1bmN0aW9uKHJvb3QpIHtcblxuXHQvLyBEZXRlY3QgZnJlZSB2YXJpYWJsZXMgYGV4cG9ydHNgLlxuXHR2YXIgZnJlZUV4cG9ydHMgPSB0eXBlb2YgZXhwb3J0cyA9PSAnb2JqZWN0JyAmJiBleHBvcnRzO1xuXG5cdC8vIERldGVjdCBmcmVlIHZhcmlhYmxlIGBtb2R1bGVgLlxuXHR2YXIgZnJlZU1vZHVsZSA9IHR5cGVvZiBtb2R1bGUgPT0gJ29iamVjdCcgJiYgbW9kdWxlICYmXG5cdFx0bW9kdWxlLmV4cG9ydHMgPT0gZnJlZUV4cG9ydHMgJiYgbW9kdWxlO1xuXG5cdC8vIERldGVjdCBmcmVlIHZhcmlhYmxlIGBnbG9iYWxgLCBmcm9tIE5vZGUuanMgb3IgQnJvd3NlcmlmaWVkIGNvZGUsIGFuZCB1c2Vcblx0Ly8gaXQgYXMgYHJvb3RgLlxuXHR2YXIgZnJlZUdsb2JhbCA9IHR5cGVvZiBnbG9iYWwgPT0gJ29iamVjdCcgJiYgZ2xvYmFsO1xuXHRpZiAoZnJlZUdsb2JhbC5nbG9iYWwgPT09IGZyZWVHbG9iYWwgfHwgZnJlZUdsb2JhbC53aW5kb3cgPT09IGZyZWVHbG9iYWwpIHtcblx0XHRyb290ID0gZnJlZUdsb2JhbDtcblx0fVxuXG5cdC8qLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0qL1xuXG5cdHZhciBJbnZhbGlkQ2hhcmFjdGVyRXJyb3IgPSBmdW5jdGlvbihtZXNzYWdlKSB7XG5cdFx0dGhpcy5tZXNzYWdlID0gbWVzc2FnZTtcblx0fTtcblx0SW52YWxpZENoYXJhY3RlckVycm9yLnByb3RvdHlwZSA9IG5ldyBFcnJvcjtcblx0SW52YWxpZENoYXJhY3RlckVycm9yLnByb3RvdHlwZS5uYW1lID0gJ0ludmFsaWRDaGFyYWN0ZXJFcnJvcic7XG5cblx0dmFyIGVycm9yID0gZnVuY3Rpb24obWVzc2FnZSkge1xuXHRcdC8vIE5vdGU6IHRoZSBlcnJvciBtZXNzYWdlcyB1c2VkIHRocm91Z2hvdXQgdGhpcyBmaWxlIG1hdGNoIHRob3NlIHVzZWQgYnlcblx0XHQvLyB0aGUgbmF0aXZlIGBhdG9iYC9gYnRvYWAgaW1wbGVtZW50YXRpb24gaW4gQ2hyb21pdW0uXG5cdFx0dGhyb3cgbmV3IEludmFsaWRDaGFyYWN0ZXJFcnJvcihtZXNzYWdlKTtcblx0fTtcblxuXHR2YXIgVEFCTEUgPSAnQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ejAxMjM0NTY3ODkrLyc7XG5cdC8vIGh0dHA6Ly93aGF0d2cub3JnL2h0bWwvY29tbW9uLW1pY3Jvc3ludGF4ZXMuaHRtbCNzcGFjZS1jaGFyYWN0ZXJcblx0dmFyIFJFR0VYX1NQQUNFX0NIQVJBQ1RFUlMgPSAvW1xcdFxcblxcZlxcciBdL2c7XG5cblx0Ly8gYGRlY29kZWAgaXMgZGVzaWduZWQgdG8gYmUgZnVsbHkgY29tcGF0aWJsZSB3aXRoIGBhdG9iYCBhcyBkZXNjcmliZWQgaW4gdGhlXG5cdC8vIEhUTUwgU3RhbmRhcmQuIGh0dHA6Ly93aGF0d2cub3JnL2h0bWwvd2ViYXBwYXBpcy5odG1sI2RvbS13aW5kb3diYXNlNjQtYXRvYlxuXHQvLyBUaGUgb3B0aW1pemVkIGJhc2U2NC1kZWNvZGluZyBhbGdvcml0aG0gdXNlZCBpcyBiYXNlZCBvbiBAYXRr4oCZcyBleGNlbGxlbnRcblx0Ly8gaW1wbGVtZW50YXRpb24uIGh0dHBzOi8vZ2lzdC5naXRodWIuY29tL2F0ay8xMDIwMzk2XG5cdHZhciBkZWNvZGUgPSBmdW5jdGlvbihpbnB1dCkge1xuXHRcdGlucHV0ID0gU3RyaW5nKGlucHV0KVxuXHRcdFx0LnJlcGxhY2UoUkVHRVhfU1BBQ0VfQ0hBUkFDVEVSUywgJycpO1xuXHRcdHZhciBsZW5ndGggPSBpbnB1dC5sZW5ndGg7XG5cdFx0aWYgKGxlbmd0aCAlIDQgPT0gMCkge1xuXHRcdFx0aW5wdXQgPSBpbnB1dC5yZXBsYWNlKC89PT8kLywgJycpO1xuXHRcdFx0bGVuZ3RoID0gaW5wdXQubGVuZ3RoO1xuXHRcdH1cblx0XHRpZiAoXG5cdFx0XHRsZW5ndGggJSA0ID09IDEgfHxcblx0XHRcdC8vIGh0dHA6Ly93aGF0d2cub3JnL0MjYWxwaGFudW1lcmljLWFzY2lpLWNoYXJhY3RlcnNcblx0XHRcdC9bXithLXpBLVowLTkvXS8udGVzdChpbnB1dClcblx0XHQpIHtcblx0XHRcdGVycm9yKFxuXHRcdFx0XHQnSW52YWxpZCBjaGFyYWN0ZXI6IHRoZSBzdHJpbmcgdG8gYmUgZGVjb2RlZCBpcyBub3QgY29ycmVjdGx5IGVuY29kZWQuJ1xuXHRcdFx0KTtcblx0XHR9XG5cdFx0dmFyIGJpdENvdW50ZXIgPSAwO1xuXHRcdHZhciBiaXRTdG9yYWdlO1xuXHRcdHZhciBidWZmZXI7XG5cdFx0dmFyIG91dHB1dCA9ICcnO1xuXHRcdHZhciBwb3NpdGlvbiA9IC0xO1xuXHRcdHdoaWxlICgrK3Bvc2l0aW9uIDwgbGVuZ3RoKSB7XG5cdFx0XHRidWZmZXIgPSBUQUJMRS5pbmRleE9mKGlucHV0LmNoYXJBdChwb3NpdGlvbikpO1xuXHRcdFx0Yml0U3RvcmFnZSA9IGJpdENvdW50ZXIgJSA0ID8gYml0U3RvcmFnZSAqIDY0ICsgYnVmZmVyIDogYnVmZmVyO1xuXHRcdFx0Ly8gVW5sZXNzIHRoaXMgaXMgdGhlIGZpcnN0IG9mIGEgZ3JvdXAgb2YgNCBjaGFyYWN0ZXJz4oCmXG5cdFx0XHRpZiAoYml0Q291bnRlcisrICUgNCkge1xuXHRcdFx0XHQvLyDigKZjb252ZXJ0IHRoZSBmaXJzdCA4IGJpdHMgdG8gYSBzaW5nbGUgQVNDSUkgY2hhcmFjdGVyLlxuXHRcdFx0XHRvdXRwdXQgKz0gU3RyaW5nLmZyb21DaGFyQ29kZShcblx0XHRcdFx0XHQweEZGICYgYml0U3RvcmFnZSA+PiAoLTIgKiBiaXRDb3VudGVyICYgNilcblx0XHRcdFx0KTtcblx0XHRcdH1cblx0XHR9XG5cdFx0cmV0dXJuIG91dHB1dDtcblx0fTtcblxuXHQvLyBgZW5jb2RlYCBpcyBkZXNpZ25lZCB0byBiZSBmdWxseSBjb21wYXRpYmxlIHdpdGggYGJ0b2FgIGFzIGRlc2NyaWJlZCBpbiB0aGVcblx0Ly8gSFRNTCBTdGFuZGFyZDogaHR0cDovL3doYXR3Zy5vcmcvaHRtbC93ZWJhcHBhcGlzLmh0bWwjZG9tLXdpbmRvd2Jhc2U2NC1idG9hXG5cdHZhciBlbmNvZGUgPSBmdW5jdGlvbihpbnB1dCkge1xuXHRcdGlucHV0ID0gU3RyaW5nKGlucHV0KTtcblx0XHRpZiAoL1teXFwwLVxceEZGXS8udGVzdChpbnB1dCkpIHtcblx0XHRcdC8vIE5vdGU6IG5vIG5lZWQgdG8gc3BlY2lhbC1jYXNlIGFzdHJhbCBzeW1ib2xzIGhlcmUsIGFzIHN1cnJvZ2F0ZXMgYXJlXG5cdFx0XHQvLyBtYXRjaGVkLCBhbmQgdGhlIGlucHV0IGlzIHN1cHBvc2VkIHRvIG9ubHkgY29udGFpbiBBU0NJSSBhbnl3YXkuXG5cdFx0XHRlcnJvcihcblx0XHRcdFx0J1RoZSBzdHJpbmcgdG8gYmUgZW5jb2RlZCBjb250YWlucyBjaGFyYWN0ZXJzIG91dHNpZGUgb2YgdGhlICcgK1xuXHRcdFx0XHQnTGF0aW4xIHJhbmdlLidcblx0XHRcdCk7XG5cdFx0fVxuXHRcdHZhciBwYWRkaW5nID0gaW5wdXQubGVuZ3RoICUgMztcblx0XHR2YXIgb3V0cHV0ID0gJyc7XG5cdFx0dmFyIHBvc2l0aW9uID0gLTE7XG5cdFx0dmFyIGE7XG5cdFx0dmFyIGI7XG5cdFx0dmFyIGM7XG5cdFx0dmFyIGJ1ZmZlcjtcblx0XHQvLyBNYWtlIHN1cmUgYW55IHBhZGRpbmcgaXMgaGFuZGxlZCBvdXRzaWRlIG9mIHRoZSBsb29wLlxuXHRcdHZhciBsZW5ndGggPSBpbnB1dC5sZW5ndGggLSBwYWRkaW5nO1xuXG5cdFx0d2hpbGUgKCsrcG9zaXRpb24gPCBsZW5ndGgpIHtcblx0XHRcdC8vIFJlYWQgdGhyZWUgYnl0ZXMsIGkuZS4gMjQgYml0cy5cblx0XHRcdGEgPSBpbnB1dC5jaGFyQ29kZUF0KHBvc2l0aW9uKSA8PCAxNjtcblx0XHRcdGIgPSBpbnB1dC5jaGFyQ29kZUF0KCsrcG9zaXRpb24pIDw8IDg7XG5cdFx0XHRjID0gaW5wdXQuY2hhckNvZGVBdCgrK3Bvc2l0aW9uKTtcblx0XHRcdGJ1ZmZlciA9IGEgKyBiICsgYztcblx0XHRcdC8vIFR1cm4gdGhlIDI0IGJpdHMgaW50byBmb3VyIGNodW5rcyBvZiA2IGJpdHMgZWFjaCwgYW5kIGFwcGVuZCB0aGVcblx0XHRcdC8vIG1hdGNoaW5nIGNoYXJhY3RlciBmb3IgZWFjaCBvZiB0aGVtIHRvIHRoZSBvdXRwdXQuXG5cdFx0XHRvdXRwdXQgKz0gKFxuXHRcdFx0XHRUQUJMRS5jaGFyQXQoYnVmZmVyID4+IDE4ICYgMHgzRikgK1xuXHRcdFx0XHRUQUJMRS5jaGFyQXQoYnVmZmVyID4+IDEyICYgMHgzRikgK1xuXHRcdFx0XHRUQUJMRS5jaGFyQXQoYnVmZmVyID4+IDYgJiAweDNGKSArXG5cdFx0XHRcdFRBQkxFLmNoYXJBdChidWZmZXIgJiAweDNGKVxuXHRcdFx0KTtcblx0XHR9XG5cblx0XHRpZiAocGFkZGluZyA9PSAyKSB7XG5cdFx0XHRhID0gaW5wdXQuY2hhckNvZGVBdChwb3NpdGlvbikgPDwgODtcblx0XHRcdGIgPSBpbnB1dC5jaGFyQ29kZUF0KCsrcG9zaXRpb24pO1xuXHRcdFx0YnVmZmVyID0gYSArIGI7XG5cdFx0XHRvdXRwdXQgKz0gKFxuXHRcdFx0XHRUQUJMRS5jaGFyQXQoYnVmZmVyID4+IDEwKSArXG5cdFx0XHRcdFRBQkxFLmNoYXJBdCgoYnVmZmVyID4+IDQpICYgMHgzRikgK1xuXHRcdFx0XHRUQUJMRS5jaGFyQXQoKGJ1ZmZlciA8PCAyKSAmIDB4M0YpICtcblx0XHRcdFx0Jz0nXG5cdFx0XHQpO1xuXHRcdH0gZWxzZSBpZiAocGFkZGluZyA9PSAxKSB7XG5cdFx0XHRidWZmZXIgPSBpbnB1dC5jaGFyQ29kZUF0KHBvc2l0aW9uKTtcblx0XHRcdG91dHB1dCArPSAoXG5cdFx0XHRcdFRBQkxFLmNoYXJBdChidWZmZXIgPj4gMikgK1xuXHRcdFx0XHRUQUJMRS5jaGFyQXQoKGJ1ZmZlciA8PCA0KSAmIDB4M0YpICtcblx0XHRcdFx0Jz09J1xuXHRcdFx0KTtcblx0XHR9XG5cblx0XHRyZXR1cm4gb3V0cHV0O1xuXHR9O1xuXG5cdHZhciBiYXNlNjQgPSB7XG5cdFx0J2VuY29kZSc6IGVuY29kZSxcblx0XHQnZGVjb2RlJzogZGVjb2RlLFxuXHRcdCd2ZXJzaW9uJzogJzEuMC4wJ1xuXHR9O1xuXG5cdC8vIFNvbWUgQU1EIGJ1aWxkIG9wdGltaXplcnMsIGxpa2Ugci5qcywgY2hlY2sgZm9yIHNwZWNpZmljIGNvbmRpdGlvbiBwYXR0ZXJuc1xuXHQvLyBsaWtlIHRoZSBmb2xsb3dpbmc6XG5cdGlmIChcblx0XHR0eXBlb2YgZGVmaW5lID09ICdmdW5jdGlvbicgJiZcblx0XHR0eXBlb2YgZGVmaW5lLmFtZCA9PSAnb2JqZWN0JyAmJlxuXHRcdGRlZmluZS5hbWRcblx0KSB7XG5cdFx0ZGVmaW5lKGZ1bmN0aW9uKCkge1xuXHRcdFx0cmV0dXJuIGJhc2U2NDtcblx0XHR9KTtcblx0fVx0ZWxzZSBpZiAoZnJlZUV4cG9ydHMgJiYgIWZyZWVFeHBvcnRzLm5vZGVUeXBlKSB7XG5cdFx0aWYgKGZyZWVNb2R1bGUpIHsgLy8gaW4gTm9kZS5qcyBvciBSaW5nb0pTIHYwLjguMCtcblx0XHRcdGZyZWVNb2R1bGUuZXhwb3J0cyA9IGJhc2U2NDtcblx0XHR9IGVsc2UgeyAvLyBpbiBOYXJ3aGFsIG9yIFJpbmdvSlMgdjAuNy4wLVxuXHRcdFx0Zm9yICh2YXIga2V5IGluIGJhc2U2NCkge1xuXHRcdFx0XHRiYXNlNjQuaGFzT3duUHJvcGVydHkoa2V5KSAmJiAoZnJlZUV4cG9ydHNba2V5XSA9IGJhc2U2NFtrZXldKTtcblx0XHRcdH1cblx0XHR9XG5cdH0gZWxzZSB7IC8vIGluIFJoaW5vIG9yIGEgd2ViIGJyb3dzZXJcblx0XHRyb290LmJhc2U2NCA9IGJhc2U2NDtcblx0fVxuXG59KHRoaXMpKTtcbiIsInZhciB1dGlsID0gcmVxdWlyZSgndXRpbCcpO1xudmFyIFN0cmVhbSA9IHJlcXVpcmUoJ3N0cmVhbScpLlN0cmVhbTtcbnZhciBEZWxheWVkU3RyZWFtID0gcmVxdWlyZSgnZGVsYXllZC1zdHJlYW0nKTtcblxubW9kdWxlLmV4cG9ydHMgPSBDb21iaW5lZFN0cmVhbTtcbmZ1bmN0aW9uIENvbWJpbmVkU3RyZWFtKCkge1xuICB0aGlzLndyaXRhYmxlID0gZmFsc2U7XG4gIHRoaXMucmVhZGFibGUgPSB0cnVlO1xuICB0aGlzLmRhdGFTaXplID0gMDtcbiAgdGhpcy5tYXhEYXRhU2l6ZSA9IDIgKiAxMDI0ICogMTAyNDtcbiAgdGhpcy5wYXVzZVN0cmVhbXMgPSB0cnVlO1xuXG4gIHRoaXMuX3JlbGVhc2VkID0gZmFsc2U7XG4gIHRoaXMuX3N0cmVhbXMgPSBbXTtcbiAgdGhpcy5fY3VycmVudFN0cmVhbSA9IG51bGw7XG4gIHRoaXMuX2luc2lkZUxvb3AgPSBmYWxzZTtcbiAgdGhpcy5fcGVuZGluZ05leHQgPSBmYWxzZTtcbn1cbnV0aWwuaW5oZXJpdHMoQ29tYmluZWRTdHJlYW0sIFN0cmVhbSk7XG5cbkNvbWJpbmVkU3RyZWFtLmNyZWF0ZSA9IGZ1bmN0aW9uKG9wdGlvbnMpIHtcbiAgdmFyIGNvbWJpbmVkU3RyZWFtID0gbmV3IHRoaXMoKTtcblxuICBvcHRpb25zID0gb3B0aW9ucyB8fCB7fTtcbiAgZm9yICh2YXIgb3B0aW9uIGluIG9wdGlvbnMpIHtcbiAgICBjb21iaW5lZFN0cmVhbVtvcHRpb25dID0gb3B0aW9uc1tvcHRpb25dO1xuICB9XG5cbiAgcmV0dXJuIGNvbWJpbmVkU3RyZWFtO1xufTtcblxuQ29tYmluZWRTdHJlYW0uaXNTdHJlYW1MaWtlID0gZnVuY3Rpb24oc3RyZWFtKSB7XG4gIHJldHVybiAodHlwZW9mIHN0cmVhbSAhPT0gJ2Z1bmN0aW9uJylcbiAgICAmJiAodHlwZW9mIHN0cmVhbSAhPT0gJ3N0cmluZycpXG4gICAgJiYgKHR5cGVvZiBzdHJlYW0gIT09ICdib29sZWFuJylcbiAgICAmJiAodHlwZW9mIHN0cmVhbSAhPT0gJ251bWJlcicpXG4gICAgJiYgKCFCdWZmZXIuaXNCdWZmZXIoc3RyZWFtKSk7XG59O1xuXG5Db21iaW5lZFN0cmVhbS5wcm90b3R5cGUuYXBwZW5kID0gZnVuY3Rpb24oc3RyZWFtKSB7XG4gIHZhciBpc1N0cmVhbUxpa2UgPSBDb21iaW5lZFN0cmVhbS5pc1N0cmVhbUxpa2Uoc3RyZWFtKTtcblxuICBpZiAoaXNTdHJlYW1MaWtlKSB7XG4gICAgaWYgKCEoc3RyZWFtIGluc3RhbmNlb2YgRGVsYXllZFN0cmVhbSkpIHtcbiAgICAgIHZhciBuZXdTdHJlYW0gPSBEZWxheWVkU3RyZWFtLmNyZWF0ZShzdHJlYW0sIHtcbiAgICAgICAgbWF4RGF0YVNpemU6IEluZmluaXR5LFxuICAgICAgICBwYXVzZVN0cmVhbTogdGhpcy5wYXVzZVN0cmVhbXMsXG4gICAgICB9KTtcbiAgICAgIHN0cmVhbS5vbignZGF0YScsIHRoaXMuX2NoZWNrRGF0YVNpemUuYmluZCh0aGlzKSk7XG4gICAgICBzdHJlYW0gPSBuZXdTdHJlYW07XG4gICAgfVxuXG4gICAgdGhpcy5faGFuZGxlRXJyb3JzKHN0cmVhbSk7XG5cbiAgICBpZiAodGhpcy5wYXVzZVN0cmVhbXMpIHtcbiAgICAgIHN0cmVhbS5wYXVzZSgpO1xuICAgIH1cbiAgfVxuXG4gIHRoaXMuX3N0cmVhbXMucHVzaChzdHJlYW0pO1xuICByZXR1cm4gdGhpcztcbn07XG5cbkNvbWJpbmVkU3RyZWFtLnByb3RvdHlwZS5waXBlID0gZnVuY3Rpb24oZGVzdCwgb3B0aW9ucykge1xuICBTdHJlYW0ucHJvdG90eXBlLnBpcGUuY2FsbCh0aGlzLCBkZXN0LCBvcHRpb25zKTtcbiAgdGhpcy5yZXN1bWUoKTtcbiAgcmV0dXJuIGRlc3Q7XG59O1xuXG5Db21iaW5lZFN0cmVhbS5wcm90b3R5cGUuX2dldE5leHQgPSBmdW5jdGlvbigpIHtcbiAgdGhpcy5fY3VycmVudFN0cmVhbSA9IG51bGw7XG5cbiAgaWYgKHRoaXMuX2luc2lkZUxvb3ApIHtcbiAgICB0aGlzLl9wZW5kaW5nTmV4dCA9IHRydWU7XG4gICAgcmV0dXJuOyAvLyBkZWZlciBjYWxsXG4gIH1cblxuICB0aGlzLl9pbnNpZGVMb29wID0gdHJ1ZTtcbiAgdHJ5IHtcbiAgICBkbyB7XG4gICAgICB0aGlzLl9wZW5kaW5nTmV4dCA9IGZhbHNlO1xuICAgICAgdGhpcy5fcmVhbEdldE5leHQoKTtcbiAgICB9IHdoaWxlICh0aGlzLl9wZW5kaW5nTmV4dCk7XG4gIH0gZmluYWxseSB7XG4gICAgdGhpcy5faW5zaWRlTG9vcCA9IGZhbHNlO1xuICB9XG59O1xuXG5Db21iaW5lZFN0cmVhbS5wcm90b3R5cGUuX3JlYWxHZXROZXh0ID0gZnVuY3Rpb24oKSB7XG4gIHZhciBzdHJlYW0gPSB0aGlzLl9zdHJlYW1zLnNoaWZ0KCk7XG5cblxuICBpZiAodHlwZW9mIHN0cmVhbSA9PSAndW5kZWZpbmVkJykge1xuICAgIHRoaXMuZW5kKCk7XG4gICAgcmV0dXJuO1xuICB9XG5cbiAgaWYgKHR5cGVvZiBzdHJlYW0gIT09ICdmdW5jdGlvbicpIHtcbiAgICB0aGlzLl9waXBlTmV4dChzdHJlYW0pO1xuICAgIHJldHVybjtcbiAgfVxuXG4gIHZhciBnZXRTdHJlYW0gPSBzdHJlYW07XG4gIGdldFN0cmVhbShmdW5jdGlvbihzdHJlYW0pIHtcbiAgICB2YXIgaXNTdHJlYW1MaWtlID0gQ29tYmluZWRTdHJlYW0uaXNTdHJlYW1MaWtlKHN0cmVhbSk7XG4gICAgaWYgKGlzU3RyZWFtTGlrZSkge1xuICAgICAgc3RyZWFtLm9uKCdkYXRhJywgdGhpcy5fY2hlY2tEYXRhU2l6ZS5iaW5kKHRoaXMpKTtcbiAgICAgIHRoaXMuX2hhbmRsZUVycm9ycyhzdHJlYW0pO1xuICAgIH1cblxuICAgIHRoaXMuX3BpcGVOZXh0KHN0cmVhbSk7XG4gIH0uYmluZCh0aGlzKSk7XG59O1xuXG5Db21iaW5lZFN0cmVhbS5wcm90b3R5cGUuX3BpcGVOZXh0ID0gZnVuY3Rpb24oc3RyZWFtKSB7XG4gIHRoaXMuX2N1cnJlbnRTdHJlYW0gPSBzdHJlYW07XG5cbiAgdmFyIGlzU3RyZWFtTGlrZSA9IENvbWJpbmVkU3RyZWFtLmlzU3RyZWFtTGlrZShzdHJlYW0pO1xuICBpZiAoaXNTdHJlYW1MaWtlKSB7XG4gICAgc3RyZWFtLm9uKCdlbmQnLCB0aGlzLl9nZXROZXh0LmJpbmQodGhpcykpO1xuICAgIHN0cmVhbS5waXBlKHRoaXMsIHtlbmQ6IGZhbHNlfSk7XG4gICAgcmV0dXJuO1xuICB9XG5cbiAgdmFyIHZhbHVlID0gc3RyZWFtO1xuICB0aGlzLndyaXRlKHZhbHVlKTtcbiAgdGhpcy5fZ2V0TmV4dCgpO1xufTtcblxuQ29tYmluZWRTdHJlYW0ucHJvdG90eXBlLl9oYW5kbGVFcnJvcnMgPSBmdW5jdGlvbihzdHJlYW0pIHtcbiAgdmFyIHNlbGYgPSB0aGlzO1xuICBzdHJlYW0ub24oJ2Vycm9yJywgZnVuY3Rpb24oZXJyKSB7XG4gICAgc2VsZi5fZW1pdEVycm9yKGVycik7XG4gIH0pO1xufTtcblxuQ29tYmluZWRTdHJlYW0ucHJvdG90eXBlLndyaXRlID0gZnVuY3Rpb24oZGF0YSkge1xuICB0aGlzLmVtaXQoJ2RhdGEnLCBkYXRhKTtcbn07XG5cbkNvbWJpbmVkU3RyZWFtLnByb3RvdHlwZS5wYXVzZSA9IGZ1bmN0aW9uKCkge1xuICBpZiAoIXRoaXMucGF1c2VTdHJlYW1zKSB7XG4gICAgcmV0dXJuO1xuICB9XG5cbiAgaWYodGhpcy5wYXVzZVN0cmVhbXMgJiYgdGhpcy5fY3VycmVudFN0cmVhbSAmJiB0eXBlb2YodGhpcy5fY3VycmVudFN0cmVhbS5wYXVzZSkgPT0gJ2Z1bmN0aW9uJykgdGhpcy5fY3VycmVudFN0cmVhbS5wYXVzZSgpO1xuICB0aGlzLmVtaXQoJ3BhdXNlJyk7XG59O1xuXG5Db21iaW5lZFN0cmVhbS5wcm90b3R5cGUucmVzdW1lID0gZnVuY3Rpb24oKSB7XG4gIGlmICghdGhpcy5fcmVsZWFzZWQpIHtcbiAgICB0aGlzLl9yZWxlYXNlZCA9IHRydWU7XG4gICAgdGhpcy53cml0YWJsZSA9IHRydWU7XG4gICAgdGhpcy5fZ2V0TmV4dCgpO1xuICB9XG5cbiAgaWYodGhpcy5wYXVzZVN0cmVhbXMgJiYgdGhpcy5fY3VycmVudFN0cmVhbSAmJiB0eXBlb2YodGhpcy5fY3VycmVudFN0cmVhbS5yZXN1bWUpID09ICdmdW5jdGlvbicpIHRoaXMuX2N1cnJlbnRTdHJlYW0ucmVzdW1lKCk7XG4gIHRoaXMuZW1pdCgncmVzdW1lJyk7XG59O1xuXG5Db21iaW5lZFN0cmVhbS5wcm90b3R5cGUuZW5kID0gZnVuY3Rpb24oKSB7XG4gIHRoaXMuX3Jlc2V0KCk7XG4gIHRoaXMuZW1pdCgnZW5kJyk7XG59O1xuXG5Db21iaW5lZFN0cmVhbS5wcm90b3R5cGUuZGVzdHJveSA9IGZ1bmN0aW9uKCkge1xuICB0aGlzLl9yZXNldCgpO1xuICB0aGlzLmVtaXQoJ2Nsb3NlJyk7XG59O1xuXG5Db21iaW5lZFN0cmVhbS5wcm90b3R5cGUuX3Jlc2V0ID0gZnVuY3Rpb24oKSB7XG4gIHRoaXMud3JpdGFibGUgPSBmYWxzZTtcbiAgdGhpcy5fc3RyZWFtcyA9IFtdO1xuICB0aGlzLl9jdXJyZW50U3RyZWFtID0gbnVsbDtcbn07XG5cbkNvbWJpbmVkU3RyZWFtLnByb3RvdHlwZS5fY2hlY2tEYXRhU2l6ZSA9IGZ1bmN0aW9uKCkge1xuICB0aGlzLl91cGRhdGVEYXRhU2l6ZSgpO1xuICBpZiAodGhpcy5kYXRhU2l6ZSA8PSB0aGlzLm1heERhdGFTaXplKSB7XG4gICAgcmV0dXJuO1xuICB9XG5cbiAgdmFyIG1lc3NhZ2UgPVxuICAgICdEZWxheWVkU3RyZWFtI21heERhdGFTaXplIG9mICcgKyB0aGlzLm1heERhdGFTaXplICsgJyBieXRlcyBleGNlZWRlZC4nO1xuICB0aGlzLl9lbWl0RXJyb3IobmV3IEVycm9yKG1lc3NhZ2UpKTtcbn07XG5cbkNvbWJpbmVkU3RyZWFtLnByb3RvdHlwZS5fdXBkYXRlRGF0YVNpemUgPSBmdW5jdGlvbigpIHtcbiAgdGhpcy5kYXRhU2l6ZSA9IDA7XG5cbiAgdmFyIHNlbGYgPSB0aGlzO1xuICB0aGlzLl9zdHJlYW1zLmZvckVhY2goZnVuY3Rpb24oc3RyZWFtKSB7XG4gICAgaWYgKCFzdHJlYW0uZGF0YVNpemUpIHtcbiAgICAgIHJldHVybjtcbiAgICB9XG5cbiAgICBzZWxmLmRhdGFTaXplICs9IHN0cmVhbS5kYXRhU2l6ZTtcbiAgfSk7XG5cbiAgaWYgKHRoaXMuX2N1cnJlbnRTdHJlYW0gJiYgdGhpcy5fY3VycmVudFN0cmVhbS5kYXRhU2l6ZSkge1xuICAgIHRoaXMuZGF0YVNpemUgKz0gdGhpcy5fY3VycmVudFN0cmVhbS5kYXRhU2l6ZTtcbiAgfVxufTtcblxuQ29tYmluZWRTdHJlYW0ucHJvdG90eXBlLl9lbWl0RXJyb3IgPSBmdW5jdGlvbihlcnIpIHtcbiAgdGhpcy5fcmVzZXQoKTtcbiAgdGhpcy5lbWl0KCdlcnJvcicsIGVycik7XG59O1xuIiwiLyogZXNsaW50LWVudiBicm93c2VyICovXG5cbi8qKlxuICogVGhpcyBpcyB0aGUgd2ViIGJyb3dzZXIgaW1wbGVtZW50YXRpb24gb2YgYGRlYnVnKClgLlxuICovXG5cbmV4cG9ydHMuZm9ybWF0QXJncyA9IGZvcm1hdEFyZ3M7XG5leHBvcnRzLnNhdmUgPSBzYXZlO1xuZXhwb3J0cy5sb2FkID0gbG9hZDtcbmV4cG9ydHMudXNlQ29sb3JzID0gdXNlQ29sb3JzO1xuZXhwb3J0cy5zdG9yYWdlID0gbG9jYWxzdG9yYWdlKCk7XG5leHBvcnRzLmRlc3Ryb3kgPSAoKCkgPT4ge1xuXHRsZXQgd2FybmVkID0gZmFsc2U7XG5cblx0cmV0dXJuICgpID0+IHtcblx0XHRpZiAoIXdhcm5lZCkge1xuXHRcdFx0d2FybmVkID0gdHJ1ZTtcblx0XHRcdGNvbnNvbGUud2FybignSW5zdGFuY2UgbWV0aG9kIGBkZWJ1Zy5kZXN0cm95KClgIGlzIGRlcHJlY2F0ZWQgYW5kIG5vIGxvbmdlciBkb2VzIGFueXRoaW5nLiBJdCB3aWxsIGJlIHJlbW92ZWQgaW4gdGhlIG5leHQgbWFqb3IgdmVyc2lvbiBvZiBgZGVidWdgLicpO1xuXHRcdH1cblx0fTtcbn0pKCk7XG5cbi8qKlxuICogQ29sb3JzLlxuICovXG5cbmV4cG9ydHMuY29sb3JzID0gW1xuXHQnIzAwMDBDQycsXG5cdCcjMDAwMEZGJyxcblx0JyMwMDMzQ0MnLFxuXHQnIzAwMzNGRicsXG5cdCcjMDA2NkNDJyxcblx0JyMwMDY2RkYnLFxuXHQnIzAwOTlDQycsXG5cdCcjMDA5OUZGJyxcblx0JyMwMENDMDAnLFxuXHQnIzAwQ0MzMycsXG5cdCcjMDBDQzY2Jyxcblx0JyMwMENDOTknLFxuXHQnIzAwQ0NDQycsXG5cdCcjMDBDQ0ZGJyxcblx0JyMzMzAwQ0MnLFxuXHQnIzMzMDBGRicsXG5cdCcjMzMzM0NDJyxcblx0JyMzMzMzRkYnLFxuXHQnIzMzNjZDQycsXG5cdCcjMzM2NkZGJyxcblx0JyMzMzk5Q0MnLFxuXHQnIzMzOTlGRicsXG5cdCcjMzNDQzAwJyxcblx0JyMzM0NDMzMnLFxuXHQnIzMzQ0M2NicsXG5cdCcjMzNDQzk5Jyxcblx0JyMzM0NDQ0MnLFxuXHQnIzMzQ0NGRicsXG5cdCcjNjYwMENDJyxcblx0JyM2NjAwRkYnLFxuXHQnIzY2MzNDQycsXG5cdCcjNjYzM0ZGJyxcblx0JyM2NkNDMDAnLFxuXHQnIzY2Q0MzMycsXG5cdCcjOTkwMENDJyxcblx0JyM5OTAwRkYnLFxuXHQnIzk5MzNDQycsXG5cdCcjOTkzM0ZGJyxcblx0JyM5OUNDMDAnLFxuXHQnIzk5Q0MzMycsXG5cdCcjQ0MwMDAwJyxcblx0JyNDQzAwMzMnLFxuXHQnI0NDMDA2NicsXG5cdCcjQ0MwMDk5Jyxcblx0JyNDQzAwQ0MnLFxuXHQnI0NDMDBGRicsXG5cdCcjQ0MzMzAwJyxcblx0JyNDQzMzMzMnLFxuXHQnI0NDMzM2NicsXG5cdCcjQ0MzMzk5Jyxcblx0JyNDQzMzQ0MnLFxuXHQnI0NDMzNGRicsXG5cdCcjQ0M2NjAwJyxcblx0JyNDQzY2MzMnLFxuXHQnI0NDOTkwMCcsXG5cdCcjQ0M5OTMzJyxcblx0JyNDQ0NDMDAnLFxuXHQnI0NDQ0MzMycsXG5cdCcjRkYwMDAwJyxcblx0JyNGRjAwMzMnLFxuXHQnI0ZGMDA2NicsXG5cdCcjRkYwMDk5Jyxcblx0JyNGRjAwQ0MnLFxuXHQnI0ZGMDBGRicsXG5cdCcjRkYzMzAwJyxcblx0JyNGRjMzMzMnLFxuXHQnI0ZGMzM2NicsXG5cdCcjRkYzMzk5Jyxcblx0JyNGRjMzQ0MnLFxuXHQnI0ZGMzNGRicsXG5cdCcjRkY2NjAwJyxcblx0JyNGRjY2MzMnLFxuXHQnI0ZGOTkwMCcsXG5cdCcjRkY5OTMzJyxcblx0JyNGRkNDMDAnLFxuXHQnI0ZGQ0MzMydcbl07XG5cbi8qKlxuICogQ3VycmVudGx5IG9ubHkgV2ViS2l0LWJhc2VkIFdlYiBJbnNwZWN0b3JzLCBGaXJlZm94ID49IHYzMSxcbiAqIGFuZCB0aGUgRmlyZWJ1ZyBleHRlbnNpb24gKGFueSBGaXJlZm94IHZlcnNpb24pIGFyZSBrbm93blxuICogdG8gc3VwcG9ydCBcIiVjXCIgQ1NTIGN1c3RvbWl6YXRpb25zLlxuICpcbiAqIFRPRE86IGFkZCBhIGBsb2NhbFN0b3JhZ2VgIHZhcmlhYmxlIHRvIGV4cGxpY2l0bHkgZW5hYmxlL2Rpc2FibGUgY29sb3JzXG4gKi9cblxuLy8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIGNvbXBsZXhpdHlcbmZ1bmN0aW9uIHVzZUNvbG9ycygpIHtcblx0Ly8gTkI6IEluIGFuIEVsZWN0cm9uIHByZWxvYWQgc2NyaXB0LCBkb2N1bWVudCB3aWxsIGJlIGRlZmluZWQgYnV0IG5vdCBmdWxseVxuXHQvLyBpbml0aWFsaXplZC4gU2luY2Ugd2Uga25vdyB3ZSdyZSBpbiBDaHJvbWUsIHdlJ2xsIGp1c3QgZGV0ZWN0IHRoaXMgY2FzZVxuXHQvLyBleHBsaWNpdGx5XG5cdGlmICh0eXBlb2Ygd2luZG93ICE9PSAndW5kZWZpbmVkJyAmJiB3aW5kb3cucHJvY2VzcyAmJiAod2luZG93LnByb2Nlc3MudHlwZSA9PT0gJ3JlbmRlcmVyJyB8fCB3aW5kb3cucHJvY2Vzcy5fX253anMpKSB7XG5cdFx0cmV0dXJuIHRydWU7XG5cdH1cblxuXHQvLyBJbnRlcm5ldCBFeHBsb3JlciBhbmQgRWRnZSBkbyBub3Qgc3VwcG9ydCBjb2xvcnMuXG5cdGlmICh0eXBlb2YgbmF2aWdhdG9yICE9PSAndW5kZWZpbmVkJyAmJiBuYXZpZ2F0b3IudXNlckFnZW50ICYmIG5hdmlnYXRvci51c2VyQWdlbnQudG9Mb3dlckNhc2UoKS5tYXRjaCgvKGVkZ2V8dHJpZGVudClcXC8oXFxkKykvKSkge1xuXHRcdHJldHVybiBmYWxzZTtcblx0fVxuXG5cdC8vIElzIHdlYmtpdD8gaHR0cDovL3N0YWNrb3ZlcmZsb3cuY29tL2EvMTY0NTk2MDYvMzc2NzczXG5cdC8vIGRvY3VtZW50IGlzIHVuZGVmaW5lZCBpbiByZWFjdC1uYXRpdmU6IGh0dHBzOi8vZ2l0aHViLmNvbS9mYWNlYm9vay9yZWFjdC1uYXRpdmUvcHVsbC8xNjMyXG5cdHJldHVybiAodHlwZW9mIGRvY3VtZW50ICE9PSAndW5kZWZpbmVkJyAmJiBkb2N1bWVudC5kb2N1bWVudEVsZW1lbnQgJiYgZG9jdW1lbnQuZG9jdW1lbnRFbGVtZW50LnN0eWxlICYmIGRvY3VtZW50LmRvY3VtZW50RWxlbWVudC5zdHlsZS5XZWJraXRBcHBlYXJhbmNlKSB8fFxuXHRcdC8vIElzIGZpcmVidWc/IGh0dHA6Ly9zdGFja292ZXJmbG93LmNvbS9hLzM5ODEyMC8zNzY3NzNcblx0XHQodHlwZW9mIHdpbmRvdyAhPT0gJ3VuZGVmaW5lZCcgJiYgd2luZG93LmNvbnNvbGUgJiYgKHdpbmRvdy5jb25zb2xlLmZpcmVidWcgfHwgKHdpbmRvdy5jb25zb2xlLmV4Y2VwdGlvbiAmJiB3aW5kb3cuY29uc29sZS50YWJsZSkpKSB8fFxuXHRcdC8vIElzIGZpcmVmb3ggPj0gdjMxP1xuXHRcdC8vIGh0dHBzOi8vZGV2ZWxvcGVyLm1vemlsbGEub3JnL2VuLVVTL2RvY3MvVG9vbHMvV2ViX0NvbnNvbGUjU3R5bGluZ19tZXNzYWdlc1xuXHRcdCh0eXBlb2YgbmF2aWdhdG9yICE9PSAndW5kZWZpbmVkJyAmJiBuYXZpZ2F0b3IudXNlckFnZW50ICYmIG5hdmlnYXRvci51c2VyQWdlbnQudG9Mb3dlckNhc2UoKS5tYXRjaCgvZmlyZWZveFxcLyhcXGQrKS8pICYmIHBhcnNlSW50KFJlZ0V4cC4kMSwgMTApID49IDMxKSB8fFxuXHRcdC8vIERvdWJsZSBjaGVjayB3ZWJraXQgaW4gdXNlckFnZW50IGp1c3QgaW4gY2FzZSB3ZSBhcmUgaW4gYSB3b3JrZXJcblx0XHQodHlwZW9mIG5hdmlnYXRvciAhPT0gJ3VuZGVmaW5lZCcgJiYgbmF2aWdhdG9yLnVzZXJBZ2VudCAmJiBuYXZpZ2F0b3IudXNlckFnZW50LnRvTG93ZXJDYXNlKCkubWF0Y2goL2FwcGxld2Via2l0XFwvKFxcZCspLykpO1xufVxuXG4vKipcbiAqIENvbG9yaXplIGxvZyBhcmd1bWVudHMgaWYgZW5hYmxlZC5cbiAqXG4gKiBAYXBpIHB1YmxpY1xuICovXG5cbmZ1bmN0aW9uIGZvcm1hdEFyZ3MoYXJncykge1xuXHRhcmdzWzBdID0gKHRoaXMudXNlQ29sb3JzID8gJyVjJyA6ICcnKSArXG5cdFx0dGhpcy5uYW1lc3BhY2UgK1xuXHRcdCh0aGlzLnVzZUNvbG9ycyA/ICcgJWMnIDogJyAnKSArXG5cdFx0YXJnc1swXSArXG5cdFx0KHRoaXMudXNlQ29sb3JzID8gJyVjICcgOiAnICcpICtcblx0XHQnKycgKyBtb2R1bGUuZXhwb3J0cy5odW1hbml6ZSh0aGlzLmRpZmYpO1xuXG5cdGlmICghdGhpcy51c2VDb2xvcnMpIHtcblx0XHRyZXR1cm47XG5cdH1cblxuXHRjb25zdCBjID0gJ2NvbG9yOiAnICsgdGhpcy5jb2xvcjtcblx0YXJncy5zcGxpY2UoMSwgMCwgYywgJ2NvbG9yOiBpbmhlcml0Jyk7XG5cblx0Ly8gVGhlIGZpbmFsIFwiJWNcIiBpcyBzb21ld2hhdCB0cmlja3ksIGJlY2F1c2UgdGhlcmUgY291bGQgYmUgb3RoZXJcblx0Ly8gYXJndW1lbnRzIHBhc3NlZCBlaXRoZXIgYmVmb3JlIG9yIGFmdGVyIHRoZSAlYywgc28gd2UgbmVlZCB0b1xuXHQvLyBmaWd1cmUgb3V0IHRoZSBjb3JyZWN0IGluZGV4IHRvIGluc2VydCB0aGUgQ1NTIGludG9cblx0bGV0IGluZGV4ID0gMDtcblx0bGV0IGxhc3RDID0gMDtcblx0YXJnc1swXS5yZXBsYWNlKC8lW2EtekEtWiVdL2csIG1hdGNoID0+IHtcblx0XHRpZiAobWF0Y2ggPT09ICclJScpIHtcblx0XHRcdHJldHVybjtcblx0XHR9XG5cdFx0aW5kZXgrKztcblx0XHRpZiAobWF0Y2ggPT09ICclYycpIHtcblx0XHRcdC8vIFdlIG9ubHkgYXJlIGludGVyZXN0ZWQgaW4gdGhlICpsYXN0KiAlY1xuXHRcdFx0Ly8gKHRoZSB1c2VyIG1heSBoYXZlIHByb3ZpZGVkIHRoZWlyIG93bilcblx0XHRcdGxhc3RDID0gaW5kZXg7XG5cdFx0fVxuXHR9KTtcblxuXHRhcmdzLnNwbGljZShsYXN0QywgMCwgYyk7XG59XG5cbi8qKlxuICogSW52b2tlcyBgY29uc29sZS5kZWJ1ZygpYCB3aGVuIGF2YWlsYWJsZS5cbiAqIE5vLW9wIHdoZW4gYGNvbnNvbGUuZGVidWdgIGlzIG5vdCBhIFwiZnVuY3Rpb25cIi5cbiAqIElmIGBjb25zb2xlLmRlYnVnYCBpcyBub3QgYXZhaWxhYmxlLCBmYWxscyBiYWNrXG4gKiB0byBgY29uc29sZS5sb2dgLlxuICpcbiAqIEBhcGkgcHVibGljXG4gKi9cbmV4cG9ydHMubG9nID0gY29uc29sZS5kZWJ1ZyB8fCBjb25zb2xlLmxvZyB8fCAoKCkgPT4ge30pO1xuXG4vKipcbiAqIFNhdmUgYG5hbWVzcGFjZXNgLlxuICpcbiAqIEBwYXJhbSB7U3RyaW5nfSBuYW1lc3BhY2VzXG4gKiBAYXBpIHByaXZhdGVcbiAqL1xuZnVuY3Rpb24gc2F2ZShuYW1lc3BhY2VzKSB7XG5cdHRyeSB7XG5cdFx0aWYgKG5hbWVzcGFjZXMpIHtcblx0XHRcdGV4cG9ydHMuc3RvcmFnZS5zZXRJdGVtKCdkZWJ1ZycsIG5hbWVzcGFjZXMpO1xuXHRcdH0gZWxzZSB7XG5cdFx0XHRleHBvcnRzLnN0b3JhZ2UucmVtb3ZlSXRlbSgnZGVidWcnKTtcblx0XHR9XG5cdH0gY2F0Y2ggKGVycm9yKSB7XG5cdFx0Ly8gU3dhbGxvd1xuXHRcdC8vIFhYWCAoQFFpeC0pIHNob3VsZCB3ZSBiZSBsb2dnaW5nIHRoZXNlP1xuXHR9XG59XG5cbi8qKlxuICogTG9hZCBgbmFtZXNwYWNlc2AuXG4gKlxuICogQHJldHVybiB7U3RyaW5nfSByZXR1cm5zIHRoZSBwcmV2aW91c2x5IHBlcnNpc3RlZCBkZWJ1ZyBtb2Rlc1xuICogQGFwaSBwcml2YXRlXG4gKi9cbmZ1bmN0aW9uIGxvYWQoKSB7XG5cdGxldCByO1xuXHR0cnkge1xuXHRcdHIgPSBleHBvcnRzLnN0b3JhZ2UuZ2V0SXRlbSgnZGVidWcnKTtcblx0fSBjYXRjaCAoZXJyb3IpIHtcblx0XHQvLyBTd2FsbG93XG5cdFx0Ly8gWFhYIChAUWl4LSkgc2hvdWxkIHdlIGJlIGxvZ2dpbmcgdGhlc2U/XG5cdH1cblxuXHQvLyBJZiBkZWJ1ZyBpc24ndCBzZXQgaW4gTFMsIGFuZCB3ZSdyZSBpbiBFbGVjdHJvbiwgdHJ5IHRvIGxvYWQgJERFQlVHXG5cdGlmICghciAmJiB0eXBlb2YgcHJvY2VzcyAhPT0gJ3VuZGVmaW5lZCcgJiYgJ2VudicgaW4gcHJvY2Vzcykge1xuXHRcdHIgPSBwcm9jZXNzLmVudi5ERUJVRztcblx0fVxuXG5cdHJldHVybiByO1xufVxuXG4vKipcbiAqIExvY2Fsc3RvcmFnZSBhdHRlbXB0cyB0byByZXR1cm4gdGhlIGxvY2Fsc3RvcmFnZS5cbiAqXG4gKiBUaGlzIGlzIG5lY2Vzc2FyeSBiZWNhdXNlIHNhZmFyaSB0aHJvd3NcbiAqIHdoZW4gYSB1c2VyIGRpc2FibGVzIGNvb2tpZXMvbG9jYWxzdG9yYWdlXG4gKiBhbmQgeW91IGF0dGVtcHQgdG8gYWNjZXNzIGl0LlxuICpcbiAqIEByZXR1cm4ge0xvY2FsU3RvcmFnZX1cbiAqIEBhcGkgcHJpdmF0ZVxuICovXG5cbmZ1bmN0aW9uIGxvY2Fsc3RvcmFnZSgpIHtcblx0dHJ5IHtcblx0XHQvLyBUVk1MS2l0IChBcHBsZSBUViBKUyBSdW50aW1lKSBkb2VzIG5vdCBoYXZlIGEgd2luZG93IG9iamVjdCwganVzdCBsb2NhbFN0b3JhZ2UgaW4gdGhlIGdsb2JhbCBjb250ZXh0XG5cdFx0Ly8gVGhlIEJyb3dzZXIgYWxzbyBoYXMgbG9jYWxTdG9yYWdlIGluIHRoZSBnbG9iYWwgY29udGV4dC5cblx0XHRyZXR1cm4gbG9jYWxTdG9yYWdlO1xuXHR9IGNhdGNoIChlcnJvcikge1xuXHRcdC8vIFN3YWxsb3dcblx0XHQvLyBYWFggKEBRaXgtKSBzaG91bGQgd2UgYmUgbG9nZ2luZyB0aGVzZT9cblx0fVxufVxuXG5tb2R1bGUuZXhwb3J0cyA9IHJlcXVpcmUoJy4vY29tbW9uJykoZXhwb3J0cyk7XG5cbmNvbnN0IHtmb3JtYXR0ZXJzfSA9IG1vZHVsZS5leHBvcnRzO1xuXG4vKipcbiAqIE1hcCAlaiB0byBgSlNPTi5zdHJpbmdpZnkoKWAsIHNpbmNlIG5vIFdlYiBJbnNwZWN0b3JzIGRvIHRoYXQgYnkgZGVmYXVsdC5cbiAqL1xuXG5mb3JtYXR0ZXJzLmogPSBmdW5jdGlvbiAodikge1xuXHR0cnkge1xuXHRcdHJldHVybiBKU09OLnN0cmluZ2lmeSh2KTtcblx0fSBjYXRjaCAoZXJyb3IpIHtcblx0XHRyZXR1cm4gJ1tVbmV4cGVjdGVkSlNPTlBhcnNlRXJyb3JdOiAnICsgZXJyb3IubWVzc2FnZTtcblx0fVxufTtcbiIsIlxuLyoqXG4gKiBUaGlzIGlzIHRoZSBjb21tb24gbG9naWMgZm9yIGJvdGggdGhlIE5vZGUuanMgYW5kIHdlYiBicm93c2VyXG4gKiBpbXBsZW1lbnRhdGlvbnMgb2YgYGRlYnVnKClgLlxuICovXG5cbmZ1bmN0aW9uIHNldHVwKGVudikge1xuXHRjcmVhdGVEZWJ1Zy5kZWJ1ZyA9IGNyZWF0ZURlYnVnO1xuXHRjcmVhdGVEZWJ1Zy5kZWZhdWx0ID0gY3JlYXRlRGVidWc7XG5cdGNyZWF0ZURlYnVnLmNvZXJjZSA9IGNvZXJjZTtcblx0Y3JlYXRlRGVidWcuZGlzYWJsZSA9IGRpc2FibGU7XG5cdGNyZWF0ZURlYnVnLmVuYWJsZSA9IGVuYWJsZTtcblx0Y3JlYXRlRGVidWcuZW5hYmxlZCA9IGVuYWJsZWQ7XG5cdGNyZWF0ZURlYnVnLmh1bWFuaXplID0gcmVxdWlyZSgnbXMnKTtcblx0Y3JlYXRlRGVidWcuZGVzdHJveSA9IGRlc3Ryb3k7XG5cblx0T2JqZWN0LmtleXMoZW52KS5mb3JFYWNoKGtleSA9PiB7XG5cdFx0Y3JlYXRlRGVidWdba2V5XSA9IGVudltrZXldO1xuXHR9KTtcblxuXHQvKipcblx0KiBUaGUgY3VycmVudGx5IGFjdGl2ZSBkZWJ1ZyBtb2RlIG5hbWVzLCBhbmQgbmFtZXMgdG8gc2tpcC5cblx0Ki9cblxuXHRjcmVhdGVEZWJ1Zy5uYW1lcyA9IFtdO1xuXHRjcmVhdGVEZWJ1Zy5za2lwcyA9IFtdO1xuXG5cdC8qKlxuXHQqIE1hcCBvZiBzcGVjaWFsIFwiJW5cIiBoYW5kbGluZyBmdW5jdGlvbnMsIGZvciB0aGUgZGVidWcgXCJmb3JtYXRcIiBhcmd1bWVudC5cblx0KlxuXHQqIFZhbGlkIGtleSBuYW1lcyBhcmUgYSBzaW5nbGUsIGxvd2VyIG9yIHVwcGVyLWNhc2UgbGV0dGVyLCBpLmUuIFwiblwiIGFuZCBcIk5cIi5cblx0Ki9cblx0Y3JlYXRlRGVidWcuZm9ybWF0dGVycyA9IHt9O1xuXG5cdC8qKlxuXHQqIFNlbGVjdHMgYSBjb2xvciBmb3IgYSBkZWJ1ZyBuYW1lc3BhY2Vcblx0KiBAcGFyYW0ge1N0cmluZ30gbmFtZXNwYWNlIFRoZSBuYW1lc3BhY2Ugc3RyaW5nIGZvciB0aGUgZGVidWcgaW5zdGFuY2UgdG8gYmUgY29sb3JlZFxuXHQqIEByZXR1cm4ge051bWJlcnxTdHJpbmd9IEFuIEFOU0kgY29sb3IgY29kZSBmb3IgdGhlIGdpdmVuIG5hbWVzcGFjZVxuXHQqIEBhcGkgcHJpdmF0ZVxuXHQqL1xuXHRmdW5jdGlvbiBzZWxlY3RDb2xvcihuYW1lc3BhY2UpIHtcblx0XHRsZXQgaGFzaCA9IDA7XG5cblx0XHRmb3IgKGxldCBpID0gMDsgaSA8IG5hbWVzcGFjZS5sZW5ndGg7IGkrKykge1xuXHRcdFx0aGFzaCA9ICgoaGFzaCA8PCA1KSAtIGhhc2gpICsgbmFtZXNwYWNlLmNoYXJDb2RlQXQoaSk7XG5cdFx0XHRoYXNoIHw9IDA7IC8vIENvbnZlcnQgdG8gMzJiaXQgaW50ZWdlclxuXHRcdH1cblxuXHRcdHJldHVybiBjcmVhdGVEZWJ1Zy5jb2xvcnNbTWF0aC5hYnMoaGFzaCkgJSBjcmVhdGVEZWJ1Zy5jb2xvcnMubGVuZ3RoXTtcblx0fVxuXHRjcmVhdGVEZWJ1Zy5zZWxlY3RDb2xvciA9IHNlbGVjdENvbG9yO1xuXG5cdC8qKlxuXHQqIENyZWF0ZSBhIGRlYnVnZ2VyIHdpdGggdGhlIGdpdmVuIGBuYW1lc3BhY2VgLlxuXHQqXG5cdCogQHBhcmFtIHtTdHJpbmd9IG5hbWVzcGFjZVxuXHQqIEByZXR1cm4ge0Z1bmN0aW9ufVxuXHQqIEBhcGkgcHVibGljXG5cdCovXG5cdGZ1bmN0aW9uIGNyZWF0ZURlYnVnKG5hbWVzcGFjZSkge1xuXHRcdGxldCBwcmV2VGltZTtcblx0XHRsZXQgZW5hYmxlT3ZlcnJpZGUgPSBudWxsO1xuXHRcdGxldCBuYW1lc3BhY2VzQ2FjaGU7XG5cdFx0bGV0IGVuYWJsZWRDYWNoZTtcblxuXHRcdGZ1bmN0aW9uIGRlYnVnKC4uLmFyZ3MpIHtcblx0XHRcdC8vIERpc2FibGVkP1xuXHRcdFx0aWYgKCFkZWJ1Zy5lbmFibGVkKSB7XG5cdFx0XHRcdHJldHVybjtcblx0XHRcdH1cblxuXHRcdFx0Y29uc3Qgc2VsZiA9IGRlYnVnO1xuXG5cdFx0XHQvLyBTZXQgYGRpZmZgIHRpbWVzdGFtcFxuXHRcdFx0Y29uc3QgY3VyciA9IE51bWJlcihuZXcgRGF0ZSgpKTtcblx0XHRcdGNvbnN0IG1zID0gY3VyciAtIChwcmV2VGltZSB8fCBjdXJyKTtcblx0XHRcdHNlbGYuZGlmZiA9IG1zO1xuXHRcdFx0c2VsZi5wcmV2ID0gcHJldlRpbWU7XG5cdFx0XHRzZWxmLmN1cnIgPSBjdXJyO1xuXHRcdFx0cHJldlRpbWUgPSBjdXJyO1xuXG5cdFx0XHRhcmdzWzBdID0gY3JlYXRlRGVidWcuY29lcmNlKGFyZ3NbMF0pO1xuXG5cdFx0XHRpZiAodHlwZW9mIGFyZ3NbMF0gIT09ICdzdHJpbmcnKSB7XG5cdFx0XHRcdC8vIEFueXRoaW5nIGVsc2UgbGV0J3MgaW5zcGVjdCB3aXRoICVPXG5cdFx0XHRcdGFyZ3MudW5zaGlmdCgnJU8nKTtcblx0XHRcdH1cblxuXHRcdFx0Ly8gQXBwbHkgYW55IGBmb3JtYXR0ZXJzYCB0cmFuc2Zvcm1hdGlvbnNcblx0XHRcdGxldCBpbmRleCA9IDA7XG5cdFx0XHRhcmdzWzBdID0gYXJnc1swXS5yZXBsYWNlKC8lKFthLXpBLVolXSkvZywgKG1hdGNoLCBmb3JtYXQpID0+IHtcblx0XHRcdFx0Ly8gSWYgd2UgZW5jb3VudGVyIGFuIGVzY2FwZWQgJSB0aGVuIGRvbid0IGluY3JlYXNlIHRoZSBhcnJheSBpbmRleFxuXHRcdFx0XHRpZiAobWF0Y2ggPT09ICclJScpIHtcblx0XHRcdFx0XHRyZXR1cm4gJyUnO1xuXHRcdFx0XHR9XG5cdFx0XHRcdGluZGV4Kys7XG5cdFx0XHRcdGNvbnN0IGZvcm1hdHRlciA9IGNyZWF0ZURlYnVnLmZvcm1hdHRlcnNbZm9ybWF0XTtcblx0XHRcdFx0aWYgKHR5cGVvZiBmb3JtYXR0ZXIgPT09ICdmdW5jdGlvbicpIHtcblx0XHRcdFx0XHRjb25zdCB2YWwgPSBhcmdzW2luZGV4XTtcblx0XHRcdFx0XHRtYXRjaCA9IGZvcm1hdHRlci5jYWxsKHNlbGYsIHZhbCk7XG5cblx0XHRcdFx0XHQvLyBOb3cgd2UgbmVlZCB0byByZW1vdmUgYGFyZ3NbaW5kZXhdYCBzaW5jZSBpdCdzIGlubGluZWQgaW4gdGhlIGBmb3JtYXRgXG5cdFx0XHRcdFx0YXJncy5zcGxpY2UoaW5kZXgsIDEpO1xuXHRcdFx0XHRcdGluZGV4LS07XG5cdFx0XHRcdH1cblx0XHRcdFx0cmV0dXJuIG1hdGNoO1xuXHRcdFx0fSk7XG5cblx0XHRcdC8vIEFwcGx5IGVudi1zcGVjaWZpYyBmb3JtYXR0aW5nIChjb2xvcnMsIGV0Yy4pXG5cdFx0XHRjcmVhdGVEZWJ1Zy5mb3JtYXRBcmdzLmNhbGwoc2VsZiwgYXJncyk7XG5cblx0XHRcdGNvbnN0IGxvZ0ZuID0gc2VsZi5sb2cgfHwgY3JlYXRlRGVidWcubG9nO1xuXHRcdFx0bG9nRm4uYXBwbHkoc2VsZiwgYXJncyk7XG5cdFx0fVxuXG5cdFx0ZGVidWcubmFtZXNwYWNlID0gbmFtZXNwYWNlO1xuXHRcdGRlYnVnLnVzZUNvbG9ycyA9IGNyZWF0ZURlYnVnLnVzZUNvbG9ycygpO1xuXHRcdGRlYnVnLmNvbG9yID0gY3JlYXRlRGVidWcuc2VsZWN0Q29sb3IobmFtZXNwYWNlKTtcblx0XHRkZWJ1Zy5leHRlbmQgPSBleHRlbmQ7XG5cdFx0ZGVidWcuZGVzdHJveSA9IGNyZWF0ZURlYnVnLmRlc3Ryb3k7IC8vIFhYWCBUZW1wb3JhcnkuIFdpbGwgYmUgcmVtb3ZlZCBpbiB0aGUgbmV4dCBtYWpvciByZWxlYXNlLlxuXG5cdFx0T2JqZWN0LmRlZmluZVByb3BlcnR5KGRlYnVnLCAnZW5hYmxlZCcsIHtcblx0XHRcdGVudW1lcmFibGU6IHRydWUsXG5cdFx0XHRjb25maWd1cmFibGU6IGZhbHNlLFxuXHRcdFx0Z2V0OiAoKSA9PiB7XG5cdFx0XHRcdGlmIChlbmFibGVPdmVycmlkZSAhPT0gbnVsbCkge1xuXHRcdFx0XHRcdHJldHVybiBlbmFibGVPdmVycmlkZTtcblx0XHRcdFx0fVxuXHRcdFx0XHRpZiAobmFtZXNwYWNlc0NhY2hlICE9PSBjcmVhdGVEZWJ1Zy5uYW1lc3BhY2VzKSB7XG5cdFx0XHRcdFx0bmFtZXNwYWNlc0NhY2hlID0gY3JlYXRlRGVidWcubmFtZXNwYWNlcztcblx0XHRcdFx0XHRlbmFibGVkQ2FjaGUgPSBjcmVhdGVEZWJ1Zy5lbmFibGVkKG5hbWVzcGFjZSk7XG5cdFx0XHRcdH1cblxuXHRcdFx0XHRyZXR1cm4gZW5hYmxlZENhY2hlO1xuXHRcdFx0fSxcblx0XHRcdHNldDogdiA9PiB7XG5cdFx0XHRcdGVuYWJsZU92ZXJyaWRlID0gdjtcblx0XHRcdH1cblx0XHR9KTtcblxuXHRcdC8vIEVudi1zcGVjaWZpYyBpbml0aWFsaXphdGlvbiBsb2dpYyBmb3IgZGVidWcgaW5zdGFuY2VzXG5cdFx0aWYgKHR5cGVvZiBjcmVhdGVEZWJ1Zy5pbml0ID09PSAnZnVuY3Rpb24nKSB7XG5cdFx0XHRjcmVhdGVEZWJ1Zy5pbml0KGRlYnVnKTtcblx0XHR9XG5cblx0XHRyZXR1cm4gZGVidWc7XG5cdH1cblxuXHRmdW5jdGlvbiBleHRlbmQobmFtZXNwYWNlLCBkZWxpbWl0ZXIpIHtcblx0XHRjb25zdCBuZXdEZWJ1ZyA9IGNyZWF0ZURlYnVnKHRoaXMubmFtZXNwYWNlICsgKHR5cGVvZiBkZWxpbWl0ZXIgPT09ICd1bmRlZmluZWQnID8gJzonIDogZGVsaW1pdGVyKSArIG5hbWVzcGFjZSk7XG5cdFx0bmV3RGVidWcubG9nID0gdGhpcy5sb2c7XG5cdFx0cmV0dXJuIG5ld0RlYnVnO1xuXHR9XG5cblx0LyoqXG5cdCogRW5hYmxlcyBhIGRlYnVnIG1vZGUgYnkgbmFtZXNwYWNlcy4gVGhpcyBjYW4gaW5jbHVkZSBtb2Rlc1xuXHQqIHNlcGFyYXRlZCBieSBhIGNvbG9uIGFuZCB3aWxkY2FyZHMuXG5cdCpcblx0KiBAcGFyYW0ge1N0cmluZ30gbmFtZXNwYWNlc1xuXHQqIEBhcGkgcHVibGljXG5cdCovXG5cdGZ1bmN0aW9uIGVuYWJsZShuYW1lc3BhY2VzKSB7XG5cdFx0Y3JlYXRlRGVidWcuc2F2ZShuYW1lc3BhY2VzKTtcblx0XHRjcmVhdGVEZWJ1Zy5uYW1lc3BhY2VzID0gbmFtZXNwYWNlcztcblxuXHRcdGNyZWF0ZURlYnVnLm5hbWVzID0gW107XG5cdFx0Y3JlYXRlRGVidWcuc2tpcHMgPSBbXTtcblxuXHRcdGxldCBpO1xuXHRcdGNvbnN0IHNwbGl0ID0gKHR5cGVvZiBuYW1lc3BhY2VzID09PSAnc3RyaW5nJyA/IG5hbWVzcGFjZXMgOiAnJykuc3BsaXQoL1tcXHMsXSsvKTtcblx0XHRjb25zdCBsZW4gPSBzcGxpdC5sZW5ndGg7XG5cblx0XHRmb3IgKGkgPSAwOyBpIDwgbGVuOyBpKyspIHtcblx0XHRcdGlmICghc3BsaXRbaV0pIHtcblx0XHRcdFx0Ly8gaWdub3JlIGVtcHR5IHN0cmluZ3Ncblx0XHRcdFx0Y29udGludWU7XG5cdFx0XHR9XG5cblx0XHRcdG5hbWVzcGFjZXMgPSBzcGxpdFtpXS5yZXBsYWNlKC9cXCovZywgJy4qPycpO1xuXG5cdFx0XHRpZiAobmFtZXNwYWNlc1swXSA9PT0gJy0nKSB7XG5cdFx0XHRcdGNyZWF0ZURlYnVnLnNraXBzLnB1c2gobmV3IFJlZ0V4cCgnXicgKyBuYW1lc3BhY2VzLnNsaWNlKDEpICsgJyQnKSk7XG5cdFx0XHR9IGVsc2Uge1xuXHRcdFx0XHRjcmVhdGVEZWJ1Zy5uYW1lcy5wdXNoKG5ldyBSZWdFeHAoJ14nICsgbmFtZXNwYWNlcyArICckJykpO1xuXHRcdFx0fVxuXHRcdH1cblx0fVxuXG5cdC8qKlxuXHQqIERpc2FibGUgZGVidWcgb3V0cHV0LlxuXHQqXG5cdCogQHJldHVybiB7U3RyaW5nfSBuYW1lc3BhY2VzXG5cdCogQGFwaSBwdWJsaWNcblx0Ki9cblx0ZnVuY3Rpb24gZGlzYWJsZSgpIHtcblx0XHRjb25zdCBuYW1lc3BhY2VzID0gW1xuXHRcdFx0Li4uY3JlYXRlRGVidWcubmFtZXMubWFwKHRvTmFtZXNwYWNlKSxcblx0XHRcdC4uLmNyZWF0ZURlYnVnLnNraXBzLm1hcCh0b05hbWVzcGFjZSkubWFwKG5hbWVzcGFjZSA9PiAnLScgKyBuYW1lc3BhY2UpXG5cdFx0XS5qb2luKCcsJyk7XG5cdFx0Y3JlYXRlRGVidWcuZW5hYmxlKCcnKTtcblx0XHRyZXR1cm4gbmFtZXNwYWNlcztcblx0fVxuXG5cdC8qKlxuXHQqIFJldHVybnMgdHJ1ZSBpZiB0aGUgZ2l2ZW4gbW9kZSBuYW1lIGlzIGVuYWJsZWQsIGZhbHNlIG90aGVyd2lzZS5cblx0KlxuXHQqIEBwYXJhbSB7U3RyaW5nfSBuYW1lXG5cdCogQHJldHVybiB7Qm9vbGVhbn1cblx0KiBAYXBpIHB1YmxpY1xuXHQqL1xuXHRmdW5jdGlvbiBlbmFibGVkKG5hbWUpIHtcblx0XHRpZiAobmFtZVtuYW1lLmxlbmd0aCAtIDFdID09PSAnKicpIHtcblx0XHRcdHJldHVybiB0cnVlO1xuXHRcdH1cblxuXHRcdGxldCBpO1xuXHRcdGxldCBsZW47XG5cblx0XHRmb3IgKGkgPSAwLCBsZW4gPSBjcmVhdGVEZWJ1Zy5za2lwcy5sZW5ndGg7IGkgPCBsZW47IGkrKykge1xuXHRcdFx0aWYgKGNyZWF0ZURlYnVnLnNraXBzW2ldLnRlc3QobmFtZSkpIHtcblx0XHRcdFx0cmV0dXJuIGZhbHNlO1xuXHRcdFx0fVxuXHRcdH1cblxuXHRcdGZvciAoaSA9IDAsIGxlbiA9IGNyZWF0ZURlYnVnLm5hbWVzLmxlbmd0aDsgaSA8IGxlbjsgaSsrKSB7XG5cdFx0XHRpZiAoY3JlYXRlRGVidWcubmFtZXNbaV0udGVzdChuYW1lKSkge1xuXHRcdFx0XHRyZXR1cm4gdHJ1ZTtcblx0XHRcdH1cblx0XHR9XG5cblx0XHRyZXR1cm4gZmFsc2U7XG5cdH1cblxuXHQvKipcblx0KiBDb252ZXJ0IHJlZ2V4cCB0byBuYW1lc3BhY2Vcblx0KlxuXHQqIEBwYXJhbSB7UmVnRXhwfSByZWd4ZXBcblx0KiBAcmV0dXJuIHtTdHJpbmd9IG5hbWVzcGFjZVxuXHQqIEBhcGkgcHJpdmF0ZVxuXHQqL1xuXHRmdW5jdGlvbiB0b05hbWVzcGFjZShyZWdleHApIHtcblx0XHRyZXR1cm4gcmVnZXhwLnRvU3RyaW5nKClcblx0XHRcdC5zdWJzdHJpbmcoMiwgcmVnZXhwLnRvU3RyaW5nKCkubGVuZ3RoIC0gMilcblx0XHRcdC5yZXBsYWNlKC9cXC5cXCpcXD8kLywgJyonKTtcblx0fVxuXG5cdC8qKlxuXHQqIENvZXJjZSBgdmFsYC5cblx0KlxuXHQqIEBwYXJhbSB7TWl4ZWR9IHZhbFxuXHQqIEByZXR1cm4ge01peGVkfVxuXHQqIEBhcGkgcHJpdmF0ZVxuXHQqL1xuXHRmdW5jdGlvbiBjb2VyY2UodmFsKSB7XG5cdFx0aWYgKHZhbCBpbnN0YW5jZW9mIEVycm9yKSB7XG5cdFx0XHRyZXR1cm4gdmFsLnN0YWNrIHx8IHZhbC5tZXNzYWdlO1xuXHRcdH1cblx0XHRyZXR1cm4gdmFsO1xuXHR9XG5cblx0LyoqXG5cdCogWFhYIERPIE5PVCBVU0UuIFRoaXMgaXMgYSB0ZW1wb3Jhcnkgc3R1YiBmdW5jdGlvbi5cblx0KiBYWFggSXQgV0lMTCBiZSByZW1vdmVkIGluIHRoZSBuZXh0IG1ham9yIHJlbGVhc2UuXG5cdCovXG5cdGZ1bmN0aW9uIGRlc3Ryb3koKSB7XG5cdFx0Y29uc29sZS53YXJuKCdJbnN0YW5jZSBtZXRob2QgYGRlYnVnLmRlc3Ryb3koKWAgaXMgZGVwcmVjYXRlZCBhbmQgbm8gbG9uZ2VyIGRvZXMgYW55dGhpbmcuIEl0IHdpbGwgYmUgcmVtb3ZlZCBpbiB0aGUgbmV4dCBtYWpvciB2ZXJzaW9uIG9mIGBkZWJ1Z2AuJyk7XG5cdH1cblxuXHRjcmVhdGVEZWJ1Zy5lbmFibGUoY3JlYXRlRGVidWcubG9hZCgpKTtcblxuXHRyZXR1cm4gY3JlYXRlRGVidWc7XG59XG5cbm1vZHVsZS5leHBvcnRzID0gc2V0dXA7XG4iLCIvKipcbiAqIERldGVjdCBFbGVjdHJvbiByZW5kZXJlciAvIG53anMgcHJvY2Vzcywgd2hpY2ggaXMgbm9kZSwgYnV0IHdlIHNob3VsZFxuICogdHJlYXQgYXMgYSBicm93c2VyLlxuICovXG5cbmlmICh0eXBlb2YgcHJvY2VzcyA9PT0gJ3VuZGVmaW5lZCcgfHwgcHJvY2Vzcy50eXBlID09PSAncmVuZGVyZXInIHx8IHByb2Nlc3MuYnJvd3NlciA9PT0gdHJ1ZSB8fCBwcm9jZXNzLl9fbndqcykge1xuXHRtb2R1bGUuZXhwb3J0cyA9IHJlcXVpcmUoJy4vYnJvd3Nlci5qcycpO1xufSBlbHNlIHtcblx0bW9kdWxlLmV4cG9ydHMgPSByZXF1aXJlKCcuL25vZGUuanMnKTtcbn1cbiIsIi8qKlxuICogTW9kdWxlIGRlcGVuZGVuY2llcy5cbiAqL1xuXG5jb25zdCB0dHkgPSByZXF1aXJlKCd0dHknKTtcbmNvbnN0IHV0aWwgPSByZXF1aXJlKCd1dGlsJyk7XG5cbi8qKlxuICogVGhpcyBpcyB0aGUgTm9kZS5qcyBpbXBsZW1lbnRhdGlvbiBvZiBgZGVidWcoKWAuXG4gKi9cblxuZXhwb3J0cy5pbml0ID0gaW5pdDtcbmV4cG9ydHMubG9nID0gbG9nO1xuZXhwb3J0cy5mb3JtYXRBcmdzID0gZm9ybWF0QXJncztcbmV4cG9ydHMuc2F2ZSA9IHNhdmU7XG5leHBvcnRzLmxvYWQgPSBsb2FkO1xuZXhwb3J0cy51c2VDb2xvcnMgPSB1c2VDb2xvcnM7XG5leHBvcnRzLmRlc3Ryb3kgPSB1dGlsLmRlcHJlY2F0ZShcblx0KCkgPT4ge30sXG5cdCdJbnN0YW5jZSBtZXRob2QgYGRlYnVnLmRlc3Ryb3koKWAgaXMgZGVwcmVjYXRlZCBhbmQgbm8gbG9uZ2VyIGRvZXMgYW55dGhpbmcuIEl0IHdpbGwgYmUgcmVtb3ZlZCBpbiB0aGUgbmV4dCBtYWpvciB2ZXJzaW9uIG9mIGBkZWJ1Z2AuJ1xuKTtcblxuLyoqXG4gKiBDb2xvcnMuXG4gKi9cblxuZXhwb3J0cy5jb2xvcnMgPSBbNiwgMiwgMywgNCwgNSwgMV07XG5cbnRyeSB7XG5cdC8vIE9wdGlvbmFsIGRlcGVuZGVuY3kgKGFzIGluLCBkb2Vzbid0IG5lZWQgdG8gYmUgaW5zdGFsbGVkLCBOT1QgbGlrZSBvcHRpb25hbERlcGVuZGVuY2llcyBpbiBwYWNrYWdlLmpzb24pXG5cdC8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBpbXBvcnQvbm8tZXh0cmFuZW91cy1kZXBlbmRlbmNpZXNcblx0Y29uc3Qgc3VwcG9ydHNDb2xvciA9IHJlcXVpcmUoJ3N1cHBvcnRzLWNvbG9yJyk7XG5cblx0aWYgKHN1cHBvcnRzQ29sb3IgJiYgKHN1cHBvcnRzQ29sb3Iuc3RkZXJyIHx8IHN1cHBvcnRzQ29sb3IpLmxldmVsID49IDIpIHtcblx0XHRleHBvcnRzLmNvbG9ycyA9IFtcblx0XHRcdDIwLFxuXHRcdFx0MjEsXG5cdFx0XHQyNixcblx0XHRcdDI3LFxuXHRcdFx0MzIsXG5cdFx0XHQzMyxcblx0XHRcdDM4LFxuXHRcdFx0MzksXG5cdFx0XHQ0MCxcblx0XHRcdDQxLFxuXHRcdFx0NDIsXG5cdFx0XHQ0Myxcblx0XHRcdDQ0LFxuXHRcdFx0NDUsXG5cdFx0XHQ1Nixcblx0XHRcdDU3LFxuXHRcdFx0NjIsXG5cdFx0XHQ2Myxcblx0XHRcdDY4LFxuXHRcdFx0NjksXG5cdFx0XHQ3NCxcblx0XHRcdDc1LFxuXHRcdFx0NzYsXG5cdFx0XHQ3Nyxcblx0XHRcdDc4LFxuXHRcdFx0NzksXG5cdFx0XHQ4MCxcblx0XHRcdDgxLFxuXHRcdFx0OTIsXG5cdFx0XHQ5Myxcblx0XHRcdDk4LFxuXHRcdFx0OTksXG5cdFx0XHQxMTIsXG5cdFx0XHQxMTMsXG5cdFx0XHQxMjgsXG5cdFx0XHQxMjksXG5cdFx0XHQxMzQsXG5cdFx0XHQxMzUsXG5cdFx0XHQxNDgsXG5cdFx0XHQxNDksXG5cdFx0XHQxNjAsXG5cdFx0XHQxNjEsXG5cdFx0XHQxNjIsXG5cdFx0XHQxNjMsXG5cdFx0XHQxNjQsXG5cdFx0XHQxNjUsXG5cdFx0XHQxNjYsXG5cdFx0XHQxNjcsXG5cdFx0XHQxNjgsXG5cdFx0XHQxNjksXG5cdFx0XHQxNzAsXG5cdFx0XHQxNzEsXG5cdFx0XHQxNzIsXG5cdFx0XHQxNzMsXG5cdFx0XHQxNzgsXG5cdFx0XHQxNzksXG5cdFx0XHQxODQsXG5cdFx0XHQxODUsXG5cdFx0XHQxOTYsXG5cdFx0XHQxOTcsXG5cdFx0XHQxOTgsXG5cdFx0XHQxOTksXG5cdFx0XHQyMDAsXG5cdFx0XHQyMDEsXG5cdFx0XHQyMDIsXG5cdFx0XHQyMDMsXG5cdFx0XHQyMDQsXG5cdFx0XHQyMDUsXG5cdFx0XHQyMDYsXG5cdFx0XHQyMDcsXG5cdFx0XHQyMDgsXG5cdFx0XHQyMDksXG5cdFx0XHQyMTQsXG5cdFx0XHQyMTUsXG5cdFx0XHQyMjAsXG5cdFx0XHQyMjFcblx0XHRdO1xuXHR9XG59IGNhdGNoIChlcnJvcikge1xuXHQvLyBTd2FsbG93IC0gd2Ugb25seSBjYXJlIGlmIGBzdXBwb3J0cy1jb2xvcmAgaXMgYXZhaWxhYmxlOyBpdCBkb2Vzbid0IGhhdmUgdG8gYmUuXG59XG5cbi8qKlxuICogQnVpbGQgdXAgdGhlIGRlZmF1bHQgYGluc3BlY3RPcHRzYCBvYmplY3QgZnJvbSB0aGUgZW52aXJvbm1lbnQgdmFyaWFibGVzLlxuICpcbiAqICAgJCBERUJVR19DT0xPUlM9bm8gREVCVUdfREVQVEg9MTAgREVCVUdfU0hPV19ISURERU49ZW5hYmxlZCBub2RlIHNjcmlwdC5qc1xuICovXG5cbmV4cG9ydHMuaW5zcGVjdE9wdHMgPSBPYmplY3Qua2V5cyhwcm9jZXNzLmVudikuZmlsdGVyKGtleSA9PiB7XG5cdHJldHVybiAvXmRlYnVnXy9pLnRlc3Qoa2V5KTtcbn0pLnJlZHVjZSgob2JqLCBrZXkpID0+IHtcblx0Ly8gQ2FtZWwtY2FzZVxuXHRjb25zdCBwcm9wID0ga2V5XG5cdFx0LnN1YnN0cmluZyg2KVxuXHRcdC50b0xvd2VyQ2FzZSgpXG5cdFx0LnJlcGxhY2UoL18oW2Etel0pL2csIChfLCBrKSA9PiB7XG5cdFx0XHRyZXR1cm4gay50b1VwcGVyQ2FzZSgpO1xuXHRcdH0pO1xuXG5cdC8vIENvZXJjZSBzdHJpbmcgdmFsdWUgaW50byBKUyB2YWx1ZVxuXHRsZXQgdmFsID0gcHJvY2Vzcy5lbnZba2V5XTtcblx0aWYgKC9eKHllc3xvbnx0cnVlfGVuYWJsZWQpJC9pLnRlc3QodmFsKSkge1xuXHRcdHZhbCA9IHRydWU7XG5cdH0gZWxzZSBpZiAoL14obm98b2ZmfGZhbHNlfGRpc2FibGVkKSQvaS50ZXN0KHZhbCkpIHtcblx0XHR2YWwgPSBmYWxzZTtcblx0fSBlbHNlIGlmICh2YWwgPT09ICdudWxsJykge1xuXHRcdHZhbCA9IG51bGw7XG5cdH0gZWxzZSB7XG5cdFx0dmFsID0gTnVtYmVyKHZhbCk7XG5cdH1cblxuXHRvYmpbcHJvcF0gPSB2YWw7XG5cdHJldHVybiBvYmo7XG59LCB7fSk7XG5cbi8qKlxuICogSXMgc3Rkb3V0IGEgVFRZPyBDb2xvcmVkIG91dHB1dCBpcyBlbmFibGVkIHdoZW4gYHRydWVgLlxuICovXG5cbmZ1bmN0aW9uIHVzZUNvbG9ycygpIHtcblx0cmV0dXJuICdjb2xvcnMnIGluIGV4cG9ydHMuaW5zcGVjdE9wdHMgP1xuXHRcdEJvb2xlYW4oZXhwb3J0cy5pbnNwZWN0T3B0cy5jb2xvcnMpIDpcblx0XHR0dHkuaXNhdHR5KHByb2Nlc3Muc3RkZXJyLmZkKTtcbn1cblxuLyoqXG4gKiBBZGRzIEFOU0kgY29sb3IgZXNjYXBlIGNvZGVzIGlmIGVuYWJsZWQuXG4gKlxuICogQGFwaSBwdWJsaWNcbiAqL1xuXG5mdW5jdGlvbiBmb3JtYXRBcmdzKGFyZ3MpIHtcblx0Y29uc3Qge25hbWVzcGFjZTogbmFtZSwgdXNlQ29sb3JzfSA9IHRoaXM7XG5cblx0aWYgKHVzZUNvbG9ycykge1xuXHRcdGNvbnN0IGMgPSB0aGlzLmNvbG9yO1xuXHRcdGNvbnN0IGNvbG9yQ29kZSA9ICdcXHUwMDFCWzMnICsgKGMgPCA4ID8gYyA6ICc4OzU7JyArIGMpO1xuXHRcdGNvbnN0IHByZWZpeCA9IGAgICR7Y29sb3JDb2RlfTsxbSR7bmFtZX0gXFx1MDAxQlswbWA7XG5cblx0XHRhcmdzWzBdID0gcHJlZml4ICsgYXJnc1swXS5zcGxpdCgnXFxuJykuam9pbignXFxuJyArIHByZWZpeCk7XG5cdFx0YXJncy5wdXNoKGNvbG9yQ29kZSArICdtKycgKyBtb2R1bGUuZXhwb3J0cy5odW1hbml6ZSh0aGlzLmRpZmYpICsgJ1xcdTAwMUJbMG0nKTtcblx0fSBlbHNlIHtcblx0XHRhcmdzWzBdID0gZ2V0RGF0ZSgpICsgbmFtZSArICcgJyArIGFyZ3NbMF07XG5cdH1cbn1cblxuZnVuY3Rpb24gZ2V0RGF0ZSgpIHtcblx0aWYgKGV4cG9ydHMuaW5zcGVjdE9wdHMuaGlkZURhdGUpIHtcblx0XHRyZXR1cm4gJyc7XG5cdH1cblx0cmV0dXJuIG5ldyBEYXRlKCkudG9JU09TdHJpbmcoKSArICcgJztcbn1cblxuLyoqXG4gKiBJbnZva2VzIGB1dGlsLmZvcm1hdCgpYCB3aXRoIHRoZSBzcGVjaWZpZWQgYXJndW1lbnRzIGFuZCB3cml0ZXMgdG8gc3RkZXJyLlxuICovXG5cbmZ1bmN0aW9uIGxvZyguLi5hcmdzKSB7XG5cdHJldHVybiBwcm9jZXNzLnN0ZGVyci53cml0ZSh1dGlsLmZvcm1hdCguLi5hcmdzKSArICdcXG4nKTtcbn1cblxuLyoqXG4gKiBTYXZlIGBuYW1lc3BhY2VzYC5cbiAqXG4gKiBAcGFyYW0ge1N0cmluZ30gbmFtZXNwYWNlc1xuICogQGFwaSBwcml2YXRlXG4gKi9cbmZ1bmN0aW9uIHNhdmUobmFtZXNwYWNlcykge1xuXHRpZiAobmFtZXNwYWNlcykge1xuXHRcdHByb2Nlc3MuZW52LkRFQlVHID0gbmFtZXNwYWNlcztcblx0fSBlbHNlIHtcblx0XHQvLyBJZiB5b3Ugc2V0IGEgcHJvY2Vzcy5lbnYgZmllbGQgdG8gbnVsbCBvciB1bmRlZmluZWQsIGl0IGdldHMgY2FzdCB0byB0aGVcblx0XHQvLyBzdHJpbmcgJ251bGwnIG9yICd1bmRlZmluZWQnLiBKdXN0IGRlbGV0ZSBpbnN0ZWFkLlxuXHRcdGRlbGV0ZSBwcm9jZXNzLmVudi5ERUJVRztcblx0fVxufVxuXG4vKipcbiAqIExvYWQgYG5hbWVzcGFjZXNgLlxuICpcbiAqIEByZXR1cm4ge1N0cmluZ30gcmV0dXJucyB0aGUgcHJldmlvdXNseSBwZXJzaXN0ZWQgZGVidWcgbW9kZXNcbiAqIEBhcGkgcHJpdmF0ZVxuICovXG5cbmZ1bmN0aW9uIGxvYWQoKSB7XG5cdHJldHVybiBwcm9jZXNzLmVudi5ERUJVRztcbn1cblxuLyoqXG4gKiBJbml0IGxvZ2ljIGZvciBgZGVidWdgIGluc3RhbmNlcy5cbiAqXG4gKiBDcmVhdGUgYSBuZXcgYGluc3BlY3RPcHRzYCBvYmplY3QgaW4gY2FzZSBgdXNlQ29sb3JzYCBpcyBzZXRcbiAqIGRpZmZlcmVudGx5IGZvciBhIHBhcnRpY3VsYXIgYGRlYnVnYCBpbnN0YW5jZS5cbiAqL1xuXG5mdW5jdGlvbiBpbml0KGRlYnVnKSB7XG5cdGRlYnVnLmluc3BlY3RPcHRzID0ge307XG5cblx0Y29uc3Qga2V5cyA9IE9iamVjdC5rZXlzKGV4cG9ydHMuaW5zcGVjdE9wdHMpO1xuXHRmb3IgKGxldCBpID0gMDsgaSA8IGtleXMubGVuZ3RoOyBpKyspIHtcblx0XHRkZWJ1Zy5pbnNwZWN0T3B0c1trZXlzW2ldXSA9IGV4cG9ydHMuaW5zcGVjdE9wdHNba2V5c1tpXV07XG5cdH1cbn1cblxubW9kdWxlLmV4cG9ydHMgPSByZXF1aXJlKCcuL2NvbW1vbicpKGV4cG9ydHMpO1xuXG5jb25zdCB7Zm9ybWF0dGVyc30gPSBtb2R1bGUuZXhwb3J0cztcblxuLyoqXG4gKiBNYXAgJW8gdG8gYHV0aWwuaW5zcGVjdCgpYCwgYWxsIG9uIGEgc2luZ2xlIGxpbmUuXG4gKi9cblxuZm9ybWF0dGVycy5vID0gZnVuY3Rpb24gKHYpIHtcblx0dGhpcy5pbnNwZWN0T3B0cy5jb2xvcnMgPSB0aGlzLnVzZUNvbG9ycztcblx0cmV0dXJuIHV0aWwuaW5zcGVjdCh2LCB0aGlzLmluc3BlY3RPcHRzKVxuXHRcdC5zcGxpdCgnXFxuJylcblx0XHQubWFwKHN0ciA9PiBzdHIudHJpbSgpKVxuXHRcdC5qb2luKCcgJyk7XG59O1xuXG4vKipcbiAqIE1hcCAlTyB0byBgdXRpbC5pbnNwZWN0KClgLCBhbGxvd2luZyBtdWx0aXBsZSBsaW5lcyBpZiBuZWVkZWQuXG4gKi9cblxuZm9ybWF0dGVycy5PID0gZnVuY3Rpb24gKHYpIHtcblx0dGhpcy5pbnNwZWN0T3B0cy5jb2xvcnMgPSB0aGlzLnVzZUNvbG9ycztcblx0cmV0dXJuIHV0aWwuaW5zcGVjdCh2LCB0aGlzLmluc3BlY3RPcHRzKTtcbn07XG4iLCJ2YXIgU3RyZWFtID0gcmVxdWlyZSgnc3RyZWFtJykuU3RyZWFtO1xudmFyIHV0aWwgPSByZXF1aXJlKCd1dGlsJyk7XG5cbm1vZHVsZS5leHBvcnRzID0gRGVsYXllZFN0cmVhbTtcbmZ1bmN0aW9uIERlbGF5ZWRTdHJlYW0oKSB7XG4gIHRoaXMuc291cmNlID0gbnVsbDtcbiAgdGhpcy5kYXRhU2l6ZSA9IDA7XG4gIHRoaXMubWF4RGF0YVNpemUgPSAxMDI0ICogMTAyNDtcbiAgdGhpcy5wYXVzZVN0cmVhbSA9IHRydWU7XG5cbiAgdGhpcy5fbWF4RGF0YVNpemVFeGNlZWRlZCA9IGZhbHNlO1xuICB0aGlzLl9yZWxlYXNlZCA9IGZhbHNlO1xuICB0aGlzLl9idWZmZXJlZEV2ZW50cyA9IFtdO1xufVxudXRpbC5pbmhlcml0cyhEZWxheWVkU3RyZWFtLCBTdHJlYW0pO1xuXG5EZWxheWVkU3RyZWFtLmNyZWF0ZSA9IGZ1bmN0aW9uKHNvdXJjZSwgb3B0aW9ucykge1xuICB2YXIgZGVsYXllZFN0cmVhbSA9IG5ldyB0aGlzKCk7XG5cbiAgb3B0aW9ucyA9IG9wdGlvbnMgfHwge307XG4gIGZvciAodmFyIG9wdGlvbiBpbiBvcHRpb25zKSB7XG4gICAgZGVsYXllZFN0cmVhbVtvcHRpb25dID0gb3B0aW9uc1tvcHRpb25dO1xuICB9XG5cbiAgZGVsYXllZFN0cmVhbS5zb3VyY2UgPSBzb3VyY2U7XG5cbiAgdmFyIHJlYWxFbWl0ID0gc291cmNlLmVtaXQ7XG4gIHNvdXJjZS5lbWl0ID0gZnVuY3Rpb24oKSB7XG4gICAgZGVsYXllZFN0cmVhbS5faGFuZGxlRW1pdChhcmd1bWVudHMpO1xuICAgIHJldHVybiByZWFsRW1pdC5hcHBseShzb3VyY2UsIGFyZ3VtZW50cyk7XG4gIH07XG5cbiAgc291cmNlLm9uKCdlcnJvcicsIGZ1bmN0aW9uKCkge30pO1xuICBpZiAoZGVsYXllZFN0cmVhbS5wYXVzZVN0cmVhbSkge1xuICAgIHNvdXJjZS5wYXVzZSgpO1xuICB9XG5cbiAgcmV0dXJuIGRlbGF5ZWRTdHJlYW07XG59O1xuXG5PYmplY3QuZGVmaW5lUHJvcGVydHkoRGVsYXllZFN0cmVhbS5wcm90b3R5cGUsICdyZWFkYWJsZScsIHtcbiAgY29uZmlndXJhYmxlOiB0cnVlLFxuICBlbnVtZXJhYmxlOiB0cnVlLFxuICBnZXQ6IGZ1bmN0aW9uKCkge1xuICAgIHJldHVybiB0aGlzLnNvdXJjZS5yZWFkYWJsZTtcbiAgfVxufSk7XG5cbkRlbGF5ZWRTdHJlYW0ucHJvdG90eXBlLnNldEVuY29kaW5nID0gZnVuY3Rpb24oKSB7XG4gIHJldHVybiB0aGlzLnNvdXJjZS5zZXRFbmNvZGluZy5hcHBseSh0aGlzLnNvdXJjZSwgYXJndW1lbnRzKTtcbn07XG5cbkRlbGF5ZWRTdHJlYW0ucHJvdG90eXBlLnJlc3VtZSA9IGZ1bmN0aW9uKCkge1xuICBpZiAoIXRoaXMuX3JlbGVhc2VkKSB7XG4gICAgdGhpcy5yZWxlYXNlKCk7XG4gIH1cblxuICB0aGlzLnNvdXJjZS5yZXN1bWUoKTtcbn07XG5cbkRlbGF5ZWRTdHJlYW0ucHJvdG90eXBlLnBhdXNlID0gZnVuY3Rpb24oKSB7XG4gIHRoaXMuc291cmNlLnBhdXNlKCk7XG59O1xuXG5EZWxheWVkU3RyZWFtLnByb3RvdHlwZS5yZWxlYXNlID0gZnVuY3Rpb24oKSB7XG4gIHRoaXMuX3JlbGVhc2VkID0gdHJ1ZTtcblxuICB0aGlzLl9idWZmZXJlZEV2ZW50cy5mb3JFYWNoKGZ1bmN0aW9uKGFyZ3MpIHtcbiAgICB0aGlzLmVtaXQuYXBwbHkodGhpcywgYXJncyk7XG4gIH0uYmluZCh0aGlzKSk7XG4gIHRoaXMuX2J1ZmZlcmVkRXZlbnRzID0gW107XG59O1xuXG5EZWxheWVkU3RyZWFtLnByb3RvdHlwZS5waXBlID0gZnVuY3Rpb24oKSB7XG4gIHZhciByID0gU3RyZWFtLnByb3RvdHlwZS5waXBlLmFwcGx5KHRoaXMsIGFyZ3VtZW50cyk7XG4gIHRoaXMucmVzdW1lKCk7XG4gIHJldHVybiByO1xufTtcblxuRGVsYXllZFN0cmVhbS5wcm90b3R5cGUuX2hhbmRsZUVtaXQgPSBmdW5jdGlvbihhcmdzKSB7XG4gIGlmICh0aGlzLl9yZWxlYXNlZCkge1xuICAgIHRoaXMuZW1pdC5hcHBseSh0aGlzLCBhcmdzKTtcbiAgICByZXR1cm47XG4gIH1cblxuICBpZiAoYXJnc1swXSA9PT0gJ2RhdGEnKSB7XG4gICAgdGhpcy5kYXRhU2l6ZSArPSBhcmdzWzFdLmxlbmd0aDtcbiAgICB0aGlzLl9jaGVja0lmTWF4RGF0YVNpemVFeGNlZWRlZCgpO1xuICB9XG5cbiAgdGhpcy5fYnVmZmVyZWRFdmVudHMucHVzaChhcmdzKTtcbn07XG5cbkRlbGF5ZWRTdHJlYW0ucHJvdG90eXBlLl9jaGVja0lmTWF4RGF0YVNpemVFeGNlZWRlZCA9IGZ1bmN0aW9uKCkge1xuICBpZiAodGhpcy5fbWF4RGF0YVNpemVFeGNlZWRlZCkge1xuICAgIHJldHVybjtcbiAgfVxuXG4gIGlmICh0aGlzLmRhdGFTaXplIDw9IHRoaXMubWF4RGF0YVNpemUpIHtcbiAgICByZXR1cm47XG4gIH1cblxuICB0aGlzLl9tYXhEYXRhU2l6ZUV4Y2VlZGVkID0gdHJ1ZTtcbiAgdmFyIG1lc3NhZ2UgPVxuICAgICdEZWxheWVkU3RyZWFtI21heERhdGFTaXplIG9mICcgKyB0aGlzLm1heERhdGFTaXplICsgJyBieXRlcyBleGNlZWRlZC4nXG4gIHRoaXMuZW1pdCgnZXJyb3InLCBuZXcgRXJyb3IobWVzc2FnZSkpO1xufTtcbiIsInZhciBkZWJ1ZztcblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiAoKSB7XG4gIGlmICghZGVidWcpIHtcbiAgICB0cnkge1xuICAgICAgLyogZXNsaW50IGdsb2JhbC1yZXF1aXJlOiBvZmYgKi9cbiAgICAgIGRlYnVnID0gcmVxdWlyZShcImRlYnVnXCIpKFwiZm9sbG93LXJlZGlyZWN0c1wiKTtcbiAgICB9XG4gICAgY2F0Y2ggKGVycm9yKSB7IC8qICovIH1cbiAgICBpZiAodHlwZW9mIGRlYnVnICE9PSBcImZ1bmN0aW9uXCIpIHtcbiAgICAgIGRlYnVnID0gZnVuY3Rpb24gKCkgeyAvKiAqLyB9O1xuICAgIH1cbiAgfVxuICBkZWJ1Zy5hcHBseShudWxsLCBhcmd1bWVudHMpO1xufTtcbiIsInZhciB1cmwgPSByZXF1aXJlKFwidXJsXCIpO1xudmFyIFVSTCA9IHVybC5VUkw7XG52YXIgaHR0cCA9IHJlcXVpcmUoXCJodHRwXCIpO1xudmFyIGh0dHBzID0gcmVxdWlyZShcImh0dHBzXCIpO1xudmFyIFdyaXRhYmxlID0gcmVxdWlyZShcInN0cmVhbVwiKS5Xcml0YWJsZTtcbnZhciBhc3NlcnQgPSByZXF1aXJlKFwiYXNzZXJ0XCIpO1xudmFyIGRlYnVnID0gcmVxdWlyZShcIi4vZGVidWdcIik7XG5cbi8vIFdoZXRoZXIgdG8gdXNlIHRoZSBuYXRpdmUgVVJMIG9iamVjdCBvciB0aGUgbGVnYWN5IHVybCBtb2R1bGVcbnZhciB1c2VOYXRpdmVVUkwgPSBmYWxzZTtcbnRyeSB7XG4gIGFzc2VydChuZXcgVVJMKCkpO1xufVxuY2F0Y2ggKGVycm9yKSB7XG4gIHVzZU5hdGl2ZVVSTCA9IGVycm9yLmNvZGUgPT09IFwiRVJSX0lOVkFMSURfVVJMXCI7XG59XG5cbi8vIFVSTCBmaWVsZHMgdG8gcHJlc2VydmUgaW4gY29weSBvcGVyYXRpb25zXG52YXIgcHJlc2VydmVkVXJsRmllbGRzID0gW1xuICBcImF1dGhcIixcbiAgXCJob3N0XCIsXG4gIFwiaG9zdG5hbWVcIixcbiAgXCJocmVmXCIsXG4gIFwicGF0aFwiLFxuICBcInBhdGhuYW1lXCIsXG4gIFwicG9ydFwiLFxuICBcInByb3RvY29sXCIsXG4gIFwicXVlcnlcIixcbiAgXCJzZWFyY2hcIixcbiAgXCJoYXNoXCIsXG5dO1xuXG4vLyBDcmVhdGUgaGFuZGxlcnMgdGhhdCBwYXNzIGV2ZW50cyBmcm9tIG5hdGl2ZSByZXF1ZXN0c1xudmFyIGV2ZW50cyA9IFtcImFib3J0XCIsIFwiYWJvcnRlZFwiLCBcImNvbm5lY3RcIiwgXCJlcnJvclwiLCBcInNvY2tldFwiLCBcInRpbWVvdXRcIl07XG52YXIgZXZlbnRIYW5kbGVycyA9IE9iamVjdC5jcmVhdGUobnVsbCk7XG5ldmVudHMuZm9yRWFjaChmdW5jdGlvbiAoZXZlbnQpIHtcbiAgZXZlbnRIYW5kbGVyc1tldmVudF0gPSBmdW5jdGlvbiAoYXJnMSwgYXJnMiwgYXJnMykge1xuICAgIHRoaXMuX3JlZGlyZWN0YWJsZS5lbWl0KGV2ZW50LCBhcmcxLCBhcmcyLCBhcmczKTtcbiAgfTtcbn0pO1xuXG4vLyBFcnJvciB0eXBlcyB3aXRoIGNvZGVzXG52YXIgSW52YWxpZFVybEVycm9yID0gY3JlYXRlRXJyb3JUeXBlKFxuICBcIkVSUl9JTlZBTElEX1VSTFwiLFxuICBcIkludmFsaWQgVVJMXCIsXG4gIFR5cGVFcnJvclxuKTtcbnZhciBSZWRpcmVjdGlvbkVycm9yID0gY3JlYXRlRXJyb3JUeXBlKFxuICBcIkVSUl9GUl9SRURJUkVDVElPTl9GQUlMVVJFXCIsXG4gIFwiUmVkaXJlY3RlZCByZXF1ZXN0IGZhaWxlZFwiXG4pO1xudmFyIFRvb01hbnlSZWRpcmVjdHNFcnJvciA9IGNyZWF0ZUVycm9yVHlwZShcbiAgXCJFUlJfRlJfVE9PX01BTllfUkVESVJFQ1RTXCIsXG4gIFwiTWF4aW11bSBudW1iZXIgb2YgcmVkaXJlY3RzIGV4Y2VlZGVkXCIsXG4gIFJlZGlyZWN0aW9uRXJyb3Jcbik7XG52YXIgTWF4Qm9keUxlbmd0aEV4Y2VlZGVkRXJyb3IgPSBjcmVhdGVFcnJvclR5cGUoXG4gIFwiRVJSX0ZSX01BWF9CT0RZX0xFTkdUSF9FWENFRURFRFwiLFxuICBcIlJlcXVlc3QgYm9keSBsYXJnZXIgdGhhbiBtYXhCb2R5TGVuZ3RoIGxpbWl0XCJcbik7XG52YXIgV3JpdGVBZnRlckVuZEVycm9yID0gY3JlYXRlRXJyb3JUeXBlKFxuICBcIkVSUl9TVFJFQU1fV1JJVEVfQUZURVJfRU5EXCIsXG4gIFwid3JpdGUgYWZ0ZXIgZW5kXCJcbik7XG5cbi8vIGlzdGFuYnVsIGlnbm9yZSBuZXh0XG52YXIgZGVzdHJveSA9IFdyaXRhYmxlLnByb3RvdHlwZS5kZXN0cm95IHx8IG5vb3A7XG5cbi8vIEFuIEhUVFAoUykgcmVxdWVzdCB0aGF0IGNhbiBiZSByZWRpcmVjdGVkXG5mdW5jdGlvbiBSZWRpcmVjdGFibGVSZXF1ZXN0KG9wdGlvbnMsIHJlc3BvbnNlQ2FsbGJhY2spIHtcbiAgLy8gSW5pdGlhbGl6ZSB0aGUgcmVxdWVzdFxuICBXcml0YWJsZS5jYWxsKHRoaXMpO1xuICB0aGlzLl9zYW5pdGl6ZU9wdGlvbnMob3B0aW9ucyk7XG4gIHRoaXMuX29wdGlvbnMgPSBvcHRpb25zO1xuICB0aGlzLl9lbmRlZCA9IGZhbHNlO1xuICB0aGlzLl9lbmRpbmcgPSBmYWxzZTtcbiAgdGhpcy5fcmVkaXJlY3RDb3VudCA9IDA7XG4gIHRoaXMuX3JlZGlyZWN0cyA9IFtdO1xuICB0aGlzLl9yZXF1ZXN0Qm9keUxlbmd0aCA9IDA7XG4gIHRoaXMuX3JlcXVlc3RCb2R5QnVmZmVycyA9IFtdO1xuXG4gIC8vIEF0dGFjaCBhIGNhbGxiYWNrIGlmIHBhc3NlZFxuICBpZiAocmVzcG9uc2VDYWxsYmFjaykge1xuICAgIHRoaXMub24oXCJyZXNwb25zZVwiLCByZXNwb25zZUNhbGxiYWNrKTtcbiAgfVxuXG4gIC8vIFJlYWN0IHRvIHJlc3BvbnNlcyBvZiBuYXRpdmUgcmVxdWVzdHNcbiAgdmFyIHNlbGYgPSB0aGlzO1xuICB0aGlzLl9vbk5hdGl2ZVJlc3BvbnNlID0gZnVuY3Rpb24gKHJlc3BvbnNlKSB7XG4gICAgdHJ5IHtcbiAgICAgIHNlbGYuX3Byb2Nlc3NSZXNwb25zZShyZXNwb25zZSk7XG4gICAgfVxuICAgIGNhdGNoIChjYXVzZSkge1xuICAgICAgc2VsZi5lbWl0KFwiZXJyb3JcIiwgY2F1c2UgaW5zdGFuY2VvZiBSZWRpcmVjdGlvbkVycm9yID9cbiAgICAgICAgY2F1c2UgOiBuZXcgUmVkaXJlY3Rpb25FcnJvcih7IGNhdXNlOiBjYXVzZSB9KSk7XG4gICAgfVxuICB9O1xuXG4gIC8vIFBlcmZvcm0gdGhlIGZpcnN0IHJlcXVlc3RcbiAgdGhpcy5fcGVyZm9ybVJlcXVlc3QoKTtcbn1cblJlZGlyZWN0YWJsZVJlcXVlc3QucHJvdG90eXBlID0gT2JqZWN0LmNyZWF0ZShXcml0YWJsZS5wcm90b3R5cGUpO1xuXG5SZWRpcmVjdGFibGVSZXF1ZXN0LnByb3RvdHlwZS5hYm9ydCA9IGZ1bmN0aW9uICgpIHtcbiAgZGVzdHJveVJlcXVlc3QodGhpcy5fY3VycmVudFJlcXVlc3QpO1xuICB0aGlzLl9jdXJyZW50UmVxdWVzdC5hYm9ydCgpO1xuICB0aGlzLmVtaXQoXCJhYm9ydFwiKTtcbn07XG5cblJlZGlyZWN0YWJsZVJlcXVlc3QucHJvdG90eXBlLmRlc3Ryb3kgPSBmdW5jdGlvbiAoZXJyb3IpIHtcbiAgZGVzdHJveVJlcXVlc3QodGhpcy5fY3VycmVudFJlcXVlc3QsIGVycm9yKTtcbiAgZGVzdHJveS5jYWxsKHRoaXMsIGVycm9yKTtcbiAgcmV0dXJuIHRoaXM7XG59O1xuXG4vLyBXcml0ZXMgYnVmZmVyZWQgZGF0YSB0byB0aGUgY3VycmVudCBuYXRpdmUgcmVxdWVzdFxuUmVkaXJlY3RhYmxlUmVxdWVzdC5wcm90b3R5cGUud3JpdGUgPSBmdW5jdGlvbiAoZGF0YSwgZW5jb2RpbmcsIGNhbGxiYWNrKSB7XG4gIC8vIFdyaXRpbmcgaXMgbm90IGFsbG93ZWQgaWYgZW5kIGhhcyBiZWVuIGNhbGxlZFxuICBpZiAodGhpcy5fZW5kaW5nKSB7XG4gICAgdGhyb3cgbmV3IFdyaXRlQWZ0ZXJFbmRFcnJvcigpO1xuICB9XG5cbiAgLy8gVmFsaWRhdGUgaW5wdXQgYW5kIHNoaWZ0IHBhcmFtZXRlcnMgaWYgbmVjZXNzYXJ5XG4gIGlmICghaXNTdHJpbmcoZGF0YSkgJiYgIWlzQnVmZmVyKGRhdGEpKSB7XG4gICAgdGhyb3cgbmV3IFR5cGVFcnJvcihcImRhdGEgc2hvdWxkIGJlIGEgc3RyaW5nLCBCdWZmZXIgb3IgVWludDhBcnJheVwiKTtcbiAgfVxuICBpZiAoaXNGdW5jdGlvbihlbmNvZGluZykpIHtcbiAgICBjYWxsYmFjayA9IGVuY29kaW5nO1xuICAgIGVuY29kaW5nID0gbnVsbDtcbiAgfVxuXG4gIC8vIElnbm9yZSBlbXB0eSBidWZmZXJzLCBzaW5jZSB3cml0aW5nIHRoZW0gZG9lc24ndCBpbnZva2UgdGhlIGNhbGxiYWNrXG4gIC8vIGh0dHBzOi8vZ2l0aHViLmNvbS9ub2RlanMvbm9kZS9pc3N1ZXMvMjIwNjZcbiAgaWYgKGRhdGEubGVuZ3RoID09PSAwKSB7XG4gICAgaWYgKGNhbGxiYWNrKSB7XG4gICAgICBjYWxsYmFjaygpO1xuICAgIH1cbiAgICByZXR1cm47XG4gIH1cbiAgLy8gT25seSB3cml0ZSB3aGVuIHdlIGRvbid0IGV4Y2VlZCB0aGUgbWF4aW11bSBib2R5IGxlbmd0aFxuICBpZiAodGhpcy5fcmVxdWVzdEJvZHlMZW5ndGggKyBkYXRhLmxlbmd0aCA8PSB0aGlzLl9vcHRpb25zLm1heEJvZHlMZW5ndGgpIHtcbiAgICB0aGlzLl9yZXF1ZXN0Qm9keUxlbmd0aCArPSBkYXRhLmxlbmd0aDtcbiAgICB0aGlzLl9yZXF1ZXN0Qm9keUJ1ZmZlcnMucHVzaCh7IGRhdGE6IGRhdGEsIGVuY29kaW5nOiBlbmNvZGluZyB9KTtcbiAgICB0aGlzLl9jdXJyZW50UmVxdWVzdC53cml0ZShkYXRhLCBlbmNvZGluZywgY2FsbGJhY2spO1xuICB9XG4gIC8vIEVycm9yIHdoZW4gd2UgZXhjZWVkIHRoZSBtYXhpbXVtIGJvZHkgbGVuZ3RoXG4gIGVsc2Uge1xuICAgIHRoaXMuZW1pdChcImVycm9yXCIsIG5ldyBNYXhCb2R5TGVuZ3RoRXhjZWVkZWRFcnJvcigpKTtcbiAgICB0aGlzLmFib3J0KCk7XG4gIH1cbn07XG5cbi8vIEVuZHMgdGhlIGN1cnJlbnQgbmF0aXZlIHJlcXVlc3RcblJlZGlyZWN0YWJsZVJlcXVlc3QucHJvdG90eXBlLmVuZCA9IGZ1bmN0aW9uIChkYXRhLCBlbmNvZGluZywgY2FsbGJhY2spIHtcbiAgLy8gU2hpZnQgcGFyYW1ldGVycyBpZiBuZWNlc3NhcnlcbiAgaWYgKGlzRnVuY3Rpb24oZGF0YSkpIHtcbiAgICBjYWxsYmFjayA9IGRhdGE7XG4gICAgZGF0YSA9IGVuY29kaW5nID0gbnVsbDtcbiAgfVxuICBlbHNlIGlmIChpc0Z1bmN0aW9uKGVuY29kaW5nKSkge1xuICAgIGNhbGxiYWNrID0gZW5jb2Rpbmc7XG4gICAgZW5jb2RpbmcgPSBudWxsO1xuICB9XG5cbiAgLy8gV3JpdGUgZGF0YSBpZiBuZWVkZWQgYW5kIGVuZFxuICBpZiAoIWRhdGEpIHtcbiAgICB0aGlzLl9lbmRlZCA9IHRoaXMuX2VuZGluZyA9IHRydWU7XG4gICAgdGhpcy5fY3VycmVudFJlcXVlc3QuZW5kKG51bGwsIG51bGwsIGNhbGxiYWNrKTtcbiAgfVxuICBlbHNlIHtcbiAgICB2YXIgc2VsZiA9IHRoaXM7XG4gICAgdmFyIGN1cnJlbnRSZXF1ZXN0ID0gdGhpcy5fY3VycmVudFJlcXVlc3Q7XG4gICAgdGhpcy53cml0ZShkYXRhLCBlbmNvZGluZywgZnVuY3Rpb24gKCkge1xuICAgICAgc2VsZi5fZW5kZWQgPSB0cnVlO1xuICAgICAgY3VycmVudFJlcXVlc3QuZW5kKG51bGwsIG51bGwsIGNhbGxiYWNrKTtcbiAgICB9KTtcbiAgICB0aGlzLl9lbmRpbmcgPSB0cnVlO1xuICB9XG59O1xuXG4vLyBTZXRzIGEgaGVhZGVyIHZhbHVlIG9uIHRoZSBjdXJyZW50IG5hdGl2ZSByZXF1ZXN0XG5SZWRpcmVjdGFibGVSZXF1ZXN0LnByb3RvdHlwZS5zZXRIZWFkZXIgPSBmdW5jdGlvbiAobmFtZSwgdmFsdWUpIHtcbiAgdGhpcy5fb3B0aW9ucy5oZWFkZXJzW25hbWVdID0gdmFsdWU7XG4gIHRoaXMuX2N1cnJlbnRSZXF1ZXN0LnNldEhlYWRlcihuYW1lLCB2YWx1ZSk7XG59O1xuXG4vLyBDbGVhcnMgYSBoZWFkZXIgdmFsdWUgb24gdGhlIGN1cnJlbnQgbmF0aXZlIHJlcXVlc3RcblJlZGlyZWN0YWJsZVJlcXVlc3QucHJvdG90eXBlLnJlbW92ZUhlYWRlciA9IGZ1bmN0aW9uIChuYW1lKSB7XG4gIGRlbGV0ZSB0aGlzLl9vcHRpb25zLmhlYWRlcnNbbmFtZV07XG4gIHRoaXMuX2N1cnJlbnRSZXF1ZXN0LnJlbW92ZUhlYWRlcihuYW1lKTtcbn07XG5cbi8vIEdsb2JhbCB0aW1lb3V0IGZvciBhbGwgdW5kZXJseWluZyByZXF1ZXN0c1xuUmVkaXJlY3RhYmxlUmVxdWVzdC5wcm90b3R5cGUuc2V0VGltZW91dCA9IGZ1bmN0aW9uIChtc2VjcywgY2FsbGJhY2spIHtcbiAgdmFyIHNlbGYgPSB0aGlzO1xuXG4gIC8vIERlc3Ryb3lzIHRoZSBzb2NrZXQgb24gdGltZW91dFxuICBmdW5jdGlvbiBkZXN0cm95T25UaW1lb3V0KHNvY2tldCkge1xuICAgIHNvY2tldC5zZXRUaW1lb3V0KG1zZWNzKTtcbiAgICBzb2NrZXQucmVtb3ZlTGlzdGVuZXIoXCJ0aW1lb3V0XCIsIHNvY2tldC5kZXN0cm95KTtcbiAgICBzb2NrZXQuYWRkTGlzdGVuZXIoXCJ0aW1lb3V0XCIsIHNvY2tldC5kZXN0cm95KTtcbiAgfVxuXG4gIC8vIFNldHMgdXAgYSB0aW1lciB0byB0cmlnZ2VyIGEgdGltZW91dCBldmVudFxuICBmdW5jdGlvbiBzdGFydFRpbWVyKHNvY2tldCkge1xuICAgIGlmIChzZWxmLl90aW1lb3V0KSB7XG4gICAgICBjbGVhclRpbWVvdXQoc2VsZi5fdGltZW91dCk7XG4gICAgfVxuICAgIHNlbGYuX3RpbWVvdXQgPSBzZXRUaW1lb3V0KGZ1bmN0aW9uICgpIHtcbiAgICAgIHNlbGYuZW1pdChcInRpbWVvdXRcIik7XG4gICAgICBjbGVhclRpbWVyKCk7XG4gICAgfSwgbXNlY3MpO1xuICAgIGRlc3Ryb3lPblRpbWVvdXQoc29ja2V0KTtcbiAgfVxuXG4gIC8vIFN0b3BzIGEgdGltZW91dCBmcm9tIHRyaWdnZXJpbmdcbiAgZnVuY3Rpb24gY2xlYXJUaW1lcigpIHtcbiAgICAvLyBDbGVhciB0aGUgdGltZW91dFxuICAgIGlmIChzZWxmLl90aW1lb3V0KSB7XG4gICAgICBjbGVhclRpbWVvdXQoc2VsZi5fdGltZW91dCk7XG4gICAgICBzZWxmLl90aW1lb3V0ID0gbnVsbDtcbiAgICB9XG5cbiAgICAvLyBDbGVhbiB1cCBhbGwgYXR0YWNoZWQgbGlzdGVuZXJzXG4gICAgc2VsZi5yZW1vdmVMaXN0ZW5lcihcImFib3J0XCIsIGNsZWFyVGltZXIpO1xuICAgIHNlbGYucmVtb3ZlTGlzdGVuZXIoXCJlcnJvclwiLCBjbGVhclRpbWVyKTtcbiAgICBzZWxmLnJlbW92ZUxpc3RlbmVyKFwicmVzcG9uc2VcIiwgY2xlYXJUaW1lcik7XG4gICAgc2VsZi5yZW1vdmVMaXN0ZW5lcihcImNsb3NlXCIsIGNsZWFyVGltZXIpO1xuICAgIGlmIChjYWxsYmFjaykge1xuICAgICAgc2VsZi5yZW1vdmVMaXN0ZW5lcihcInRpbWVvdXRcIiwgY2FsbGJhY2spO1xuICAgIH1cbiAgICBpZiAoIXNlbGYuc29ja2V0KSB7XG4gICAgICBzZWxmLl9jdXJyZW50UmVxdWVzdC5yZW1vdmVMaXN0ZW5lcihcInNvY2tldFwiLCBzdGFydFRpbWVyKTtcbiAgICB9XG4gIH1cblxuICAvLyBBdHRhY2ggY2FsbGJhY2sgaWYgcGFzc2VkXG4gIGlmIChjYWxsYmFjaykge1xuICAgIHRoaXMub24oXCJ0aW1lb3V0XCIsIGNhbGxiYWNrKTtcbiAgfVxuXG4gIC8vIFN0YXJ0IHRoZSB0aW1lciBpZiBvciB3aGVuIHRoZSBzb2NrZXQgaXMgb3BlbmVkXG4gIGlmICh0aGlzLnNvY2tldCkge1xuICAgIHN0YXJ0VGltZXIodGhpcy5zb2NrZXQpO1xuICB9XG4gIGVsc2Uge1xuICAgIHRoaXMuX2N1cnJlbnRSZXF1ZXN0Lm9uY2UoXCJzb2NrZXRcIiwgc3RhcnRUaW1lcik7XG4gIH1cblxuICAvLyBDbGVhbiB1cCBvbiBldmVudHNcbiAgdGhpcy5vbihcInNvY2tldFwiLCBkZXN0cm95T25UaW1lb3V0KTtcbiAgdGhpcy5vbihcImFib3J0XCIsIGNsZWFyVGltZXIpO1xuICB0aGlzLm9uKFwiZXJyb3JcIiwgY2xlYXJUaW1lcik7XG4gIHRoaXMub24oXCJyZXNwb25zZVwiLCBjbGVhclRpbWVyKTtcbiAgdGhpcy5vbihcImNsb3NlXCIsIGNsZWFyVGltZXIpO1xuXG4gIHJldHVybiB0aGlzO1xufTtcblxuLy8gUHJveHkgYWxsIG90aGVyIHB1YmxpYyBDbGllbnRSZXF1ZXN0IG1ldGhvZHNcbltcbiAgXCJmbHVzaEhlYWRlcnNcIiwgXCJnZXRIZWFkZXJcIixcbiAgXCJzZXROb0RlbGF5XCIsIFwic2V0U29ja2V0S2VlcEFsaXZlXCIsXG5dLmZvckVhY2goZnVuY3Rpb24gKG1ldGhvZCkge1xuICBSZWRpcmVjdGFibGVSZXF1ZXN0LnByb3RvdHlwZVttZXRob2RdID0gZnVuY3Rpb24gKGEsIGIpIHtcbiAgICByZXR1cm4gdGhpcy5fY3VycmVudFJlcXVlc3RbbWV0aG9kXShhLCBiKTtcbiAgfTtcbn0pO1xuXG4vLyBQcm94eSBhbGwgcHVibGljIENsaWVudFJlcXVlc3QgcHJvcGVydGllc1xuW1wiYWJvcnRlZFwiLCBcImNvbm5lY3Rpb25cIiwgXCJzb2NrZXRcIl0uZm9yRWFjaChmdW5jdGlvbiAocHJvcGVydHkpIHtcbiAgT2JqZWN0LmRlZmluZVByb3BlcnR5KFJlZGlyZWN0YWJsZVJlcXVlc3QucHJvdG90eXBlLCBwcm9wZXJ0eSwge1xuICAgIGdldDogZnVuY3Rpb24gKCkgeyByZXR1cm4gdGhpcy5fY3VycmVudFJlcXVlc3RbcHJvcGVydHldOyB9LFxuICB9KTtcbn0pO1xuXG5SZWRpcmVjdGFibGVSZXF1ZXN0LnByb3RvdHlwZS5fc2FuaXRpemVPcHRpb25zID0gZnVuY3Rpb24gKG9wdGlvbnMpIHtcbiAgLy8gRW5zdXJlIGhlYWRlcnMgYXJlIGFsd2F5cyBwcmVzZW50XG4gIGlmICghb3B0aW9ucy5oZWFkZXJzKSB7XG4gICAgb3B0aW9ucy5oZWFkZXJzID0ge307XG4gIH1cblxuICAvLyBTaW5jZSBodHRwLnJlcXVlc3QgdHJlYXRzIGhvc3QgYXMgYW4gYWxpYXMgb2YgaG9zdG5hbWUsXG4gIC8vIGJ1dCB0aGUgdXJsIG1vZHVsZSBpbnRlcnByZXRzIGhvc3QgYXMgaG9zdG5hbWUgcGx1cyBwb3J0LFxuICAvLyBlbGltaW5hdGUgdGhlIGhvc3QgcHJvcGVydHkgdG8gYXZvaWQgY29uZnVzaW9uLlxuICBpZiAob3B0aW9ucy5ob3N0KSB7XG4gICAgLy8gVXNlIGhvc3RuYW1lIGlmIHNldCwgYmVjYXVzZSBpdCBoYXMgcHJlY2VkZW5jZVxuICAgIGlmICghb3B0aW9ucy5ob3N0bmFtZSkge1xuICAgICAgb3B0aW9ucy5ob3N0bmFtZSA9IG9wdGlvbnMuaG9zdDtcbiAgICB9XG4gICAgZGVsZXRlIG9wdGlvbnMuaG9zdDtcbiAgfVxuXG4gIC8vIENvbXBsZXRlIHRoZSBVUkwgb2JqZWN0IHdoZW4gbmVjZXNzYXJ5XG4gIGlmICghb3B0aW9ucy5wYXRobmFtZSAmJiBvcHRpb25zLnBhdGgpIHtcbiAgICB2YXIgc2VhcmNoUG9zID0gb3B0aW9ucy5wYXRoLmluZGV4T2YoXCI/XCIpO1xuICAgIGlmIChzZWFyY2hQb3MgPCAwKSB7XG4gICAgICBvcHRpb25zLnBhdGhuYW1lID0gb3B0aW9ucy5wYXRoO1xuICAgIH1cbiAgICBlbHNlIHtcbiAgICAgIG9wdGlvbnMucGF0aG5hbWUgPSBvcHRpb25zLnBhdGguc3Vic3RyaW5nKDAsIHNlYXJjaFBvcyk7XG4gICAgICBvcHRpb25zLnNlYXJjaCA9IG9wdGlvbnMucGF0aC5zdWJzdHJpbmcoc2VhcmNoUG9zKTtcbiAgICB9XG4gIH1cbn07XG5cblxuLy8gRXhlY3V0ZXMgdGhlIG5leHQgbmF0aXZlIHJlcXVlc3QgKGluaXRpYWwgb3IgcmVkaXJlY3QpXG5SZWRpcmVjdGFibGVSZXF1ZXN0LnByb3RvdHlwZS5fcGVyZm9ybVJlcXVlc3QgPSBmdW5jdGlvbiAoKSB7XG4gIC8vIExvYWQgdGhlIG5hdGl2ZSBwcm90b2NvbFxuICB2YXIgcHJvdG9jb2wgPSB0aGlzLl9vcHRpb25zLnByb3RvY29sO1xuICB2YXIgbmF0aXZlUHJvdG9jb2wgPSB0aGlzLl9vcHRpb25zLm5hdGl2ZVByb3RvY29sc1twcm90b2NvbF07XG4gIGlmICghbmF0aXZlUHJvdG9jb2wpIHtcbiAgICB0aHJvdyBuZXcgVHlwZUVycm9yKFwiVW5zdXBwb3J0ZWQgcHJvdG9jb2wgXCIgKyBwcm90b2NvbCk7XG4gIH1cblxuICAvLyBJZiBzcGVjaWZpZWQsIHVzZSB0aGUgYWdlbnQgY29ycmVzcG9uZGluZyB0byB0aGUgcHJvdG9jb2xcbiAgLy8gKEhUVFAgYW5kIEhUVFBTIHVzZSBkaWZmZXJlbnQgdHlwZXMgb2YgYWdlbnRzKVxuICBpZiAodGhpcy5fb3B0aW9ucy5hZ2VudHMpIHtcbiAgICB2YXIgc2NoZW1lID0gcHJvdG9jb2wuc2xpY2UoMCwgLTEpO1xuICAgIHRoaXMuX29wdGlvbnMuYWdlbnQgPSB0aGlzLl9vcHRpb25zLmFnZW50c1tzY2hlbWVdO1xuICB9XG5cbiAgLy8gQ3JlYXRlIHRoZSBuYXRpdmUgcmVxdWVzdCBhbmQgc2V0IHVwIGl0cyBldmVudCBoYW5kbGVyc1xuICB2YXIgcmVxdWVzdCA9IHRoaXMuX2N1cnJlbnRSZXF1ZXN0ID1cbiAgICAgICAgbmF0aXZlUHJvdG9jb2wucmVxdWVzdCh0aGlzLl9vcHRpb25zLCB0aGlzLl9vbk5hdGl2ZVJlc3BvbnNlKTtcbiAgcmVxdWVzdC5fcmVkaXJlY3RhYmxlID0gdGhpcztcbiAgZm9yICh2YXIgZXZlbnQgb2YgZXZlbnRzKSB7XG4gICAgcmVxdWVzdC5vbihldmVudCwgZXZlbnRIYW5kbGVyc1tldmVudF0pO1xuICB9XG5cbiAgLy8gUkZDNzIzMMKnNS4zLjE6IFdoZW4gbWFraW5nIGEgcmVxdWVzdCBkaXJlY3RseSB0byBhbiBvcmlnaW4gc2VydmVyLCBb4oCmXVxuICAvLyBhIGNsaWVudCBNVVNUIHNlbmQgb25seSB0aGUgYWJzb2x1dGUgcGF0aCBb4oCmXSBhcyB0aGUgcmVxdWVzdC10YXJnZXQuXG4gIHRoaXMuX2N1cnJlbnRVcmwgPSAvXlxcLy8udGVzdCh0aGlzLl9vcHRpb25zLnBhdGgpID9cbiAgICB1cmwuZm9ybWF0KHRoaXMuX29wdGlvbnMpIDpcbiAgICAvLyBXaGVuIG1ha2luZyBhIHJlcXVlc3QgdG8gYSBwcm94eSwgW+KApl1cbiAgICAvLyBhIGNsaWVudCBNVVNUIHNlbmQgdGhlIHRhcmdldCBVUkkgaW4gYWJzb2x1dGUtZm9ybSBb4oCmXS5cbiAgICB0aGlzLl9vcHRpb25zLnBhdGg7XG5cbiAgLy8gRW5kIGEgcmVkaXJlY3RlZCByZXF1ZXN0XG4gIC8vIChUaGUgZmlyc3QgcmVxdWVzdCBtdXN0IGJlIGVuZGVkIGV4cGxpY2l0bHkgd2l0aCBSZWRpcmVjdGFibGVSZXF1ZXN0I2VuZClcbiAgaWYgKHRoaXMuX2lzUmVkaXJlY3QpIHtcbiAgICAvLyBXcml0ZSB0aGUgcmVxdWVzdCBlbnRpdHkgYW5kIGVuZFxuICAgIHZhciBpID0gMDtcbiAgICB2YXIgc2VsZiA9IHRoaXM7XG4gICAgdmFyIGJ1ZmZlcnMgPSB0aGlzLl9yZXF1ZXN0Qm9keUJ1ZmZlcnM7XG4gICAgKGZ1bmN0aW9uIHdyaXRlTmV4dChlcnJvcikge1xuICAgICAgLy8gT25seSB3cml0ZSBpZiB0aGlzIHJlcXVlc3QgaGFzIG5vdCBiZWVuIHJlZGlyZWN0ZWQgeWV0XG4gICAgICAvKiBpc3RhbmJ1bCBpZ25vcmUgZWxzZSAqL1xuICAgICAgaWYgKHJlcXVlc3QgPT09IHNlbGYuX2N1cnJlbnRSZXF1ZXN0KSB7XG4gICAgICAgIC8vIFJlcG9ydCBhbnkgd3JpdGUgZXJyb3JzXG4gICAgICAgIC8qIGlzdGFuYnVsIGlnbm9yZSBpZiAqL1xuICAgICAgICBpZiAoZXJyb3IpIHtcbiAgICAgICAgICBzZWxmLmVtaXQoXCJlcnJvclwiLCBlcnJvcik7XG4gICAgICAgIH1cbiAgICAgICAgLy8gV3JpdGUgdGhlIG5leHQgYnVmZmVyIGlmIHRoZXJlIGFyZSBzdGlsbCBsZWZ0XG4gICAgICAgIGVsc2UgaWYgKGkgPCBidWZmZXJzLmxlbmd0aCkge1xuICAgICAgICAgIHZhciBidWZmZXIgPSBidWZmZXJzW2krK107XG4gICAgICAgICAgLyogaXN0YW5idWwgaWdub3JlIGVsc2UgKi9cbiAgICAgICAgICBpZiAoIXJlcXVlc3QuZmluaXNoZWQpIHtcbiAgICAgICAgICAgIHJlcXVlc3Qud3JpdGUoYnVmZmVyLmRhdGEsIGJ1ZmZlci5lbmNvZGluZywgd3JpdGVOZXh0KTtcbiAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICAgICAgLy8gRW5kIHRoZSByZXF1ZXN0IGlmIGBlbmRgIGhhcyBiZWVuIGNhbGxlZCBvbiB1c1xuICAgICAgICBlbHNlIGlmIChzZWxmLl9lbmRlZCkge1xuICAgICAgICAgIHJlcXVlc3QuZW5kKCk7XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9KCkpO1xuICB9XG59O1xuXG4vLyBQcm9jZXNzZXMgYSByZXNwb25zZSBmcm9tIHRoZSBjdXJyZW50IG5hdGl2ZSByZXF1ZXN0XG5SZWRpcmVjdGFibGVSZXF1ZXN0LnByb3RvdHlwZS5fcHJvY2Vzc1Jlc3BvbnNlID0gZnVuY3Rpb24gKHJlc3BvbnNlKSB7XG4gIC8vIFN0b3JlIHRoZSByZWRpcmVjdGVkIHJlc3BvbnNlXG4gIHZhciBzdGF0dXNDb2RlID0gcmVzcG9uc2Uuc3RhdHVzQ29kZTtcbiAgaWYgKHRoaXMuX29wdGlvbnMudHJhY2tSZWRpcmVjdHMpIHtcbiAgICB0aGlzLl9yZWRpcmVjdHMucHVzaCh7XG4gICAgICB1cmw6IHRoaXMuX2N1cnJlbnRVcmwsXG4gICAgICBoZWFkZXJzOiByZXNwb25zZS5oZWFkZXJzLFxuICAgICAgc3RhdHVzQ29kZTogc3RhdHVzQ29kZSxcbiAgICB9KTtcbiAgfVxuXG4gIC8vIFJGQzcyMzHCpzYuNDogVGhlIDN4eCAoUmVkaXJlY3Rpb24pIGNsYXNzIG9mIHN0YXR1cyBjb2RlIGluZGljYXRlc1xuICAvLyB0aGF0IGZ1cnRoZXIgYWN0aW9uIG5lZWRzIHRvIGJlIHRha2VuIGJ5IHRoZSB1c2VyIGFnZW50IGluIG9yZGVyIHRvXG4gIC8vIGZ1bGZpbGwgdGhlIHJlcXVlc3QuIElmIGEgTG9jYXRpb24gaGVhZGVyIGZpZWxkIGlzIHByb3ZpZGVkLFxuICAvLyB0aGUgdXNlciBhZ2VudCBNQVkgYXV0b21hdGljYWxseSByZWRpcmVjdCBpdHMgcmVxdWVzdCB0byB0aGUgVVJJXG4gIC8vIHJlZmVyZW5jZWQgYnkgdGhlIExvY2F0aW9uIGZpZWxkIHZhbHVlLFxuICAvLyBldmVuIGlmIHRoZSBzcGVjaWZpYyBzdGF0dXMgY29kZSBpcyBub3QgdW5kZXJzdG9vZC5cblxuICAvLyBJZiB0aGUgcmVzcG9uc2UgaXMgbm90IGEgcmVkaXJlY3Q7IHJldHVybiBpdCBhcy1pc1xuICB2YXIgbG9jYXRpb24gPSByZXNwb25zZS5oZWFkZXJzLmxvY2F0aW9uO1xuICBpZiAoIWxvY2F0aW9uIHx8IHRoaXMuX29wdGlvbnMuZm9sbG93UmVkaXJlY3RzID09PSBmYWxzZSB8fFxuICAgICAgc3RhdHVzQ29kZSA8IDMwMCB8fCBzdGF0dXNDb2RlID49IDQwMCkge1xuICAgIHJlc3BvbnNlLnJlc3BvbnNlVXJsID0gdGhpcy5fY3VycmVudFVybDtcbiAgICByZXNwb25zZS5yZWRpcmVjdHMgPSB0aGlzLl9yZWRpcmVjdHM7XG4gICAgdGhpcy5lbWl0KFwicmVzcG9uc2VcIiwgcmVzcG9uc2UpO1xuXG4gICAgLy8gQ2xlYW4gdXBcbiAgICB0aGlzLl9yZXF1ZXN0Qm9keUJ1ZmZlcnMgPSBbXTtcbiAgICByZXR1cm47XG4gIH1cblxuICAvLyBUaGUgcmVzcG9uc2UgaXMgYSByZWRpcmVjdCwgc28gYWJvcnQgdGhlIGN1cnJlbnQgcmVxdWVzdFxuICBkZXN0cm95UmVxdWVzdCh0aGlzLl9jdXJyZW50UmVxdWVzdCk7XG4gIC8vIERpc2NhcmQgdGhlIHJlbWFpbmRlciBvZiB0aGUgcmVzcG9uc2UgdG8gYXZvaWQgd2FpdGluZyBmb3IgZGF0YVxuICByZXNwb25zZS5kZXN0cm95KCk7XG5cbiAgLy8gUkZDNzIzMcKnNi40OiBBIGNsaWVudCBTSE9VTEQgZGV0ZWN0IGFuZCBpbnRlcnZlbmVcbiAgLy8gaW4gY3ljbGljYWwgcmVkaXJlY3Rpb25zIChpLmUuLCBcImluZmluaXRlXCIgcmVkaXJlY3Rpb24gbG9vcHMpLlxuICBpZiAoKyt0aGlzLl9yZWRpcmVjdENvdW50ID4gdGhpcy5fb3B0aW9ucy5tYXhSZWRpcmVjdHMpIHtcbiAgICB0aHJvdyBuZXcgVG9vTWFueVJlZGlyZWN0c0Vycm9yKCk7XG4gIH1cblxuICAvLyBTdG9yZSB0aGUgcmVxdWVzdCBoZWFkZXJzIGlmIGFwcGxpY2FibGVcbiAgdmFyIHJlcXVlc3RIZWFkZXJzO1xuICB2YXIgYmVmb3JlUmVkaXJlY3QgPSB0aGlzLl9vcHRpb25zLmJlZm9yZVJlZGlyZWN0O1xuICBpZiAoYmVmb3JlUmVkaXJlY3QpIHtcbiAgICByZXF1ZXN0SGVhZGVycyA9IE9iamVjdC5hc3NpZ24oe1xuICAgICAgLy8gVGhlIEhvc3QgaGVhZGVyIHdhcyBzZXQgYnkgbmF0aXZlUHJvdG9jb2wucmVxdWVzdFxuICAgICAgSG9zdDogcmVzcG9uc2UucmVxLmdldEhlYWRlcihcImhvc3RcIiksXG4gICAgfSwgdGhpcy5fb3B0aW9ucy5oZWFkZXJzKTtcbiAgfVxuXG4gIC8vIFJGQzcyMzHCpzYuNDogQXV0b21hdGljIHJlZGlyZWN0aW9uIG5lZWRzIHRvIGRvbmUgd2l0aFxuICAvLyBjYXJlIGZvciBtZXRob2RzIG5vdCBrbm93biB0byBiZSBzYWZlLCBb4oCmXVxuICAvLyBSRkM3MjMxwqc2LjQuMuKAkzM6IEZvciBoaXN0b3JpY2FsIHJlYXNvbnMsIGEgdXNlciBhZ2VudCBNQVkgY2hhbmdlXG4gIC8vIHRoZSByZXF1ZXN0IG1ldGhvZCBmcm9tIFBPU1QgdG8gR0VUIGZvciB0aGUgc3Vic2VxdWVudCByZXF1ZXN0LlxuICB2YXIgbWV0aG9kID0gdGhpcy5fb3B0aW9ucy5tZXRob2Q7XG4gIGlmICgoc3RhdHVzQ29kZSA9PT0gMzAxIHx8IHN0YXR1c0NvZGUgPT09IDMwMikgJiYgdGhpcy5fb3B0aW9ucy5tZXRob2QgPT09IFwiUE9TVFwiIHx8XG4gICAgICAvLyBSRkM3MjMxwqc2LjQuNDogVGhlIDMwMyAoU2VlIE90aGVyKSBzdGF0dXMgY29kZSBpbmRpY2F0ZXMgdGhhdFxuICAgICAgLy8gdGhlIHNlcnZlciBpcyByZWRpcmVjdGluZyB0aGUgdXNlciBhZ2VudCB0byBhIGRpZmZlcmVudCByZXNvdXJjZSBb4oCmXVxuICAgICAgLy8gQSB1c2VyIGFnZW50IGNhbiBwZXJmb3JtIGEgcmV0cmlldmFsIHJlcXVlc3QgdGFyZ2V0aW5nIHRoYXQgVVJJXG4gICAgICAvLyAoYSBHRVQgb3IgSEVBRCByZXF1ZXN0IGlmIHVzaW5nIEhUVFApIFvigKZdXG4gICAgICAoc3RhdHVzQ29kZSA9PT0gMzAzKSAmJiAhL14oPzpHRVR8SEVBRCkkLy50ZXN0KHRoaXMuX29wdGlvbnMubWV0aG9kKSkge1xuICAgIHRoaXMuX29wdGlvbnMubWV0aG9kID0gXCJHRVRcIjtcbiAgICAvLyBEcm9wIGEgcG9zc2libGUgZW50aXR5IGFuZCBoZWFkZXJzIHJlbGF0ZWQgdG8gaXRcbiAgICB0aGlzLl9yZXF1ZXN0Qm9keUJ1ZmZlcnMgPSBbXTtcbiAgICByZW1vdmVNYXRjaGluZ0hlYWRlcnMoL15jb250ZW50LS9pLCB0aGlzLl9vcHRpb25zLmhlYWRlcnMpO1xuICB9XG5cbiAgLy8gRHJvcCB0aGUgSG9zdCBoZWFkZXIsIGFzIHRoZSByZWRpcmVjdCBtaWdodCBsZWFkIHRvIGEgZGlmZmVyZW50IGhvc3RcbiAgdmFyIGN1cnJlbnRIb3N0SGVhZGVyID0gcmVtb3ZlTWF0Y2hpbmdIZWFkZXJzKC9eaG9zdCQvaSwgdGhpcy5fb3B0aW9ucy5oZWFkZXJzKTtcblxuICAvLyBJZiB0aGUgcmVkaXJlY3QgaXMgcmVsYXRpdmUsIGNhcnJ5IG92ZXIgdGhlIGhvc3Qgb2YgdGhlIGxhc3QgcmVxdWVzdFxuICB2YXIgY3VycmVudFVybFBhcnRzID0gcGFyc2VVcmwodGhpcy5fY3VycmVudFVybCk7XG4gIHZhciBjdXJyZW50SG9zdCA9IGN1cnJlbnRIb3N0SGVhZGVyIHx8IGN1cnJlbnRVcmxQYXJ0cy5ob3N0O1xuICB2YXIgY3VycmVudFVybCA9IC9eXFx3KzovLnRlc3QobG9jYXRpb24pID8gdGhpcy5fY3VycmVudFVybCA6XG4gICAgdXJsLmZvcm1hdChPYmplY3QuYXNzaWduKGN1cnJlbnRVcmxQYXJ0cywgeyBob3N0OiBjdXJyZW50SG9zdCB9KSk7XG5cbiAgLy8gQ3JlYXRlIHRoZSByZWRpcmVjdGVkIHJlcXVlc3RcbiAgdmFyIHJlZGlyZWN0VXJsID0gcmVzb2x2ZVVybChsb2NhdGlvbiwgY3VycmVudFVybCk7XG4gIGRlYnVnKFwicmVkaXJlY3RpbmcgdG9cIiwgcmVkaXJlY3RVcmwuaHJlZik7XG4gIHRoaXMuX2lzUmVkaXJlY3QgPSB0cnVlO1xuICBzcHJlYWRVcmxPYmplY3QocmVkaXJlY3RVcmwsIHRoaXMuX29wdGlvbnMpO1xuXG4gIC8vIERyb3AgY29uZmlkZW50aWFsIGhlYWRlcnMgd2hlbiByZWRpcmVjdGluZyB0byBhIGxlc3Mgc2VjdXJlIHByb3RvY29sXG4gIC8vIG9yIHRvIGEgZGlmZmVyZW50IGRvbWFpbiB0aGF0IGlzIG5vdCBhIHN1cGVyZG9tYWluXG4gIGlmIChyZWRpcmVjdFVybC5wcm90b2NvbCAhPT0gY3VycmVudFVybFBhcnRzLnByb3RvY29sICYmXG4gICAgIHJlZGlyZWN0VXJsLnByb3RvY29sICE9PSBcImh0dHBzOlwiIHx8XG4gICAgIHJlZGlyZWN0VXJsLmhvc3QgIT09IGN1cnJlbnRIb3N0ICYmXG4gICAgICFpc1N1YmRvbWFpbihyZWRpcmVjdFVybC5ob3N0LCBjdXJyZW50SG9zdCkpIHtcbiAgICByZW1vdmVNYXRjaGluZ0hlYWRlcnMoL14oPzphdXRob3JpemF0aW9ufGNvb2tpZSkkL2ksIHRoaXMuX29wdGlvbnMuaGVhZGVycyk7XG4gIH1cblxuICAvLyBFdmFsdWF0ZSB0aGUgYmVmb3JlUmVkaXJlY3QgY2FsbGJhY2tcbiAgaWYgKGlzRnVuY3Rpb24oYmVmb3JlUmVkaXJlY3QpKSB7XG4gICAgdmFyIHJlc3BvbnNlRGV0YWlscyA9IHtcbiAgICAgIGhlYWRlcnM6IHJlc3BvbnNlLmhlYWRlcnMsXG4gICAgICBzdGF0dXNDb2RlOiBzdGF0dXNDb2RlLFxuICAgIH07XG4gICAgdmFyIHJlcXVlc3REZXRhaWxzID0ge1xuICAgICAgdXJsOiBjdXJyZW50VXJsLFxuICAgICAgbWV0aG9kOiBtZXRob2QsXG4gICAgICBoZWFkZXJzOiByZXF1ZXN0SGVhZGVycyxcbiAgICB9O1xuICAgIGJlZm9yZVJlZGlyZWN0KHRoaXMuX29wdGlvbnMsIHJlc3BvbnNlRGV0YWlscywgcmVxdWVzdERldGFpbHMpO1xuICAgIHRoaXMuX3Nhbml0aXplT3B0aW9ucyh0aGlzLl9vcHRpb25zKTtcbiAgfVxuXG4gIC8vIFBlcmZvcm0gdGhlIHJlZGlyZWN0ZWQgcmVxdWVzdFxuICB0aGlzLl9wZXJmb3JtUmVxdWVzdCgpO1xufTtcblxuLy8gV3JhcHMgdGhlIGtleS92YWx1ZSBvYmplY3Qgb2YgcHJvdG9jb2xzIHdpdGggcmVkaXJlY3QgZnVuY3Rpb25hbGl0eVxuZnVuY3Rpb24gd3JhcChwcm90b2NvbHMpIHtcbiAgLy8gRGVmYXVsdCBzZXR0aW5nc1xuICB2YXIgZXhwb3J0cyA9IHtcbiAgICBtYXhSZWRpcmVjdHM6IDIxLFxuICAgIG1heEJvZHlMZW5ndGg6IDEwICogMTAyNCAqIDEwMjQsXG4gIH07XG5cbiAgLy8gV3JhcCBlYWNoIHByb3RvY29sXG4gIHZhciBuYXRpdmVQcm90b2NvbHMgPSB7fTtcbiAgT2JqZWN0LmtleXMocHJvdG9jb2xzKS5mb3JFYWNoKGZ1bmN0aW9uIChzY2hlbWUpIHtcbiAgICB2YXIgcHJvdG9jb2wgPSBzY2hlbWUgKyBcIjpcIjtcbiAgICB2YXIgbmF0aXZlUHJvdG9jb2wgPSBuYXRpdmVQcm90b2NvbHNbcHJvdG9jb2xdID0gcHJvdG9jb2xzW3NjaGVtZV07XG4gICAgdmFyIHdyYXBwZWRQcm90b2NvbCA9IGV4cG9ydHNbc2NoZW1lXSA9IE9iamVjdC5jcmVhdGUobmF0aXZlUHJvdG9jb2wpO1xuXG4gICAgLy8gRXhlY3V0ZXMgYSByZXF1ZXN0LCBmb2xsb3dpbmcgcmVkaXJlY3RzXG4gICAgZnVuY3Rpb24gcmVxdWVzdChpbnB1dCwgb3B0aW9ucywgY2FsbGJhY2spIHtcbiAgICAgIC8vIFBhcnNlIHBhcmFtZXRlcnMsIGVuc3VyaW5nIHRoYXQgaW5wdXQgaXMgYW4gb2JqZWN0XG4gICAgICBpZiAoaXNVUkwoaW5wdXQpKSB7XG4gICAgICAgIGlucHV0ID0gc3ByZWFkVXJsT2JqZWN0KGlucHV0KTtcbiAgICAgIH1cbiAgICAgIGVsc2UgaWYgKGlzU3RyaW5nKGlucHV0KSkge1xuICAgICAgICBpbnB1dCA9IHNwcmVhZFVybE9iamVjdChwYXJzZVVybChpbnB1dCkpO1xuICAgICAgfVxuICAgICAgZWxzZSB7XG4gICAgICAgIGNhbGxiYWNrID0gb3B0aW9ucztcbiAgICAgICAgb3B0aW9ucyA9IHZhbGlkYXRlVXJsKGlucHV0KTtcbiAgICAgICAgaW5wdXQgPSB7IHByb3RvY29sOiBwcm90b2NvbCB9O1xuICAgICAgfVxuICAgICAgaWYgKGlzRnVuY3Rpb24ob3B0aW9ucykpIHtcbiAgICAgICAgY2FsbGJhY2sgPSBvcHRpb25zO1xuICAgICAgICBvcHRpb25zID0gbnVsbDtcbiAgICAgIH1cblxuICAgICAgLy8gU2V0IGRlZmF1bHRzXG4gICAgICBvcHRpb25zID0gT2JqZWN0LmFzc2lnbih7XG4gICAgICAgIG1heFJlZGlyZWN0czogZXhwb3J0cy5tYXhSZWRpcmVjdHMsXG4gICAgICAgIG1heEJvZHlMZW5ndGg6IGV4cG9ydHMubWF4Qm9keUxlbmd0aCxcbiAgICAgIH0sIGlucHV0LCBvcHRpb25zKTtcbiAgICAgIG9wdGlvbnMubmF0aXZlUHJvdG9jb2xzID0gbmF0aXZlUHJvdG9jb2xzO1xuICAgICAgaWYgKCFpc1N0cmluZyhvcHRpb25zLmhvc3QpICYmICFpc1N0cmluZyhvcHRpb25zLmhvc3RuYW1lKSkge1xuICAgICAgICBvcHRpb25zLmhvc3RuYW1lID0gXCI6OjFcIjtcbiAgICAgIH1cblxuICAgICAgYXNzZXJ0LmVxdWFsKG9wdGlvbnMucHJvdG9jb2wsIHByb3RvY29sLCBcInByb3RvY29sIG1pc21hdGNoXCIpO1xuICAgICAgZGVidWcoXCJvcHRpb25zXCIsIG9wdGlvbnMpO1xuICAgICAgcmV0dXJuIG5ldyBSZWRpcmVjdGFibGVSZXF1ZXN0KG9wdGlvbnMsIGNhbGxiYWNrKTtcbiAgICB9XG5cbiAgICAvLyBFeGVjdXRlcyBhIEdFVCByZXF1ZXN0LCBmb2xsb3dpbmcgcmVkaXJlY3RzXG4gICAgZnVuY3Rpb24gZ2V0KGlucHV0LCBvcHRpb25zLCBjYWxsYmFjaykge1xuICAgICAgdmFyIHdyYXBwZWRSZXF1ZXN0ID0gd3JhcHBlZFByb3RvY29sLnJlcXVlc3QoaW5wdXQsIG9wdGlvbnMsIGNhbGxiYWNrKTtcbiAgICAgIHdyYXBwZWRSZXF1ZXN0LmVuZCgpO1xuICAgICAgcmV0dXJuIHdyYXBwZWRSZXF1ZXN0O1xuICAgIH1cblxuICAgIC8vIEV4cG9zZSB0aGUgcHJvcGVydGllcyBvbiB0aGUgd3JhcHBlZCBwcm90b2NvbFxuICAgIE9iamVjdC5kZWZpbmVQcm9wZXJ0aWVzKHdyYXBwZWRQcm90b2NvbCwge1xuICAgICAgcmVxdWVzdDogeyB2YWx1ZTogcmVxdWVzdCwgY29uZmlndXJhYmxlOiB0cnVlLCBlbnVtZXJhYmxlOiB0cnVlLCB3cml0YWJsZTogdHJ1ZSB9LFxuICAgICAgZ2V0OiB7IHZhbHVlOiBnZXQsIGNvbmZpZ3VyYWJsZTogdHJ1ZSwgZW51bWVyYWJsZTogdHJ1ZSwgd3JpdGFibGU6IHRydWUgfSxcbiAgICB9KTtcbiAgfSk7XG4gIHJldHVybiBleHBvcnRzO1xufVxuXG5mdW5jdGlvbiBub29wKCkgeyAvKiBlbXB0eSAqLyB9XG5cbmZ1bmN0aW9uIHBhcnNlVXJsKGlucHV0KSB7XG4gIHZhciBwYXJzZWQ7XG4gIC8qIGlzdGFuYnVsIGlnbm9yZSBlbHNlICovXG4gIGlmICh1c2VOYXRpdmVVUkwpIHtcbiAgICBwYXJzZWQgPSBuZXcgVVJMKGlucHV0KTtcbiAgfVxuICBlbHNlIHtcbiAgICAvLyBFbnN1cmUgdGhlIFVSTCBpcyB2YWxpZCBhbmQgYWJzb2x1dGVcbiAgICBwYXJzZWQgPSB2YWxpZGF0ZVVybCh1cmwucGFyc2UoaW5wdXQpKTtcbiAgICBpZiAoIWlzU3RyaW5nKHBhcnNlZC5wcm90b2NvbCkpIHtcbiAgICAgIHRocm93IG5ldyBJbnZhbGlkVXJsRXJyb3IoeyBpbnB1dCB9KTtcbiAgICB9XG4gIH1cbiAgcmV0dXJuIHBhcnNlZDtcbn1cblxuZnVuY3Rpb24gcmVzb2x2ZVVybChyZWxhdGl2ZSwgYmFzZSkge1xuICAvKiBpc3RhbmJ1bCBpZ25vcmUgbmV4dCAqL1xuICByZXR1cm4gdXNlTmF0aXZlVVJMID8gbmV3IFVSTChyZWxhdGl2ZSwgYmFzZSkgOiBwYXJzZVVybCh1cmwucmVzb2x2ZShiYXNlLCByZWxhdGl2ZSkpO1xufVxuXG5mdW5jdGlvbiB2YWxpZGF0ZVVybChpbnB1dCkge1xuICBpZiAoL15cXFsvLnRlc3QoaW5wdXQuaG9zdG5hbWUpICYmICEvXlxcW1s6MC05YS1mXStcXF0kL2kudGVzdChpbnB1dC5ob3N0bmFtZSkpIHtcbiAgICB0aHJvdyBuZXcgSW52YWxpZFVybEVycm9yKHsgaW5wdXQ6IGlucHV0LmhyZWYgfHwgaW5wdXQgfSk7XG4gIH1cbiAgaWYgKC9eXFxbLy50ZXN0KGlucHV0Lmhvc3QpICYmICEvXlxcW1s6MC05YS1mXStcXF0oOlxcZCspPyQvaS50ZXN0KGlucHV0Lmhvc3QpKSB7XG4gICAgdGhyb3cgbmV3IEludmFsaWRVcmxFcnJvcih7IGlucHV0OiBpbnB1dC5ocmVmIHx8IGlucHV0IH0pO1xuICB9XG4gIHJldHVybiBpbnB1dDtcbn1cblxuZnVuY3Rpb24gc3ByZWFkVXJsT2JqZWN0KHVybE9iamVjdCwgdGFyZ2V0KSB7XG4gIHZhciBzcHJlYWQgPSB0YXJnZXQgfHwge307XG4gIGZvciAodmFyIGtleSBvZiBwcmVzZXJ2ZWRVcmxGaWVsZHMpIHtcbiAgICBzcHJlYWRba2V5XSA9IHVybE9iamVjdFtrZXldO1xuICB9XG5cbiAgLy8gRml4IElQdjYgaG9zdG5hbWVcbiAgaWYgKHNwcmVhZC5ob3N0bmFtZS5zdGFydHNXaXRoKFwiW1wiKSkge1xuICAgIHNwcmVhZC5ob3N0bmFtZSA9IHNwcmVhZC5ob3N0bmFtZS5zbGljZSgxLCAtMSk7XG4gIH1cbiAgLy8gRW5zdXJlIHBvcnQgaXMgYSBudW1iZXJcbiAgaWYgKHNwcmVhZC5wb3J0ICE9PSBcIlwiKSB7XG4gICAgc3ByZWFkLnBvcnQgPSBOdW1iZXIoc3ByZWFkLnBvcnQpO1xuICB9XG4gIC8vIENvbmNhdGVuYXRlIHBhdGhcbiAgc3ByZWFkLnBhdGggPSBzcHJlYWQuc2VhcmNoID8gc3ByZWFkLnBhdGhuYW1lICsgc3ByZWFkLnNlYXJjaCA6IHNwcmVhZC5wYXRobmFtZTtcblxuICByZXR1cm4gc3ByZWFkO1xufVxuXG5mdW5jdGlvbiByZW1vdmVNYXRjaGluZ0hlYWRlcnMocmVnZXgsIGhlYWRlcnMpIHtcbiAgdmFyIGxhc3RWYWx1ZTtcbiAgZm9yICh2YXIgaGVhZGVyIGluIGhlYWRlcnMpIHtcbiAgICBpZiAocmVnZXgudGVzdChoZWFkZXIpKSB7XG4gICAgICBsYXN0VmFsdWUgPSBoZWFkZXJzW2hlYWRlcl07XG4gICAgICBkZWxldGUgaGVhZGVyc1toZWFkZXJdO1xuICAgIH1cbiAgfVxuICByZXR1cm4gKGxhc3RWYWx1ZSA9PT0gbnVsbCB8fCB0eXBlb2YgbGFzdFZhbHVlID09PSBcInVuZGVmaW5lZFwiKSA/XG4gICAgdW5kZWZpbmVkIDogU3RyaW5nKGxhc3RWYWx1ZSkudHJpbSgpO1xufVxuXG5mdW5jdGlvbiBjcmVhdGVFcnJvclR5cGUoY29kZSwgbWVzc2FnZSwgYmFzZUNsYXNzKSB7XG4gIC8vIENyZWF0ZSBjb25zdHJ1Y3RvclxuICBmdW5jdGlvbiBDdXN0b21FcnJvcihwcm9wZXJ0aWVzKSB7XG4gICAgRXJyb3IuY2FwdHVyZVN0YWNrVHJhY2UodGhpcywgdGhpcy5jb25zdHJ1Y3Rvcik7XG4gICAgT2JqZWN0LmFzc2lnbih0aGlzLCBwcm9wZXJ0aWVzIHx8IHt9KTtcbiAgICB0aGlzLmNvZGUgPSBjb2RlO1xuICAgIHRoaXMubWVzc2FnZSA9IHRoaXMuY2F1c2UgPyBtZXNzYWdlICsgXCI6IFwiICsgdGhpcy5jYXVzZS5tZXNzYWdlIDogbWVzc2FnZTtcbiAgfVxuXG4gIC8vIEF0dGFjaCBjb25zdHJ1Y3RvciBhbmQgc2V0IGRlZmF1bHQgcHJvcGVydGllc1xuICBDdXN0b21FcnJvci5wcm90b3R5cGUgPSBuZXcgKGJhc2VDbGFzcyB8fCBFcnJvcikoKTtcbiAgT2JqZWN0LmRlZmluZVByb3BlcnRpZXMoQ3VzdG9tRXJyb3IucHJvdG90eXBlLCB7XG4gICAgY29uc3RydWN0b3I6IHtcbiAgICAgIHZhbHVlOiBDdXN0b21FcnJvcixcbiAgICAgIGVudW1lcmFibGU6IGZhbHNlLFxuICAgIH0sXG4gICAgbmFtZToge1xuICAgICAgdmFsdWU6IFwiRXJyb3IgW1wiICsgY29kZSArIFwiXVwiLFxuICAgICAgZW51bWVyYWJsZTogZmFsc2UsXG4gICAgfSxcbiAgfSk7XG4gIHJldHVybiBDdXN0b21FcnJvcjtcbn1cblxuZnVuY3Rpb24gZGVzdHJveVJlcXVlc3QocmVxdWVzdCwgZXJyb3IpIHtcbiAgZm9yICh2YXIgZXZlbnQgb2YgZXZlbnRzKSB7XG4gICAgcmVxdWVzdC5yZW1vdmVMaXN0ZW5lcihldmVudCwgZXZlbnRIYW5kbGVyc1tldmVudF0pO1xuICB9XG4gIHJlcXVlc3Qub24oXCJlcnJvclwiLCBub29wKTtcbiAgcmVxdWVzdC5kZXN0cm95KGVycm9yKTtcbn1cblxuZnVuY3Rpb24gaXNTdWJkb21haW4oc3ViZG9tYWluLCBkb21haW4pIHtcbiAgYXNzZXJ0KGlzU3RyaW5nKHN1YmRvbWFpbikgJiYgaXNTdHJpbmcoZG9tYWluKSk7XG4gIHZhciBkb3QgPSBzdWJkb21haW4ubGVuZ3RoIC0gZG9tYWluLmxlbmd0aCAtIDE7XG4gIHJldHVybiBkb3QgPiAwICYmIHN1YmRvbWFpbltkb3RdID09PSBcIi5cIiAmJiBzdWJkb21haW4uZW5kc1dpdGgoZG9tYWluKTtcbn1cblxuZnVuY3Rpb24gaXNTdHJpbmcodmFsdWUpIHtcbiAgcmV0dXJuIHR5cGVvZiB2YWx1ZSA9PT0gXCJzdHJpbmdcIiB8fCB2YWx1ZSBpbnN0YW5jZW9mIFN0cmluZztcbn1cblxuZnVuY3Rpb24gaXNGdW5jdGlvbih2YWx1ZSkge1xuICByZXR1cm4gdHlwZW9mIHZhbHVlID09PSBcImZ1bmN0aW9uXCI7XG59XG5cbmZ1bmN0aW9uIGlzQnVmZmVyKHZhbHVlKSB7XG4gIHJldHVybiB0eXBlb2YgdmFsdWUgPT09IFwib2JqZWN0XCIgJiYgKFwibGVuZ3RoXCIgaW4gdmFsdWUpO1xufVxuXG5mdW5jdGlvbiBpc1VSTCh2YWx1ZSkge1xuICByZXR1cm4gVVJMICYmIHZhbHVlIGluc3RhbmNlb2YgVVJMO1xufVxuXG4vLyBFeHBvcnRzXG5tb2R1bGUuZXhwb3J0cyA9IHdyYXAoeyBodHRwOiBodHRwLCBodHRwczogaHR0cHMgfSk7XG5tb2R1bGUuZXhwb3J0cy53cmFwID0gd3JhcDtcbiIsIid1c2Ugc3RyaWN0Jztcbm1vZHVsZS5leHBvcnRzID0gKGZsYWcsIGFyZ3YpID0+IHtcblx0YXJndiA9IGFyZ3YgfHwgcHJvY2Vzcy5hcmd2O1xuXHRjb25zdCBwcmVmaXggPSBmbGFnLnN0YXJ0c1dpdGgoJy0nKSA/ICcnIDogKGZsYWcubGVuZ3RoID09PSAxID8gJy0nIDogJy0tJyk7XG5cdGNvbnN0IHBvcyA9IGFyZ3YuaW5kZXhPZihwcmVmaXggKyBmbGFnKTtcblx0Y29uc3QgdGVybWluYXRvclBvcyA9IGFyZ3YuaW5kZXhPZignLS0nKTtcblx0cmV0dXJuIHBvcyAhPT0gLTEgJiYgKHRlcm1pbmF0b3JQb3MgPT09IC0xID8gdHJ1ZSA6IHBvcyA8IHRlcm1pbmF0b3JQb3MpO1xufTtcbiIsIi8qIVxuICogbWltZS1kYlxuICogQ29weXJpZ2h0KGMpIDIwMTQgSm9uYXRoYW4gT25nXG4gKiBDb3B5cmlnaHQoYykgMjAxNS0yMDIyIERvdWdsYXMgQ2hyaXN0b3BoZXIgV2lsc29uXG4gKiBNSVQgTGljZW5zZWRcbiAqL1xuXG4vKipcbiAqIE1vZHVsZSBleHBvcnRzLlxuICovXG5cbm1vZHVsZS5leHBvcnRzID0gcmVxdWlyZSgnLi9kYi5qc29uJylcbiIsIi8qIVxuICogbWltZS10eXBlc1xuICogQ29weXJpZ2h0KGMpIDIwMTQgSm9uYXRoYW4gT25nXG4gKiBDb3B5cmlnaHQoYykgMjAxNSBEb3VnbGFzIENocmlzdG9waGVyIFdpbHNvblxuICogTUlUIExpY2Vuc2VkXG4gKi9cblxuJ3VzZSBzdHJpY3QnXG5cbi8qKlxuICogTW9kdWxlIGRlcGVuZGVuY2llcy5cbiAqIEBwcml2YXRlXG4gKi9cblxudmFyIGRiID0gcmVxdWlyZSgnbWltZS1kYicpXG52YXIgZXh0bmFtZSA9IHJlcXVpcmUoJ3BhdGgnKS5leHRuYW1lXG5cbi8qKlxuICogTW9kdWxlIHZhcmlhYmxlcy5cbiAqIEBwcml2YXRlXG4gKi9cblxudmFyIEVYVFJBQ1RfVFlQRV9SRUdFWFAgPSAvXlxccyooW147XFxzXSopKD86O3xcXHN8JCkvXG52YXIgVEVYVF9UWVBFX1JFR0VYUCA9IC9edGV4dFxcLy9pXG5cbi8qKlxuICogTW9kdWxlIGV4cG9ydHMuXG4gKiBAcHVibGljXG4gKi9cblxuZXhwb3J0cy5jaGFyc2V0ID0gY2hhcnNldFxuZXhwb3J0cy5jaGFyc2V0cyA9IHsgbG9va3VwOiBjaGFyc2V0IH1cbmV4cG9ydHMuY29udGVudFR5cGUgPSBjb250ZW50VHlwZVxuZXhwb3J0cy5leHRlbnNpb24gPSBleHRlbnNpb25cbmV4cG9ydHMuZXh0ZW5zaW9ucyA9IE9iamVjdC5jcmVhdGUobnVsbClcbmV4cG9ydHMubG9va3VwID0gbG9va3VwXG5leHBvcnRzLnR5cGVzID0gT2JqZWN0LmNyZWF0ZShudWxsKVxuXG4vLyBQb3B1bGF0ZSB0aGUgZXh0ZW5zaW9ucy90eXBlcyBtYXBzXG5wb3B1bGF0ZU1hcHMoZXhwb3J0cy5leHRlbnNpb25zLCBleHBvcnRzLnR5cGVzKVxuXG4vKipcbiAqIEdldCB0aGUgZGVmYXVsdCBjaGFyc2V0IGZvciBhIE1JTUUgdHlwZS5cbiAqXG4gKiBAcGFyYW0ge3N0cmluZ30gdHlwZVxuICogQHJldHVybiB7Ym9vbGVhbnxzdHJpbmd9XG4gKi9cblxuZnVuY3Rpb24gY2hhcnNldCAodHlwZSkge1xuICBpZiAoIXR5cGUgfHwgdHlwZW9mIHR5cGUgIT09ICdzdHJpbmcnKSB7XG4gICAgcmV0dXJuIGZhbHNlXG4gIH1cblxuICAvLyBUT0RPOiB1c2UgbWVkaWEtdHlwZXJcbiAgdmFyIG1hdGNoID0gRVhUUkFDVF9UWVBFX1JFR0VYUC5leGVjKHR5cGUpXG4gIHZhciBtaW1lID0gbWF0Y2ggJiYgZGJbbWF0Y2hbMV0udG9Mb3dlckNhc2UoKV1cblxuICBpZiAobWltZSAmJiBtaW1lLmNoYXJzZXQpIHtcbiAgICByZXR1cm4gbWltZS5jaGFyc2V0XG4gIH1cblxuICAvLyBkZWZhdWx0IHRleHQvKiB0byB1dGYtOFxuICBpZiAobWF0Y2ggJiYgVEVYVF9UWVBFX1JFR0VYUC50ZXN0KG1hdGNoWzFdKSkge1xuICAgIHJldHVybiAnVVRGLTgnXG4gIH1cblxuICByZXR1cm4gZmFsc2Vcbn1cblxuLyoqXG4gKiBDcmVhdGUgYSBmdWxsIENvbnRlbnQtVHlwZSBoZWFkZXIgZ2l2ZW4gYSBNSU1FIHR5cGUgb3IgZXh0ZW5zaW9uLlxuICpcbiAqIEBwYXJhbSB7c3RyaW5nfSBzdHJcbiAqIEByZXR1cm4ge2Jvb2xlYW58c3RyaW5nfVxuICovXG5cbmZ1bmN0aW9uIGNvbnRlbnRUeXBlIChzdHIpIHtcbiAgLy8gVE9ETzogc2hvdWxkIHRoaXMgZXZlbiBiZSBpbiB0aGlzIG1vZHVsZT9cbiAgaWYgKCFzdHIgfHwgdHlwZW9mIHN0ciAhPT0gJ3N0cmluZycpIHtcbiAgICByZXR1cm4gZmFsc2VcbiAgfVxuXG4gIHZhciBtaW1lID0gc3RyLmluZGV4T2YoJy8nKSA9PT0gLTFcbiAgICA/IGV4cG9ydHMubG9va3VwKHN0cilcbiAgICA6IHN0clxuXG4gIGlmICghbWltZSkge1xuICAgIHJldHVybiBmYWxzZVxuICB9XG5cbiAgLy8gVE9ETzogdXNlIGNvbnRlbnQtdHlwZSBvciBvdGhlciBtb2R1bGVcbiAgaWYgKG1pbWUuaW5kZXhPZignY2hhcnNldCcpID09PSAtMSkge1xuICAgIHZhciBjaGFyc2V0ID0gZXhwb3J0cy5jaGFyc2V0KG1pbWUpXG4gICAgaWYgKGNoYXJzZXQpIG1pbWUgKz0gJzsgY2hhcnNldD0nICsgY2hhcnNldC50b0xvd2VyQ2FzZSgpXG4gIH1cblxuICByZXR1cm4gbWltZVxufVxuXG4vKipcbiAqIEdldCB0aGUgZGVmYXVsdCBleHRlbnNpb24gZm9yIGEgTUlNRSB0eXBlLlxuICpcbiAqIEBwYXJhbSB7c3RyaW5nfSB0eXBlXG4gKiBAcmV0dXJuIHtib29sZWFufHN0cmluZ31cbiAqL1xuXG5mdW5jdGlvbiBleHRlbnNpb24gKHR5cGUpIHtcbiAgaWYgKCF0eXBlIHx8IHR5cGVvZiB0eXBlICE9PSAnc3RyaW5nJykge1xuICAgIHJldHVybiBmYWxzZVxuICB9XG5cbiAgLy8gVE9ETzogdXNlIG1lZGlhLXR5cGVyXG4gIHZhciBtYXRjaCA9IEVYVFJBQ1RfVFlQRV9SRUdFWFAuZXhlYyh0eXBlKVxuXG4gIC8vIGdldCBleHRlbnNpb25zXG4gIHZhciBleHRzID0gbWF0Y2ggJiYgZXhwb3J0cy5leHRlbnNpb25zW21hdGNoWzFdLnRvTG93ZXJDYXNlKCldXG5cbiAgaWYgKCFleHRzIHx8ICFleHRzLmxlbmd0aCkge1xuICAgIHJldHVybiBmYWxzZVxuICB9XG5cbiAgcmV0dXJuIGV4dHNbMF1cbn1cblxuLyoqXG4gKiBMb29rdXAgdGhlIE1JTUUgdHlwZSBmb3IgYSBmaWxlIHBhdGgvZXh0ZW5zaW9uLlxuICpcbiAqIEBwYXJhbSB7c3RyaW5nfSBwYXRoXG4gKiBAcmV0dXJuIHtib29sZWFufHN0cmluZ31cbiAqL1xuXG5mdW5jdGlvbiBsb29rdXAgKHBhdGgpIHtcbiAgaWYgKCFwYXRoIHx8IHR5cGVvZiBwYXRoICE9PSAnc3RyaW5nJykge1xuICAgIHJldHVybiBmYWxzZVxuICB9XG5cbiAgLy8gZ2V0IHRoZSBleHRlbnNpb24gKFwiZXh0XCIgb3IgXCIuZXh0XCIgb3IgZnVsbCBwYXRoKVxuICB2YXIgZXh0ZW5zaW9uID0gZXh0bmFtZSgneC4nICsgcGF0aClcbiAgICAudG9Mb3dlckNhc2UoKVxuICAgIC5zdWJzdHIoMSlcblxuICBpZiAoIWV4dGVuc2lvbikge1xuICAgIHJldHVybiBmYWxzZVxuICB9XG5cbiAgcmV0dXJuIGV4cG9ydHMudHlwZXNbZXh0ZW5zaW9uXSB8fCBmYWxzZVxufVxuXG4vKipcbiAqIFBvcHVsYXRlIHRoZSBleHRlbnNpb25zIGFuZCB0eXBlcyBtYXBzLlxuICogQHByaXZhdGVcbiAqL1xuXG5mdW5jdGlvbiBwb3B1bGF0ZU1hcHMgKGV4dGVuc2lvbnMsIHR5cGVzKSB7XG4gIC8vIHNvdXJjZSBwcmVmZXJlbmNlIChsZWFzdCAtPiBtb3N0KVxuICB2YXIgcHJlZmVyZW5jZSA9IFsnbmdpbngnLCAnYXBhY2hlJywgdW5kZWZpbmVkLCAnaWFuYSddXG5cbiAgT2JqZWN0LmtleXMoZGIpLmZvckVhY2goZnVuY3Rpb24gZm9yRWFjaE1pbWVUeXBlICh0eXBlKSB7XG4gICAgdmFyIG1pbWUgPSBkYlt0eXBlXVxuICAgIHZhciBleHRzID0gbWltZS5leHRlbnNpb25zXG5cbiAgICBpZiAoIWV4dHMgfHwgIWV4dHMubGVuZ3RoKSB7XG4gICAgICByZXR1cm5cbiAgICB9XG5cbiAgICAvLyBtaW1lIC0+IGV4dGVuc2lvbnNcbiAgICBleHRlbnNpb25zW3R5cGVdID0gZXh0c1xuXG4gICAgLy8gZXh0ZW5zaW9uIC0+IG1pbWVcbiAgICBmb3IgKHZhciBpID0gMDsgaSA8IGV4dHMubGVuZ3RoOyBpKyspIHtcbiAgICAgIHZhciBleHRlbnNpb24gPSBleHRzW2ldXG5cbiAgICAgIGlmICh0eXBlc1tleHRlbnNpb25dKSB7XG4gICAgICAgIHZhciBmcm9tID0gcHJlZmVyZW5jZS5pbmRleE9mKGRiW3R5cGVzW2V4dGVuc2lvbl1dLnNvdXJjZSlcbiAgICAgICAgdmFyIHRvID0gcHJlZmVyZW5jZS5pbmRleE9mKG1pbWUuc291cmNlKVxuXG4gICAgICAgIGlmICh0eXBlc1tleHRlbnNpb25dICE9PSAnYXBwbGljYXRpb24vb2N0ZXQtc3RyZWFtJyAmJlxuICAgICAgICAgIChmcm9tID4gdG8gfHwgKGZyb20gPT09IHRvICYmIHR5cGVzW2V4dGVuc2lvbl0uc3Vic3RyKDAsIDEyKSA9PT0gJ2FwcGxpY2F0aW9uLycpKSkge1xuICAgICAgICAgIC8vIHNraXAgdGhlIHJlbWFwcGluZ1xuICAgICAgICAgIGNvbnRpbnVlXG4gICAgICAgIH1cbiAgICAgIH1cblxuICAgICAgLy8gc2V0IHRoZSBleHRlbnNpb24gLT4gbWltZVxuICAgICAgdHlwZXNbZXh0ZW5zaW9uXSA9IHR5cGVcbiAgICB9XG4gIH0pXG59XG4iLCIvKipcbiAqIEhlbHBlcnMuXG4gKi9cblxudmFyIHMgPSAxMDAwO1xudmFyIG0gPSBzICogNjA7XG52YXIgaCA9IG0gKiA2MDtcbnZhciBkID0gaCAqIDI0O1xudmFyIHcgPSBkICogNztcbnZhciB5ID0gZCAqIDM2NS4yNTtcblxuLyoqXG4gKiBQYXJzZSBvciBmb3JtYXQgdGhlIGdpdmVuIGB2YWxgLlxuICpcbiAqIE9wdGlvbnM6XG4gKlxuICogIC0gYGxvbmdgIHZlcmJvc2UgZm9ybWF0dGluZyBbZmFsc2VdXG4gKlxuICogQHBhcmFtIHtTdHJpbmd8TnVtYmVyfSB2YWxcbiAqIEBwYXJhbSB7T2JqZWN0fSBbb3B0aW9uc11cbiAqIEB0aHJvd3Mge0Vycm9yfSB0aHJvdyBhbiBlcnJvciBpZiB2YWwgaXMgbm90IGEgbm9uLWVtcHR5IHN0cmluZyBvciBhIG51bWJlclxuICogQHJldHVybiB7U3RyaW5nfE51bWJlcn1cbiAqIEBhcGkgcHVibGljXG4gKi9cblxubW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbih2YWwsIG9wdGlvbnMpIHtcbiAgb3B0aW9ucyA9IG9wdGlvbnMgfHwge307XG4gIHZhciB0eXBlID0gdHlwZW9mIHZhbDtcbiAgaWYgKHR5cGUgPT09ICdzdHJpbmcnICYmIHZhbC5sZW5ndGggPiAwKSB7XG4gICAgcmV0dXJuIHBhcnNlKHZhbCk7XG4gIH0gZWxzZSBpZiAodHlwZSA9PT0gJ251bWJlcicgJiYgaXNGaW5pdGUodmFsKSkge1xuICAgIHJldHVybiBvcHRpb25zLmxvbmcgPyBmbXRMb25nKHZhbCkgOiBmbXRTaG9ydCh2YWwpO1xuICB9XG4gIHRocm93IG5ldyBFcnJvcihcbiAgICAndmFsIGlzIG5vdCBhIG5vbi1lbXB0eSBzdHJpbmcgb3IgYSB2YWxpZCBudW1iZXIuIHZhbD0nICtcbiAgICAgIEpTT04uc3RyaW5naWZ5KHZhbClcbiAgKTtcbn07XG5cbi8qKlxuICogUGFyc2UgdGhlIGdpdmVuIGBzdHJgIGFuZCByZXR1cm4gbWlsbGlzZWNvbmRzLlxuICpcbiAqIEBwYXJhbSB7U3RyaW5nfSBzdHJcbiAqIEByZXR1cm4ge051bWJlcn1cbiAqIEBhcGkgcHJpdmF0ZVxuICovXG5cbmZ1bmN0aW9uIHBhcnNlKHN0cikge1xuICBzdHIgPSBTdHJpbmcoc3RyKTtcbiAgaWYgKHN0ci5sZW5ndGggPiAxMDApIHtcbiAgICByZXR1cm47XG4gIH1cbiAgdmFyIG1hdGNoID0gL14oLT8oPzpcXGQrKT9cXC4/XFxkKykgKihtaWxsaXNlY29uZHM/fG1zZWNzP3xtc3xzZWNvbmRzP3xzZWNzP3xzfG1pbnV0ZXM/fG1pbnM/fG18aG91cnM/fGhycz98aHxkYXlzP3xkfHdlZWtzP3x3fHllYXJzP3x5cnM/fHkpPyQvaS5leGVjKFxuICAgIHN0clxuICApO1xuICBpZiAoIW1hdGNoKSB7XG4gICAgcmV0dXJuO1xuICB9XG4gIHZhciBuID0gcGFyc2VGbG9hdChtYXRjaFsxXSk7XG4gIHZhciB0eXBlID0gKG1hdGNoWzJdIHx8ICdtcycpLnRvTG93ZXJDYXNlKCk7XG4gIHN3aXRjaCAodHlwZSkge1xuICAgIGNhc2UgJ3llYXJzJzpcbiAgICBjYXNlICd5ZWFyJzpcbiAgICBjYXNlICd5cnMnOlxuICAgIGNhc2UgJ3lyJzpcbiAgICBjYXNlICd5JzpcbiAgICAgIHJldHVybiBuICogeTtcbiAgICBjYXNlICd3ZWVrcyc6XG4gICAgY2FzZSAnd2Vlayc6XG4gICAgY2FzZSAndyc6XG4gICAgICByZXR1cm4gbiAqIHc7XG4gICAgY2FzZSAnZGF5cyc6XG4gICAgY2FzZSAnZGF5JzpcbiAgICBjYXNlICdkJzpcbiAgICAgIHJldHVybiBuICogZDtcbiAgICBjYXNlICdob3Vycyc6XG4gICAgY2FzZSAnaG91cic6XG4gICAgY2FzZSAnaHJzJzpcbiAgICBjYXNlICdocic6XG4gICAgY2FzZSAnaCc6XG4gICAgICByZXR1cm4gbiAqIGg7XG4gICAgY2FzZSAnbWludXRlcyc6XG4gICAgY2FzZSAnbWludXRlJzpcbiAgICBjYXNlICdtaW5zJzpcbiAgICBjYXNlICdtaW4nOlxuICAgIGNhc2UgJ20nOlxuICAgICAgcmV0dXJuIG4gKiBtO1xuICAgIGNhc2UgJ3NlY29uZHMnOlxuICAgIGNhc2UgJ3NlY29uZCc6XG4gICAgY2FzZSAnc2Vjcyc6XG4gICAgY2FzZSAnc2VjJzpcbiAgICBjYXNlICdzJzpcbiAgICAgIHJldHVybiBuICogcztcbiAgICBjYXNlICdtaWxsaXNlY29uZHMnOlxuICAgIGNhc2UgJ21pbGxpc2Vjb25kJzpcbiAgICBjYXNlICdtc2Vjcyc6XG4gICAgY2FzZSAnbXNlYyc6XG4gICAgY2FzZSAnbXMnOlxuICAgICAgcmV0dXJuIG47XG4gICAgZGVmYXVsdDpcbiAgICAgIHJldHVybiB1bmRlZmluZWQ7XG4gIH1cbn1cblxuLyoqXG4gKiBTaG9ydCBmb3JtYXQgZm9yIGBtc2AuXG4gKlxuICogQHBhcmFtIHtOdW1iZXJ9IG1zXG4gKiBAcmV0dXJuIHtTdHJpbmd9XG4gKiBAYXBpIHByaXZhdGVcbiAqL1xuXG5mdW5jdGlvbiBmbXRTaG9ydChtcykge1xuICB2YXIgbXNBYnMgPSBNYXRoLmFicyhtcyk7XG4gIGlmIChtc0FicyA+PSBkKSB7XG4gICAgcmV0dXJuIE1hdGgucm91bmQobXMgLyBkKSArICdkJztcbiAgfVxuICBpZiAobXNBYnMgPj0gaCkge1xuICAgIHJldHVybiBNYXRoLnJvdW5kKG1zIC8gaCkgKyAnaCc7XG4gIH1cbiAgaWYgKG1zQWJzID49IG0pIHtcbiAgICByZXR1cm4gTWF0aC5yb3VuZChtcyAvIG0pICsgJ20nO1xuICB9XG4gIGlmIChtc0FicyA+PSBzKSB7XG4gICAgcmV0dXJuIE1hdGgucm91bmQobXMgLyBzKSArICdzJztcbiAgfVxuICByZXR1cm4gbXMgKyAnbXMnO1xufVxuXG4vKipcbiAqIExvbmcgZm9ybWF0IGZvciBgbXNgLlxuICpcbiAqIEBwYXJhbSB7TnVtYmVyfSBtc1xuICogQHJldHVybiB7U3RyaW5nfVxuICogQGFwaSBwcml2YXRlXG4gKi9cblxuZnVuY3Rpb24gZm10TG9uZyhtcykge1xuICB2YXIgbXNBYnMgPSBNYXRoLmFicyhtcyk7XG4gIGlmIChtc0FicyA+PSBkKSB7XG4gICAgcmV0dXJuIHBsdXJhbChtcywgbXNBYnMsIGQsICdkYXknKTtcbiAgfVxuICBpZiAobXNBYnMgPj0gaCkge1xuICAgIHJldHVybiBwbHVyYWwobXMsIG1zQWJzLCBoLCAnaG91cicpO1xuICB9XG4gIGlmIChtc0FicyA+PSBtKSB7XG4gICAgcmV0dXJuIHBsdXJhbChtcywgbXNBYnMsIG0sICdtaW51dGUnKTtcbiAgfVxuICBpZiAobXNBYnMgPj0gcykge1xuICAgIHJldHVybiBwbHVyYWwobXMsIG1zQWJzLCBzLCAnc2Vjb25kJyk7XG4gIH1cbiAgcmV0dXJuIG1zICsgJyBtcyc7XG59XG5cbi8qKlxuICogUGx1cmFsaXphdGlvbiBoZWxwZXIuXG4gKi9cblxuZnVuY3Rpb24gcGx1cmFsKG1zLCBtc0FicywgbiwgbmFtZSkge1xuICB2YXIgaXNQbHVyYWwgPSBtc0FicyA+PSBuICogMS41O1xuICByZXR1cm4gTWF0aC5yb3VuZChtcyAvIG4pICsgJyAnICsgbmFtZSArIChpc1BsdXJhbCA/ICdzJyA6ICcnKTtcbn1cbiIsIid1c2Ugc3RyaWN0JztcblxudmFyIHBhcnNlVXJsID0gcmVxdWlyZSgndXJsJykucGFyc2U7XG5cbnZhciBERUZBVUxUX1BPUlRTID0ge1xuICBmdHA6IDIxLFxuICBnb3BoZXI6IDcwLFxuICBodHRwOiA4MCxcbiAgaHR0cHM6IDQ0MyxcbiAgd3M6IDgwLFxuICB3c3M6IDQ0Myxcbn07XG5cbnZhciBzdHJpbmdFbmRzV2l0aCA9IFN0cmluZy5wcm90b3R5cGUuZW5kc1dpdGggfHwgZnVuY3Rpb24ocykge1xuICByZXR1cm4gcy5sZW5ndGggPD0gdGhpcy5sZW5ndGggJiZcbiAgICB0aGlzLmluZGV4T2YocywgdGhpcy5sZW5ndGggLSBzLmxlbmd0aCkgIT09IC0xO1xufTtcblxuLyoqXG4gKiBAcGFyYW0ge3N0cmluZ3xvYmplY3R9IHVybCAtIFRoZSBVUkwsIG9yIHRoZSByZXN1bHQgZnJvbSB1cmwucGFyc2UuXG4gKiBAcmV0dXJuIHtzdHJpbmd9IFRoZSBVUkwgb2YgdGhlIHByb3h5IHRoYXQgc2hvdWxkIGhhbmRsZSB0aGUgcmVxdWVzdCB0byB0aGVcbiAqICBnaXZlbiBVUkwuIElmIG5vIHByb3h5IGlzIHNldCwgdGhpcyB3aWxsIGJlIGFuIGVtcHR5IHN0cmluZy5cbiAqL1xuZnVuY3Rpb24gZ2V0UHJveHlGb3JVcmwodXJsKSB7XG4gIHZhciBwYXJzZWRVcmwgPSB0eXBlb2YgdXJsID09PSAnc3RyaW5nJyA/IHBhcnNlVXJsKHVybCkgOiB1cmwgfHwge307XG4gIHZhciBwcm90byA9IHBhcnNlZFVybC5wcm90b2NvbDtcbiAgdmFyIGhvc3RuYW1lID0gcGFyc2VkVXJsLmhvc3Q7XG4gIHZhciBwb3J0ID0gcGFyc2VkVXJsLnBvcnQ7XG4gIGlmICh0eXBlb2YgaG9zdG5hbWUgIT09ICdzdHJpbmcnIHx8ICFob3N0bmFtZSB8fCB0eXBlb2YgcHJvdG8gIT09ICdzdHJpbmcnKSB7XG4gICAgcmV0dXJuICcnOyAgLy8gRG9uJ3QgcHJveHkgVVJMcyB3aXRob3V0IGEgdmFsaWQgc2NoZW1lIG9yIGhvc3QuXG4gIH1cblxuICBwcm90byA9IHByb3RvLnNwbGl0KCc6JywgMSlbMF07XG4gIC8vIFN0cmlwcGluZyBwb3J0cyBpbiB0aGlzIHdheSBpbnN0ZWFkIG9mIHVzaW5nIHBhcnNlZFVybC5ob3N0bmFtZSB0byBtYWtlXG4gIC8vIHN1cmUgdGhhdCB0aGUgYnJhY2tldHMgYXJvdW5kIElQdjYgYWRkcmVzc2VzIGFyZSBrZXB0LlxuICBob3N0bmFtZSA9IGhvc3RuYW1lLnJlcGxhY2UoLzpcXGQqJC8sICcnKTtcbiAgcG9ydCA9IHBhcnNlSW50KHBvcnQpIHx8IERFRkFVTFRfUE9SVFNbcHJvdG9dIHx8IDA7XG4gIGlmICghc2hvdWxkUHJveHkoaG9zdG5hbWUsIHBvcnQpKSB7XG4gICAgcmV0dXJuICcnOyAgLy8gRG9uJ3QgcHJveHkgVVJMcyB0aGF0IG1hdGNoIE5PX1BST1hZLlxuICB9XG5cbiAgdmFyIHByb3h5ID1cbiAgICBnZXRFbnYoJ25wbV9jb25maWdfJyArIHByb3RvICsgJ19wcm94eScpIHx8XG4gICAgZ2V0RW52KHByb3RvICsgJ19wcm94eScpIHx8XG4gICAgZ2V0RW52KCducG1fY29uZmlnX3Byb3h5JykgfHxcbiAgICBnZXRFbnYoJ2FsbF9wcm94eScpO1xuICBpZiAocHJveHkgJiYgcHJveHkuaW5kZXhPZignOi8vJykgPT09IC0xKSB7XG4gICAgLy8gTWlzc2luZyBzY2hlbWUgaW4gcHJveHksIGRlZmF1bHQgdG8gdGhlIHJlcXVlc3RlZCBVUkwncyBzY2hlbWUuXG4gICAgcHJveHkgPSBwcm90byArICc6Ly8nICsgcHJveHk7XG4gIH1cbiAgcmV0dXJuIHByb3h5O1xufVxuXG4vKipcbiAqIERldGVybWluZXMgd2hldGhlciBhIGdpdmVuIFVSTCBzaG91bGQgYmUgcHJveGllZC5cbiAqXG4gKiBAcGFyYW0ge3N0cmluZ30gaG9zdG5hbWUgLSBUaGUgaG9zdCBuYW1lIG9mIHRoZSBVUkwuXG4gKiBAcGFyYW0ge251bWJlcn0gcG9ydCAtIFRoZSBlZmZlY3RpdmUgcG9ydCBvZiB0aGUgVVJMLlxuICogQHJldHVybnMge2Jvb2xlYW59IFdoZXRoZXIgdGhlIGdpdmVuIFVSTCBzaG91bGQgYmUgcHJveGllZC5cbiAqIEBwcml2YXRlXG4gKi9cbmZ1bmN0aW9uIHNob3VsZFByb3h5KGhvc3RuYW1lLCBwb3J0KSB7XG4gIHZhciBOT19QUk9YWSA9XG4gICAgKGdldEVudignbnBtX2NvbmZpZ19ub19wcm94eScpIHx8IGdldEVudignbm9fcHJveHknKSkudG9Mb3dlckNhc2UoKTtcbiAgaWYgKCFOT19QUk9YWSkge1xuICAgIHJldHVybiB0cnVlOyAgLy8gQWx3YXlzIHByb3h5IGlmIE5PX1BST1hZIGlzIG5vdCBzZXQuXG4gIH1cbiAgaWYgKE5PX1BST1hZID09PSAnKicpIHtcbiAgICByZXR1cm4gZmFsc2U7ICAvLyBOZXZlciBwcm94eSBpZiB3aWxkY2FyZCBpcyBzZXQuXG4gIH1cblxuICByZXR1cm4gTk9fUFJPWFkuc3BsaXQoL1ssXFxzXS8pLmV2ZXJ5KGZ1bmN0aW9uKHByb3h5KSB7XG4gICAgaWYgKCFwcm94eSkge1xuICAgICAgcmV0dXJuIHRydWU7ICAvLyBTa2lwIHplcm8tbGVuZ3RoIGhvc3RzLlxuICAgIH1cbiAgICB2YXIgcGFyc2VkUHJveHkgPSBwcm94eS5tYXRjaCgvXiguKyk6KFxcZCspJC8pO1xuICAgIHZhciBwYXJzZWRQcm94eUhvc3RuYW1lID0gcGFyc2VkUHJveHkgPyBwYXJzZWRQcm94eVsxXSA6IHByb3h5O1xuICAgIHZhciBwYXJzZWRQcm94eVBvcnQgPSBwYXJzZWRQcm94eSA/IHBhcnNlSW50KHBhcnNlZFByb3h5WzJdKSA6IDA7XG4gICAgaWYgKHBhcnNlZFByb3h5UG9ydCAmJiBwYXJzZWRQcm94eVBvcnQgIT09IHBvcnQpIHtcbiAgICAgIHJldHVybiB0cnVlOyAgLy8gU2tpcCBpZiBwb3J0cyBkb24ndCBtYXRjaC5cbiAgICB9XG5cbiAgICBpZiAoIS9eWy4qXS8udGVzdChwYXJzZWRQcm94eUhvc3RuYW1lKSkge1xuICAgICAgLy8gTm8gd2lsZGNhcmRzLCBzbyBzdG9wIHByb3h5aW5nIGlmIHRoZXJlIGlzIGFuIGV4YWN0IG1hdGNoLlxuICAgICAgcmV0dXJuIGhvc3RuYW1lICE9PSBwYXJzZWRQcm94eUhvc3RuYW1lO1xuICAgIH1cblxuICAgIGlmIChwYXJzZWRQcm94eUhvc3RuYW1lLmNoYXJBdCgwKSA9PT0gJyonKSB7XG4gICAgICAvLyBSZW1vdmUgbGVhZGluZyB3aWxkY2FyZC5cbiAgICAgIHBhcnNlZFByb3h5SG9zdG5hbWUgPSBwYXJzZWRQcm94eUhvc3RuYW1lLnNsaWNlKDEpO1xuICAgIH1cbiAgICAvLyBTdG9wIHByb3h5aW5nIGlmIHRoZSBob3N0bmFtZSBlbmRzIHdpdGggdGhlIG5vX3Byb3h5IGhvc3QuXG4gICAgcmV0dXJuICFzdHJpbmdFbmRzV2l0aC5jYWxsKGhvc3RuYW1lLCBwYXJzZWRQcm94eUhvc3RuYW1lKTtcbiAgfSk7XG59XG5cbi8qKlxuICogR2V0IHRoZSB2YWx1ZSBmb3IgYW4gZW52aXJvbm1lbnQgdmFyaWFibGUuXG4gKlxuICogQHBhcmFtIHtzdHJpbmd9IGtleSAtIFRoZSBuYW1lIG9mIHRoZSBlbnZpcm9ubWVudCB2YXJpYWJsZS5cbiAqIEByZXR1cm4ge3N0cmluZ30gVGhlIHZhbHVlIG9mIHRoZSBlbnZpcm9ubWVudCB2YXJpYWJsZS5cbiAqIEBwcml2YXRlXG4gKi9cbmZ1bmN0aW9uIGdldEVudihrZXkpIHtcbiAgcmV0dXJuIHByb2Nlc3MuZW52W2tleS50b0xvd2VyQ2FzZSgpXSB8fCBwcm9jZXNzLmVudltrZXkudG9VcHBlckNhc2UoKV0gfHwgJyc7XG59XG5cbmV4cG9ydHMuZ2V0UHJveHlGb3JVcmwgPSBnZXRQcm94eUZvclVybDtcbiIsIid1c2Ugc3RyaWN0JztcbmNvbnN0IG9zID0gcmVxdWlyZSgnb3MnKTtcbmNvbnN0IGhhc0ZsYWcgPSByZXF1aXJlKCdoYXMtZmxhZycpO1xuXG5jb25zdCBlbnYgPSBwcm9jZXNzLmVudjtcblxubGV0IGZvcmNlQ29sb3I7XG5pZiAoaGFzRmxhZygnbm8tY29sb3InKSB8fFxuXHRoYXNGbGFnKCduby1jb2xvcnMnKSB8fFxuXHRoYXNGbGFnKCdjb2xvcj1mYWxzZScpKSB7XG5cdGZvcmNlQ29sb3IgPSBmYWxzZTtcbn0gZWxzZSBpZiAoaGFzRmxhZygnY29sb3InKSB8fFxuXHRoYXNGbGFnKCdjb2xvcnMnKSB8fFxuXHRoYXNGbGFnKCdjb2xvcj10cnVlJykgfHxcblx0aGFzRmxhZygnY29sb3I9YWx3YXlzJykpIHtcblx0Zm9yY2VDb2xvciA9IHRydWU7XG59XG5pZiAoJ0ZPUkNFX0NPTE9SJyBpbiBlbnYpIHtcblx0Zm9yY2VDb2xvciA9IGVudi5GT1JDRV9DT0xPUi5sZW5ndGggPT09IDAgfHwgcGFyc2VJbnQoZW52LkZPUkNFX0NPTE9SLCAxMCkgIT09IDA7XG59XG5cbmZ1bmN0aW9uIHRyYW5zbGF0ZUxldmVsKGxldmVsKSB7XG5cdGlmIChsZXZlbCA9PT0gMCkge1xuXHRcdHJldHVybiBmYWxzZTtcblx0fVxuXG5cdHJldHVybiB7XG5cdFx0bGV2ZWwsXG5cdFx0aGFzQmFzaWM6IHRydWUsXG5cdFx0aGFzMjU2OiBsZXZlbCA+PSAyLFxuXHRcdGhhczE2bTogbGV2ZWwgPj0gM1xuXHR9O1xufVxuXG5mdW5jdGlvbiBzdXBwb3J0c0NvbG9yKHN0cmVhbSkge1xuXHRpZiAoZm9yY2VDb2xvciA9PT0gZmFsc2UpIHtcblx0XHRyZXR1cm4gMDtcblx0fVxuXG5cdGlmIChoYXNGbGFnKCdjb2xvcj0xNm0nKSB8fFxuXHRcdGhhc0ZsYWcoJ2NvbG9yPWZ1bGwnKSB8fFxuXHRcdGhhc0ZsYWcoJ2NvbG9yPXRydWVjb2xvcicpKSB7XG5cdFx0cmV0dXJuIDM7XG5cdH1cblxuXHRpZiAoaGFzRmxhZygnY29sb3I9MjU2JykpIHtcblx0XHRyZXR1cm4gMjtcblx0fVxuXG5cdGlmIChzdHJlYW0gJiYgIXN0cmVhbS5pc1RUWSAmJiBmb3JjZUNvbG9yICE9PSB0cnVlKSB7XG5cdFx0cmV0dXJuIDA7XG5cdH1cblxuXHRjb25zdCBtaW4gPSBmb3JjZUNvbG9yID8gMSA6IDA7XG5cblx0aWYgKHByb2Nlc3MucGxhdGZvcm0gPT09ICd3aW4zMicpIHtcblx0XHQvLyBOb2RlLmpzIDcuNS4wIGlzIHRoZSBmaXJzdCB2ZXJzaW9uIG9mIE5vZGUuanMgdG8gaW5jbHVkZSBhIHBhdGNoIHRvXG5cdFx0Ly8gbGlidXYgdGhhdCBlbmFibGVzIDI1NiBjb2xvciBvdXRwdXQgb24gV2luZG93cy4gQW55dGhpbmcgZWFybGllciBhbmQgaXRcblx0XHQvLyB3b24ndCB3b3JrLiBIb3dldmVyLCBoZXJlIHdlIHRhcmdldCBOb2RlLmpzIDggYXQgbWluaW11bSBhcyBpdCBpcyBhbiBMVFNcblx0XHQvLyByZWxlYXNlLCBhbmQgTm9kZS5qcyA3IGlzIG5vdC4gV2luZG93cyAxMCBidWlsZCAxMDU4NiBpcyB0aGUgZmlyc3QgV2luZG93c1xuXHRcdC8vIHJlbGVhc2UgdGhhdCBzdXBwb3J0cyAyNTYgY29sb3JzLiBXaW5kb3dzIDEwIGJ1aWxkIDE0OTMxIGlzIHRoZSBmaXJzdCByZWxlYXNlXG5cdFx0Ly8gdGhhdCBzdXBwb3J0cyAxNm0vVHJ1ZUNvbG9yLlxuXHRcdGNvbnN0IG9zUmVsZWFzZSA9IG9zLnJlbGVhc2UoKS5zcGxpdCgnLicpO1xuXHRcdGlmIChcblx0XHRcdE51bWJlcihwcm9jZXNzLnZlcnNpb25zLm5vZGUuc3BsaXQoJy4nKVswXSkgPj0gOCAmJlxuXHRcdFx0TnVtYmVyKG9zUmVsZWFzZVswXSkgPj0gMTAgJiZcblx0XHRcdE51bWJlcihvc1JlbGVhc2VbMl0pID49IDEwNTg2XG5cdFx0KSB7XG5cdFx0XHRyZXR1cm4gTnVtYmVyKG9zUmVsZWFzZVsyXSkgPj0gMTQ5MzEgPyAzIDogMjtcblx0XHR9XG5cblx0XHRyZXR1cm4gMTtcblx0fVxuXG5cdGlmICgnQ0knIGluIGVudikge1xuXHRcdGlmIChbJ1RSQVZJUycsICdDSVJDTEVDSScsICdBUFBWRVlPUicsICdHSVRMQUJfQ0knXS5zb21lKHNpZ24gPT4gc2lnbiBpbiBlbnYpIHx8IGVudi5DSV9OQU1FID09PSAnY29kZXNoaXAnKSB7XG5cdFx0XHRyZXR1cm4gMTtcblx0XHR9XG5cblx0XHRyZXR1cm4gbWluO1xuXHR9XG5cblx0aWYgKCdURUFNQ0lUWV9WRVJTSU9OJyBpbiBlbnYpIHtcblx0XHRyZXR1cm4gL14oOVxcLigwKlsxLTldXFxkKilcXC58XFxkezIsfVxcLikvLnRlc3QoZW52LlRFQU1DSVRZX1ZFUlNJT04pID8gMSA6IDA7XG5cdH1cblxuXHRpZiAoZW52LkNPTE9SVEVSTSA9PT0gJ3RydWVjb2xvcicpIHtcblx0XHRyZXR1cm4gMztcblx0fVxuXG5cdGlmICgnVEVSTV9QUk9HUkFNJyBpbiBlbnYpIHtcblx0XHRjb25zdCB2ZXJzaW9uID0gcGFyc2VJbnQoKGVudi5URVJNX1BST0dSQU1fVkVSU0lPTiB8fCAnJykuc3BsaXQoJy4nKVswXSwgMTApO1xuXG5cdFx0c3dpdGNoIChlbnYuVEVSTV9QUk9HUkFNKSB7XG5cdFx0XHRjYXNlICdpVGVybS5hcHAnOlxuXHRcdFx0XHRyZXR1cm4gdmVyc2lvbiA+PSAzID8gMyA6IDI7XG5cdFx0XHRjYXNlICdBcHBsZV9UZXJtaW5hbCc6XG5cdFx0XHRcdHJldHVybiAyO1xuXHRcdFx0Ly8gTm8gZGVmYXVsdFxuXHRcdH1cblx0fVxuXG5cdGlmICgvLTI1Nihjb2xvcik/JC9pLnRlc3QoZW52LlRFUk0pKSB7XG5cdFx0cmV0dXJuIDI7XG5cdH1cblxuXHRpZiAoL15zY3JlZW58Xnh0ZXJtfF52dDEwMHxednQyMjB8XnJ4dnR8Y29sb3J8YW5zaXxjeWd3aW58bGludXgvaS50ZXN0KGVudi5URVJNKSkge1xuXHRcdHJldHVybiAxO1xuXHR9XG5cblx0aWYgKCdDT0xPUlRFUk0nIGluIGVudikge1xuXHRcdHJldHVybiAxO1xuXHR9XG5cblx0aWYgKGVudi5URVJNID09PSAnZHVtYicpIHtcblx0XHRyZXR1cm4gbWluO1xuXHR9XG5cblx0cmV0dXJuIG1pbjtcbn1cblxuZnVuY3Rpb24gZ2V0U3VwcG9ydExldmVsKHN0cmVhbSkge1xuXHRjb25zdCBsZXZlbCA9IHN1cHBvcnRzQ29sb3Ioc3RyZWFtKTtcblx0cmV0dXJuIHRyYW5zbGF0ZUxldmVsKGxldmVsKTtcbn1cblxubW9kdWxlLmV4cG9ydHMgPSB7XG5cdHN1cHBvcnRzQ29sb3I6IGdldFN1cHBvcnRMZXZlbCxcblx0c3Rkb3V0OiBnZXRTdXBwb3J0TGV2ZWwocHJvY2Vzcy5zdGRvdXQpLFxuXHRzdGRlcnI6IGdldFN1cHBvcnRMZXZlbChwcm9jZXNzLnN0ZGVycilcbn07XG4iLCIoZnVuY3Rpb24gKG5hbWUsIGNvbnRleHQsIGRlZmluaXRpb24pIHtcbiAgaWYgKHR5cGVvZiBtb2R1bGUgIT09ICd1bmRlZmluZWQnICYmIG1vZHVsZS5leHBvcnRzKSBtb2R1bGUuZXhwb3J0cyA9IGRlZmluaXRpb24oKTtcbiAgZWxzZSBpZiAodHlwZW9mIGRlZmluZSA9PT0gJ2Z1bmN0aW9uJyAmJiBkZWZpbmUuYW1kKSBkZWZpbmUoZGVmaW5pdGlvbik7XG4gIGVsc2UgY29udGV4dFtuYW1lXSA9IGRlZmluaXRpb24oKTtcbn0pKCd1cmxqb2luJywgdGhpcywgZnVuY3Rpb24gKCkge1xuXG4gIGZ1bmN0aW9uIG5vcm1hbGl6ZSAoc3RyQXJyYXkpIHtcbiAgICB2YXIgcmVzdWx0QXJyYXkgPSBbXTtcbiAgICBpZiAoc3RyQXJyYXkubGVuZ3RoID09PSAwKSB7IHJldHVybiAnJzsgfVxuXG4gICAgaWYgKHR5cGVvZiBzdHJBcnJheVswXSAhPT0gJ3N0cmluZycpIHtcbiAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ1VybCBtdXN0IGJlIGEgc3RyaW5nLiBSZWNlaXZlZCAnICsgc3RyQXJyYXlbMF0pO1xuICAgIH1cblxuICAgIC8vIElmIHRoZSBmaXJzdCBwYXJ0IGlzIGEgcGxhaW4gcHJvdG9jb2wsIHdlIGNvbWJpbmUgaXQgd2l0aCB0aGUgbmV4dCBwYXJ0LlxuICAgIGlmIChzdHJBcnJheVswXS5tYXRjaCgvXlteLzpdKzpcXC8qJC8pICYmIHN0ckFycmF5Lmxlbmd0aCA+IDEpIHtcbiAgICAgIHZhciBmaXJzdCA9IHN0ckFycmF5LnNoaWZ0KCk7XG4gICAgICBzdHJBcnJheVswXSA9IGZpcnN0ICsgc3RyQXJyYXlbMF07XG4gICAgfVxuXG4gICAgLy8gVGhlcmUgbXVzdCBiZSB0d28gb3IgdGhyZWUgc2xhc2hlcyBpbiB0aGUgZmlsZSBwcm90b2NvbCwgdHdvIHNsYXNoZXMgaW4gYW55dGhpbmcgZWxzZS5cbiAgICBpZiAoc3RyQXJyYXlbMF0ubWF0Y2goL15maWxlOlxcL1xcL1xcLy8pKSB7XG4gICAgICBzdHJBcnJheVswXSA9IHN0ckFycmF5WzBdLnJlcGxhY2UoL14oW14vOl0rKTpcXC8qLywgJyQxOi8vLycpO1xuICAgIH0gZWxzZSB7XG4gICAgICBzdHJBcnJheVswXSA9IHN0ckFycmF5WzBdLnJlcGxhY2UoL14oW14vOl0rKTpcXC8qLywgJyQxOi8vJyk7XG4gICAgfVxuXG4gICAgZm9yICh2YXIgaSA9IDA7IGkgPCBzdHJBcnJheS5sZW5ndGg7IGkrKykge1xuICAgICAgdmFyIGNvbXBvbmVudCA9IHN0ckFycmF5W2ldO1xuXG4gICAgICBpZiAodHlwZW9mIGNvbXBvbmVudCAhPT0gJ3N0cmluZycpIHtcbiAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcignVXJsIG11c3QgYmUgYSBzdHJpbmcuIFJlY2VpdmVkICcgKyBjb21wb25lbnQpO1xuICAgICAgfVxuXG4gICAgICBpZiAoY29tcG9uZW50ID09PSAnJykgeyBjb250aW51ZTsgfVxuXG4gICAgICBpZiAoaSA+IDApIHtcbiAgICAgICAgLy8gUmVtb3ZpbmcgdGhlIHN0YXJ0aW5nIHNsYXNoZXMgZm9yIGVhY2ggY29tcG9uZW50IGJ1dCB0aGUgZmlyc3QuXG4gICAgICAgIGNvbXBvbmVudCA9IGNvbXBvbmVudC5yZXBsYWNlKC9eW1xcL10rLywgJycpO1xuICAgICAgfVxuICAgICAgaWYgKGkgPCBzdHJBcnJheS5sZW5ndGggLSAxKSB7XG4gICAgICAgIC8vIFJlbW92aW5nIHRoZSBlbmRpbmcgc2xhc2hlcyBmb3IgZWFjaCBjb21wb25lbnQgYnV0IHRoZSBsYXN0LlxuICAgICAgICBjb21wb25lbnQgPSBjb21wb25lbnQucmVwbGFjZSgvW1xcL10rJC8sICcnKTtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIC8vIEZvciB0aGUgbGFzdCBjb21wb25lbnQgd2Ugd2lsbCBjb21iaW5lIG11bHRpcGxlIHNsYXNoZXMgdG8gYSBzaW5nbGUgb25lLlxuICAgICAgICBjb21wb25lbnQgPSBjb21wb25lbnQucmVwbGFjZSgvW1xcL10rJC8sICcvJyk7XG4gICAgICB9XG5cbiAgICAgIHJlc3VsdEFycmF5LnB1c2goY29tcG9uZW50KTtcblxuICAgIH1cblxuICAgIHZhciBzdHIgPSByZXN1bHRBcnJheS5qb2luKCcvJyk7XG4gICAgLy8gRWFjaCBpbnB1dCBjb21wb25lbnQgaXMgbm93IHNlcGFyYXRlZCBieSBhIHNpbmdsZSBzbGFzaCBleGNlcHQgdGhlIHBvc3NpYmxlIGZpcnN0IHBsYWluIHByb3RvY29sIHBhcnQuXG5cbiAgICAvLyByZW1vdmUgdHJhaWxpbmcgc2xhc2ggYmVmb3JlIHBhcmFtZXRlcnMgb3IgaGFzaFxuICAgIHN0ciA9IHN0ci5yZXBsYWNlKC9cXC8oXFw/fCZ8I1teIV0pL2csICckMScpO1xuXG4gICAgLy8gcmVwbGFjZSA/IGluIHBhcmFtZXRlcnMgd2l0aCAmXG4gICAgdmFyIHBhcnRzID0gc3RyLnNwbGl0KCc/Jyk7XG4gICAgc3RyID0gcGFydHMuc2hpZnQoKSArIChwYXJ0cy5sZW5ndGggPiAwID8gJz8nOiAnJykgKyBwYXJ0cy5qb2luKCcmJyk7XG5cbiAgICByZXR1cm4gc3RyO1xuICB9XG5cbiAgcmV0dXJuIGZ1bmN0aW9uICgpIHtcbiAgICB2YXIgaW5wdXQ7XG5cbiAgICBpZiAodHlwZW9mIGFyZ3VtZW50c1swXSA9PT0gJ29iamVjdCcpIHtcbiAgICAgIGlucHV0ID0gYXJndW1lbnRzWzBdO1xuICAgIH0gZWxzZSB7XG4gICAgICBpbnB1dCA9IFtdLnNsaWNlLmNhbGwoYXJndW1lbnRzKTtcbiAgICB9XG5cbiAgICByZXR1cm4gbm9ybWFsaXplKGlucHV0KTtcbiAgfTtcblxufSk7XG4iLCJtb2R1bGUuZXhwb3J0cyA9IHJlcXVpcmUoXCJhc3NlcnRcIik7IiwibW9kdWxlLmV4cG9ydHMgPSByZXF1aXJlKFwiZXZlbnRzXCIpOyIsIm1vZHVsZS5leHBvcnRzID0gcmVxdWlyZShcImZzXCIpOyIsIm1vZHVsZS5leHBvcnRzID0gcmVxdWlyZShcImh0dHBcIik7IiwibW9kdWxlLmV4cG9ydHMgPSByZXF1aXJlKFwiaHR0cHNcIik7IiwibW9kdWxlLmV4cG9ydHMgPSByZXF1aXJlKFwib3NcIik7IiwibW9kdWxlLmV4cG9ydHMgPSByZXF1aXJlKFwicGF0aFwiKTsiLCJtb2R1bGUuZXhwb3J0cyA9IHJlcXVpcmUoXCJzdHJlYW1cIik7IiwibW9kdWxlLmV4cG9ydHMgPSByZXF1aXJlKFwidHR5XCIpOyIsIm1vZHVsZS5leHBvcnRzID0gcmVxdWlyZShcInVybFwiKTsiLCJtb2R1bGUuZXhwb3J0cyA9IHJlcXVpcmUoXCJ1dGlsXCIpOyIsIm1vZHVsZS5leHBvcnRzID0gcmVxdWlyZShcInpsaWJcIik7IiwiLy8gQXhpb3MgdjEuNi4wIENvcHlyaWdodCAoYykgMjAyMyBNYXR0IFphYnJpc2tpZSBhbmQgY29udHJpYnV0b3JzXG4ndXNlIHN0cmljdCc7XG5cbmNvbnN0IEZvcm1EYXRhJDEgPSByZXF1aXJlKCdmb3JtLWRhdGEnKTtcbmNvbnN0IHVybCA9IHJlcXVpcmUoJ3VybCcpO1xuY29uc3QgcHJveHlGcm9tRW52ID0gcmVxdWlyZSgncHJveHktZnJvbS1lbnYnKTtcbmNvbnN0IGh0dHAgPSByZXF1aXJlKCdodHRwJyk7XG5jb25zdCBodHRwcyA9IHJlcXVpcmUoJ2h0dHBzJyk7XG5jb25zdCB1dGlsID0gcmVxdWlyZSgndXRpbCcpO1xuY29uc3QgZm9sbG93UmVkaXJlY3RzID0gcmVxdWlyZSgnZm9sbG93LXJlZGlyZWN0cycpO1xuY29uc3QgemxpYiA9IHJlcXVpcmUoJ3psaWInKTtcbmNvbnN0IHN0cmVhbSA9IHJlcXVpcmUoJ3N0cmVhbScpO1xuY29uc3QgRXZlbnRFbWl0dGVyID0gcmVxdWlyZSgnZXZlbnRzJyk7XG5cbmZ1bmN0aW9uIF9pbnRlcm9wRGVmYXVsdExlZ2FjeSAoZSkgeyByZXR1cm4gZSAmJiB0eXBlb2YgZSA9PT0gJ29iamVjdCcgJiYgJ2RlZmF1bHQnIGluIGUgPyBlIDogeyAnZGVmYXVsdCc6IGUgfTsgfVxuXG5jb25zdCBGb3JtRGF0YV9fZGVmYXVsdCA9IC8qI19fUFVSRV9fKi9faW50ZXJvcERlZmF1bHRMZWdhY3koRm9ybURhdGEkMSk7XG5jb25zdCB1cmxfX2RlZmF1bHQgPSAvKiNfX1BVUkVfXyovX2ludGVyb3BEZWZhdWx0TGVnYWN5KHVybCk7XG5jb25zdCBodHRwX19kZWZhdWx0ID0gLyojX19QVVJFX18qL19pbnRlcm9wRGVmYXVsdExlZ2FjeShodHRwKTtcbmNvbnN0IGh0dHBzX19kZWZhdWx0ID0gLyojX19QVVJFX18qL19pbnRlcm9wRGVmYXVsdExlZ2FjeShodHRwcyk7XG5jb25zdCB1dGlsX19kZWZhdWx0ID0gLyojX19QVVJFX18qL19pbnRlcm9wRGVmYXVsdExlZ2FjeSh1dGlsKTtcbmNvbnN0IGZvbGxvd1JlZGlyZWN0c19fZGVmYXVsdCA9IC8qI19fUFVSRV9fKi9faW50ZXJvcERlZmF1bHRMZWdhY3koZm9sbG93UmVkaXJlY3RzKTtcbmNvbnN0IHpsaWJfX2RlZmF1bHQgPSAvKiNfX1BVUkVfXyovX2ludGVyb3BEZWZhdWx0TGVnYWN5KHpsaWIpO1xuY29uc3Qgc3RyZWFtX19kZWZhdWx0ID0gLyojX19QVVJFX18qL19pbnRlcm9wRGVmYXVsdExlZ2FjeShzdHJlYW0pO1xuY29uc3QgRXZlbnRFbWl0dGVyX19kZWZhdWx0ID0gLyojX19QVVJFX18qL19pbnRlcm9wRGVmYXVsdExlZ2FjeShFdmVudEVtaXR0ZXIpO1xuXG5mdW5jdGlvbiBiaW5kKGZuLCB0aGlzQXJnKSB7XG4gIHJldHVybiBmdW5jdGlvbiB3cmFwKCkge1xuICAgIHJldHVybiBmbi5hcHBseSh0aGlzQXJnLCBhcmd1bWVudHMpO1xuICB9O1xufVxuXG4vLyB1dGlscyBpcyBhIGxpYnJhcnkgb2YgZ2VuZXJpYyBoZWxwZXIgZnVuY3Rpb25zIG5vbi1zcGVjaWZpYyB0byBheGlvc1xuXG5jb25zdCB7dG9TdHJpbmd9ID0gT2JqZWN0LnByb3RvdHlwZTtcbmNvbnN0IHtnZXRQcm90b3R5cGVPZn0gPSBPYmplY3Q7XG5cbmNvbnN0IGtpbmRPZiA9IChjYWNoZSA9PiB0aGluZyA9PiB7XG4gICAgY29uc3Qgc3RyID0gdG9TdHJpbmcuY2FsbCh0aGluZyk7XG4gICAgcmV0dXJuIGNhY2hlW3N0cl0gfHwgKGNhY2hlW3N0cl0gPSBzdHIuc2xpY2UoOCwgLTEpLnRvTG93ZXJDYXNlKCkpO1xufSkoT2JqZWN0LmNyZWF0ZShudWxsKSk7XG5cbmNvbnN0IGtpbmRPZlRlc3QgPSAodHlwZSkgPT4ge1xuICB0eXBlID0gdHlwZS50b0xvd2VyQ2FzZSgpO1xuICByZXR1cm4gKHRoaW5nKSA9PiBraW5kT2YodGhpbmcpID09PSB0eXBlXG59O1xuXG5jb25zdCB0eXBlT2ZUZXN0ID0gdHlwZSA9PiB0aGluZyA9PiB0eXBlb2YgdGhpbmcgPT09IHR5cGU7XG5cbi8qKlxuICogRGV0ZXJtaW5lIGlmIGEgdmFsdWUgaXMgYW4gQXJyYXlcbiAqXG4gKiBAcGFyYW0ge09iamVjdH0gdmFsIFRoZSB2YWx1ZSB0byB0ZXN0XG4gKlxuICogQHJldHVybnMge2Jvb2xlYW59IFRydWUgaWYgdmFsdWUgaXMgYW4gQXJyYXksIG90aGVyd2lzZSBmYWxzZVxuICovXG5jb25zdCB7aXNBcnJheX0gPSBBcnJheTtcblxuLyoqXG4gKiBEZXRlcm1pbmUgaWYgYSB2YWx1ZSBpcyB1bmRlZmluZWRcbiAqXG4gKiBAcGFyYW0geyp9IHZhbCBUaGUgdmFsdWUgdG8gdGVzdFxuICpcbiAqIEByZXR1cm5zIHtib29sZWFufSBUcnVlIGlmIHRoZSB2YWx1ZSBpcyB1bmRlZmluZWQsIG90aGVyd2lzZSBmYWxzZVxuICovXG5jb25zdCBpc1VuZGVmaW5lZCA9IHR5cGVPZlRlc3QoJ3VuZGVmaW5lZCcpO1xuXG4vKipcbiAqIERldGVybWluZSBpZiBhIHZhbHVlIGlzIGEgQnVmZmVyXG4gKlxuICogQHBhcmFtIHsqfSB2YWwgVGhlIHZhbHVlIHRvIHRlc3RcbiAqXG4gKiBAcmV0dXJucyB7Ym9vbGVhbn0gVHJ1ZSBpZiB2YWx1ZSBpcyBhIEJ1ZmZlciwgb3RoZXJ3aXNlIGZhbHNlXG4gKi9cbmZ1bmN0aW9uIGlzQnVmZmVyKHZhbCkge1xuICByZXR1cm4gdmFsICE9PSBudWxsICYmICFpc1VuZGVmaW5lZCh2YWwpICYmIHZhbC5jb25zdHJ1Y3RvciAhPT0gbnVsbCAmJiAhaXNVbmRlZmluZWQodmFsLmNvbnN0cnVjdG9yKVxuICAgICYmIGlzRnVuY3Rpb24odmFsLmNvbnN0cnVjdG9yLmlzQnVmZmVyKSAmJiB2YWwuY29uc3RydWN0b3IuaXNCdWZmZXIodmFsKTtcbn1cblxuLyoqXG4gKiBEZXRlcm1pbmUgaWYgYSB2YWx1ZSBpcyBhbiBBcnJheUJ1ZmZlclxuICpcbiAqIEBwYXJhbSB7Kn0gdmFsIFRoZSB2YWx1ZSB0byB0ZXN0XG4gKlxuICogQHJldHVybnMge2Jvb2xlYW59IFRydWUgaWYgdmFsdWUgaXMgYW4gQXJyYXlCdWZmZXIsIG90aGVyd2lzZSBmYWxzZVxuICovXG5jb25zdCBpc0FycmF5QnVmZmVyID0ga2luZE9mVGVzdCgnQXJyYXlCdWZmZXInKTtcblxuXG4vKipcbiAqIERldGVybWluZSBpZiBhIHZhbHVlIGlzIGEgdmlldyBvbiBhbiBBcnJheUJ1ZmZlclxuICpcbiAqIEBwYXJhbSB7Kn0gdmFsIFRoZSB2YWx1ZSB0byB0ZXN0XG4gKlxuICogQHJldHVybnMge2Jvb2xlYW59IFRydWUgaWYgdmFsdWUgaXMgYSB2aWV3IG9uIGFuIEFycmF5QnVmZmVyLCBvdGhlcndpc2UgZmFsc2VcbiAqL1xuZnVuY3Rpb24gaXNBcnJheUJ1ZmZlclZpZXcodmFsKSB7XG4gIGxldCByZXN1bHQ7XG4gIGlmICgodHlwZW9mIEFycmF5QnVmZmVyICE9PSAndW5kZWZpbmVkJykgJiYgKEFycmF5QnVmZmVyLmlzVmlldykpIHtcbiAgICByZXN1bHQgPSBBcnJheUJ1ZmZlci5pc1ZpZXcodmFsKTtcbiAgfSBlbHNlIHtcbiAgICByZXN1bHQgPSAodmFsKSAmJiAodmFsLmJ1ZmZlcikgJiYgKGlzQXJyYXlCdWZmZXIodmFsLmJ1ZmZlcikpO1xuICB9XG4gIHJldHVybiByZXN1bHQ7XG59XG5cbi8qKlxuICogRGV0ZXJtaW5lIGlmIGEgdmFsdWUgaXMgYSBTdHJpbmdcbiAqXG4gKiBAcGFyYW0geyp9IHZhbCBUaGUgdmFsdWUgdG8gdGVzdFxuICpcbiAqIEByZXR1cm5zIHtib29sZWFufSBUcnVlIGlmIHZhbHVlIGlzIGEgU3RyaW5nLCBvdGhlcndpc2UgZmFsc2VcbiAqL1xuY29uc3QgaXNTdHJpbmcgPSB0eXBlT2ZUZXN0KCdzdHJpbmcnKTtcblxuLyoqXG4gKiBEZXRlcm1pbmUgaWYgYSB2YWx1ZSBpcyBhIEZ1bmN0aW9uXG4gKlxuICogQHBhcmFtIHsqfSB2YWwgVGhlIHZhbHVlIHRvIHRlc3RcbiAqIEByZXR1cm5zIHtib29sZWFufSBUcnVlIGlmIHZhbHVlIGlzIGEgRnVuY3Rpb24sIG90aGVyd2lzZSBmYWxzZVxuICovXG5jb25zdCBpc0Z1bmN0aW9uID0gdHlwZU9mVGVzdCgnZnVuY3Rpb24nKTtcblxuLyoqXG4gKiBEZXRlcm1pbmUgaWYgYSB2YWx1ZSBpcyBhIE51bWJlclxuICpcbiAqIEBwYXJhbSB7Kn0gdmFsIFRoZSB2YWx1ZSB0byB0ZXN0XG4gKlxuICogQHJldHVybnMge2Jvb2xlYW59IFRydWUgaWYgdmFsdWUgaXMgYSBOdW1iZXIsIG90aGVyd2lzZSBmYWxzZVxuICovXG5jb25zdCBpc051bWJlciA9IHR5cGVPZlRlc3QoJ251bWJlcicpO1xuXG4vKipcbiAqIERldGVybWluZSBpZiBhIHZhbHVlIGlzIGFuIE9iamVjdFxuICpcbiAqIEBwYXJhbSB7Kn0gdGhpbmcgVGhlIHZhbHVlIHRvIHRlc3RcbiAqXG4gKiBAcmV0dXJucyB7Ym9vbGVhbn0gVHJ1ZSBpZiB2YWx1ZSBpcyBhbiBPYmplY3QsIG90aGVyd2lzZSBmYWxzZVxuICovXG5jb25zdCBpc09iamVjdCA9ICh0aGluZykgPT4gdGhpbmcgIT09IG51bGwgJiYgdHlwZW9mIHRoaW5nID09PSAnb2JqZWN0JztcblxuLyoqXG4gKiBEZXRlcm1pbmUgaWYgYSB2YWx1ZSBpcyBhIEJvb2xlYW5cbiAqXG4gKiBAcGFyYW0geyp9IHRoaW5nIFRoZSB2YWx1ZSB0byB0ZXN0XG4gKiBAcmV0dXJucyB7Ym9vbGVhbn0gVHJ1ZSBpZiB2YWx1ZSBpcyBhIEJvb2xlYW4sIG90aGVyd2lzZSBmYWxzZVxuICovXG5jb25zdCBpc0Jvb2xlYW4gPSB0aGluZyA9PiB0aGluZyA9PT0gdHJ1ZSB8fCB0aGluZyA9PT0gZmFsc2U7XG5cbi8qKlxuICogRGV0ZXJtaW5lIGlmIGEgdmFsdWUgaXMgYSBwbGFpbiBPYmplY3RcbiAqXG4gKiBAcGFyYW0geyp9IHZhbCBUaGUgdmFsdWUgdG8gdGVzdFxuICpcbiAqIEByZXR1cm5zIHtib29sZWFufSBUcnVlIGlmIHZhbHVlIGlzIGEgcGxhaW4gT2JqZWN0LCBvdGhlcndpc2UgZmFsc2VcbiAqL1xuY29uc3QgaXNQbGFpbk9iamVjdCA9ICh2YWwpID0+IHtcbiAgaWYgKGtpbmRPZih2YWwpICE9PSAnb2JqZWN0Jykge1xuICAgIHJldHVybiBmYWxzZTtcbiAgfVxuXG4gIGNvbnN0IHByb3RvdHlwZSA9IGdldFByb3RvdHlwZU9mKHZhbCk7XG4gIHJldHVybiAocHJvdG90eXBlID09PSBudWxsIHx8IHByb3RvdHlwZSA9PT0gT2JqZWN0LnByb3RvdHlwZSB8fCBPYmplY3QuZ2V0UHJvdG90eXBlT2YocHJvdG90eXBlKSA9PT0gbnVsbCkgJiYgIShTeW1ib2wudG9TdHJpbmdUYWcgaW4gdmFsKSAmJiAhKFN5bWJvbC5pdGVyYXRvciBpbiB2YWwpO1xufTtcblxuLyoqXG4gKiBEZXRlcm1pbmUgaWYgYSB2YWx1ZSBpcyBhIERhdGVcbiAqXG4gKiBAcGFyYW0geyp9IHZhbCBUaGUgdmFsdWUgdG8gdGVzdFxuICpcbiAqIEByZXR1cm5zIHtib29sZWFufSBUcnVlIGlmIHZhbHVlIGlzIGEgRGF0ZSwgb3RoZXJ3aXNlIGZhbHNlXG4gKi9cbmNvbnN0IGlzRGF0ZSA9IGtpbmRPZlRlc3QoJ0RhdGUnKTtcblxuLyoqXG4gKiBEZXRlcm1pbmUgaWYgYSB2YWx1ZSBpcyBhIEZpbGVcbiAqXG4gKiBAcGFyYW0geyp9IHZhbCBUaGUgdmFsdWUgdG8gdGVzdFxuICpcbiAqIEByZXR1cm5zIHtib29sZWFufSBUcnVlIGlmIHZhbHVlIGlzIGEgRmlsZSwgb3RoZXJ3aXNlIGZhbHNlXG4gKi9cbmNvbnN0IGlzRmlsZSA9IGtpbmRPZlRlc3QoJ0ZpbGUnKTtcblxuLyoqXG4gKiBEZXRlcm1pbmUgaWYgYSB2YWx1ZSBpcyBhIEJsb2JcbiAqXG4gKiBAcGFyYW0geyp9IHZhbCBUaGUgdmFsdWUgdG8gdGVzdFxuICpcbiAqIEByZXR1cm5zIHtib29sZWFufSBUcnVlIGlmIHZhbHVlIGlzIGEgQmxvYiwgb3RoZXJ3aXNlIGZhbHNlXG4gKi9cbmNvbnN0IGlzQmxvYiA9IGtpbmRPZlRlc3QoJ0Jsb2InKTtcblxuLyoqXG4gKiBEZXRlcm1pbmUgaWYgYSB2YWx1ZSBpcyBhIEZpbGVMaXN0XG4gKlxuICogQHBhcmFtIHsqfSB2YWwgVGhlIHZhbHVlIHRvIHRlc3RcbiAqXG4gKiBAcmV0dXJucyB7Ym9vbGVhbn0gVHJ1ZSBpZiB2YWx1ZSBpcyBhIEZpbGUsIG90aGVyd2lzZSBmYWxzZVxuICovXG5jb25zdCBpc0ZpbGVMaXN0ID0ga2luZE9mVGVzdCgnRmlsZUxpc3QnKTtcblxuLyoqXG4gKiBEZXRlcm1pbmUgaWYgYSB2YWx1ZSBpcyBhIFN0cmVhbVxuICpcbiAqIEBwYXJhbSB7Kn0gdmFsIFRoZSB2YWx1ZSB0byB0ZXN0XG4gKlxuICogQHJldHVybnMge2Jvb2xlYW59IFRydWUgaWYgdmFsdWUgaXMgYSBTdHJlYW0sIG90aGVyd2lzZSBmYWxzZVxuICovXG5jb25zdCBpc1N0cmVhbSA9ICh2YWwpID0+IGlzT2JqZWN0KHZhbCkgJiYgaXNGdW5jdGlvbih2YWwucGlwZSk7XG5cbi8qKlxuICogRGV0ZXJtaW5lIGlmIGEgdmFsdWUgaXMgYSBGb3JtRGF0YVxuICpcbiAqIEBwYXJhbSB7Kn0gdGhpbmcgVGhlIHZhbHVlIHRvIHRlc3RcbiAqXG4gKiBAcmV0dXJucyB7Ym9vbGVhbn0gVHJ1ZSBpZiB2YWx1ZSBpcyBhbiBGb3JtRGF0YSwgb3RoZXJ3aXNlIGZhbHNlXG4gKi9cbmNvbnN0IGlzRm9ybURhdGEgPSAodGhpbmcpID0+IHtcbiAgbGV0IGtpbmQ7XG4gIHJldHVybiB0aGluZyAmJiAoXG4gICAgKHR5cGVvZiBGb3JtRGF0YSA9PT0gJ2Z1bmN0aW9uJyAmJiB0aGluZyBpbnN0YW5jZW9mIEZvcm1EYXRhKSB8fCAoXG4gICAgICBpc0Z1bmN0aW9uKHRoaW5nLmFwcGVuZCkgJiYgKFxuICAgICAgICAoa2luZCA9IGtpbmRPZih0aGluZykpID09PSAnZm9ybWRhdGEnIHx8XG4gICAgICAgIC8vIGRldGVjdCBmb3JtLWRhdGEgaW5zdGFuY2VcbiAgICAgICAgKGtpbmQgPT09ICdvYmplY3QnICYmIGlzRnVuY3Rpb24odGhpbmcudG9TdHJpbmcpICYmIHRoaW5nLnRvU3RyaW5nKCkgPT09ICdbb2JqZWN0IEZvcm1EYXRhXScpXG4gICAgICApXG4gICAgKVxuICApXG59O1xuXG4vKipcbiAqIERldGVybWluZSBpZiBhIHZhbHVlIGlzIGEgVVJMU2VhcmNoUGFyYW1zIG9iamVjdFxuICpcbiAqIEBwYXJhbSB7Kn0gdmFsIFRoZSB2YWx1ZSB0byB0ZXN0XG4gKlxuICogQHJldHVybnMge2Jvb2xlYW59IFRydWUgaWYgdmFsdWUgaXMgYSBVUkxTZWFyY2hQYXJhbXMgb2JqZWN0LCBvdGhlcndpc2UgZmFsc2VcbiAqL1xuY29uc3QgaXNVUkxTZWFyY2hQYXJhbXMgPSBraW5kT2ZUZXN0KCdVUkxTZWFyY2hQYXJhbXMnKTtcblxuLyoqXG4gKiBUcmltIGV4Y2VzcyB3aGl0ZXNwYWNlIG9mZiB0aGUgYmVnaW5uaW5nIGFuZCBlbmQgb2YgYSBzdHJpbmdcbiAqXG4gKiBAcGFyYW0ge1N0cmluZ30gc3RyIFRoZSBTdHJpbmcgdG8gdHJpbVxuICpcbiAqIEByZXR1cm5zIHtTdHJpbmd9IFRoZSBTdHJpbmcgZnJlZWQgb2YgZXhjZXNzIHdoaXRlc3BhY2VcbiAqL1xuY29uc3QgdHJpbSA9IChzdHIpID0+IHN0ci50cmltID9cbiAgc3RyLnRyaW0oKSA6IHN0ci5yZXBsYWNlKC9eW1xcc1xcdUZFRkZcXHhBMF0rfFtcXHNcXHVGRUZGXFx4QTBdKyQvZywgJycpO1xuXG4vKipcbiAqIEl0ZXJhdGUgb3ZlciBhbiBBcnJheSBvciBhbiBPYmplY3QgaW52b2tpbmcgYSBmdW5jdGlvbiBmb3IgZWFjaCBpdGVtLlxuICpcbiAqIElmIGBvYmpgIGlzIGFuIEFycmF5IGNhbGxiYWNrIHdpbGwgYmUgY2FsbGVkIHBhc3NpbmdcbiAqIHRoZSB2YWx1ZSwgaW5kZXgsIGFuZCBjb21wbGV0ZSBhcnJheSBmb3IgZWFjaCBpdGVtLlxuICpcbiAqIElmICdvYmonIGlzIGFuIE9iamVjdCBjYWxsYmFjayB3aWxsIGJlIGNhbGxlZCBwYXNzaW5nXG4gKiB0aGUgdmFsdWUsIGtleSwgYW5kIGNvbXBsZXRlIG9iamVjdCBmb3IgZWFjaCBwcm9wZXJ0eS5cbiAqXG4gKiBAcGFyYW0ge09iamVjdHxBcnJheX0gb2JqIFRoZSBvYmplY3QgdG8gaXRlcmF0ZVxuICogQHBhcmFtIHtGdW5jdGlvbn0gZm4gVGhlIGNhbGxiYWNrIHRvIGludm9rZSBmb3IgZWFjaCBpdGVtXG4gKlxuICogQHBhcmFtIHtCb29sZWFufSBbYWxsT3duS2V5cyA9IGZhbHNlXVxuICogQHJldHVybnMge2FueX1cbiAqL1xuZnVuY3Rpb24gZm9yRWFjaChvYmosIGZuLCB7YWxsT3duS2V5cyA9IGZhbHNlfSA9IHt9KSB7XG4gIC8vIERvbid0IGJvdGhlciBpZiBubyB2YWx1ZSBwcm92aWRlZFxuICBpZiAob2JqID09PSBudWxsIHx8IHR5cGVvZiBvYmogPT09ICd1bmRlZmluZWQnKSB7XG4gICAgcmV0dXJuO1xuICB9XG5cbiAgbGV0IGk7XG4gIGxldCBsO1xuXG4gIC8vIEZvcmNlIGFuIGFycmF5IGlmIG5vdCBhbHJlYWR5IHNvbWV0aGluZyBpdGVyYWJsZVxuICBpZiAodHlwZW9mIG9iaiAhPT0gJ29iamVjdCcpIHtcbiAgICAvKmVzbGludCBuby1wYXJhbS1yZWFzc2lnbjowKi9cbiAgICBvYmogPSBbb2JqXTtcbiAgfVxuXG4gIGlmIChpc0FycmF5KG9iaikpIHtcbiAgICAvLyBJdGVyYXRlIG92ZXIgYXJyYXkgdmFsdWVzXG4gICAgZm9yIChpID0gMCwgbCA9IG9iai5sZW5ndGg7IGkgPCBsOyBpKyspIHtcbiAgICAgIGZuLmNhbGwobnVsbCwgb2JqW2ldLCBpLCBvYmopO1xuICAgIH1cbiAgfSBlbHNlIHtcbiAgICAvLyBJdGVyYXRlIG92ZXIgb2JqZWN0IGtleXNcbiAgICBjb25zdCBrZXlzID0gYWxsT3duS2V5cyA/IE9iamVjdC5nZXRPd25Qcm9wZXJ0eU5hbWVzKG9iaikgOiBPYmplY3Qua2V5cyhvYmopO1xuICAgIGNvbnN0IGxlbiA9IGtleXMubGVuZ3RoO1xuICAgIGxldCBrZXk7XG5cbiAgICBmb3IgKGkgPSAwOyBpIDwgbGVuOyBpKyspIHtcbiAgICAgIGtleSA9IGtleXNbaV07XG4gICAgICBmbi5jYWxsKG51bGwsIG9ialtrZXldLCBrZXksIG9iaik7XG4gICAgfVxuICB9XG59XG5cbmZ1bmN0aW9uIGZpbmRLZXkob2JqLCBrZXkpIHtcbiAga2V5ID0ga2V5LnRvTG93ZXJDYXNlKCk7XG4gIGNvbnN0IGtleXMgPSBPYmplY3Qua2V5cyhvYmopO1xuICBsZXQgaSA9IGtleXMubGVuZ3RoO1xuICBsZXQgX2tleTtcbiAgd2hpbGUgKGktLSA+IDApIHtcbiAgICBfa2V5ID0ga2V5c1tpXTtcbiAgICBpZiAoa2V5ID09PSBfa2V5LnRvTG93ZXJDYXNlKCkpIHtcbiAgICAgIHJldHVybiBfa2V5O1xuICAgIH1cbiAgfVxuICByZXR1cm4gbnVsbDtcbn1cblxuY29uc3QgX2dsb2JhbCA9ICgoKSA9PiB7XG4gIC8qZXNsaW50IG5vLXVuZGVmOjAqL1xuICBpZiAodHlwZW9mIGdsb2JhbFRoaXMgIT09IFwidW5kZWZpbmVkXCIpIHJldHVybiBnbG9iYWxUaGlzO1xuICByZXR1cm4gdHlwZW9mIHNlbGYgIT09IFwidW5kZWZpbmVkXCIgPyBzZWxmIDogKHR5cGVvZiB3aW5kb3cgIT09ICd1bmRlZmluZWQnID8gd2luZG93IDogZ2xvYmFsKVxufSkoKTtcblxuY29uc3QgaXNDb250ZXh0RGVmaW5lZCA9IChjb250ZXh0KSA9PiAhaXNVbmRlZmluZWQoY29udGV4dCkgJiYgY29udGV4dCAhPT0gX2dsb2JhbDtcblxuLyoqXG4gKiBBY2NlcHRzIHZhcmFyZ3MgZXhwZWN0aW5nIGVhY2ggYXJndW1lbnQgdG8gYmUgYW4gb2JqZWN0LCB0aGVuXG4gKiBpbW11dGFibHkgbWVyZ2VzIHRoZSBwcm9wZXJ0aWVzIG9mIGVhY2ggb2JqZWN0IGFuZCByZXR1cm5zIHJlc3VsdC5cbiAqXG4gKiBXaGVuIG11bHRpcGxlIG9iamVjdHMgY29udGFpbiB0aGUgc2FtZSBrZXkgdGhlIGxhdGVyIG9iamVjdCBpblxuICogdGhlIGFyZ3VtZW50cyBsaXN0IHdpbGwgdGFrZSBwcmVjZWRlbmNlLlxuICpcbiAqIEV4YW1wbGU6XG4gKlxuICogYGBganNcbiAqIHZhciByZXN1bHQgPSBtZXJnZSh7Zm9vOiAxMjN9LCB7Zm9vOiA0NTZ9KTtcbiAqIGNvbnNvbGUubG9nKHJlc3VsdC5mb28pOyAvLyBvdXRwdXRzIDQ1NlxuICogYGBgXG4gKlxuICogQHBhcmFtIHtPYmplY3R9IG9iajEgT2JqZWN0IHRvIG1lcmdlXG4gKlxuICogQHJldHVybnMge09iamVjdH0gUmVzdWx0IG9mIGFsbCBtZXJnZSBwcm9wZXJ0aWVzXG4gKi9cbmZ1bmN0aW9uIG1lcmdlKC8qIG9iajEsIG9iajIsIG9iajMsIC4uLiAqLykge1xuICBjb25zdCB7Y2FzZWxlc3N9ID0gaXNDb250ZXh0RGVmaW5lZCh0aGlzKSAmJiB0aGlzIHx8IHt9O1xuICBjb25zdCByZXN1bHQgPSB7fTtcbiAgY29uc3QgYXNzaWduVmFsdWUgPSAodmFsLCBrZXkpID0+IHtcbiAgICBjb25zdCB0YXJnZXRLZXkgPSBjYXNlbGVzcyAmJiBmaW5kS2V5KHJlc3VsdCwga2V5KSB8fCBrZXk7XG4gICAgaWYgKGlzUGxhaW5PYmplY3QocmVzdWx0W3RhcmdldEtleV0pICYmIGlzUGxhaW5PYmplY3QodmFsKSkge1xuICAgICAgcmVzdWx0W3RhcmdldEtleV0gPSBtZXJnZShyZXN1bHRbdGFyZ2V0S2V5XSwgdmFsKTtcbiAgICB9IGVsc2UgaWYgKGlzUGxhaW5PYmplY3QodmFsKSkge1xuICAgICAgcmVzdWx0W3RhcmdldEtleV0gPSBtZXJnZSh7fSwgdmFsKTtcbiAgICB9IGVsc2UgaWYgKGlzQXJyYXkodmFsKSkge1xuICAgICAgcmVzdWx0W3RhcmdldEtleV0gPSB2YWwuc2xpY2UoKTtcbiAgICB9IGVsc2Uge1xuICAgICAgcmVzdWx0W3RhcmdldEtleV0gPSB2YWw7XG4gICAgfVxuICB9O1xuXG4gIGZvciAobGV0IGkgPSAwLCBsID0gYXJndW1lbnRzLmxlbmd0aDsgaSA8IGw7IGkrKykge1xuICAgIGFyZ3VtZW50c1tpXSAmJiBmb3JFYWNoKGFyZ3VtZW50c1tpXSwgYXNzaWduVmFsdWUpO1xuICB9XG4gIHJldHVybiByZXN1bHQ7XG59XG5cbi8qKlxuICogRXh0ZW5kcyBvYmplY3QgYSBieSBtdXRhYmx5IGFkZGluZyB0byBpdCB0aGUgcHJvcGVydGllcyBvZiBvYmplY3QgYi5cbiAqXG4gKiBAcGFyYW0ge09iamVjdH0gYSBUaGUgb2JqZWN0IHRvIGJlIGV4dGVuZGVkXG4gKiBAcGFyYW0ge09iamVjdH0gYiBUaGUgb2JqZWN0IHRvIGNvcHkgcHJvcGVydGllcyBmcm9tXG4gKiBAcGFyYW0ge09iamVjdH0gdGhpc0FyZyBUaGUgb2JqZWN0IHRvIGJpbmQgZnVuY3Rpb24gdG9cbiAqXG4gKiBAcGFyYW0ge0Jvb2xlYW59IFthbGxPd25LZXlzXVxuICogQHJldHVybnMge09iamVjdH0gVGhlIHJlc3VsdGluZyB2YWx1ZSBvZiBvYmplY3QgYVxuICovXG5jb25zdCBleHRlbmQgPSAoYSwgYiwgdGhpc0FyZywge2FsbE93bktleXN9PSB7fSkgPT4ge1xuICBmb3JFYWNoKGIsICh2YWwsIGtleSkgPT4ge1xuICAgIGlmICh0aGlzQXJnICYmIGlzRnVuY3Rpb24odmFsKSkge1xuICAgICAgYVtrZXldID0gYmluZCh2YWwsIHRoaXNBcmcpO1xuICAgIH0gZWxzZSB7XG4gICAgICBhW2tleV0gPSB2YWw7XG4gICAgfVxuICB9LCB7YWxsT3duS2V5c30pO1xuICByZXR1cm4gYTtcbn07XG5cbi8qKlxuICogUmVtb3ZlIGJ5dGUgb3JkZXIgbWFya2VyLiBUaGlzIGNhdGNoZXMgRUYgQkIgQkYgKHRoZSBVVEYtOCBCT00pXG4gKlxuICogQHBhcmFtIHtzdHJpbmd9IGNvbnRlbnQgd2l0aCBCT01cbiAqXG4gKiBAcmV0dXJucyB7c3RyaW5nfSBjb250ZW50IHZhbHVlIHdpdGhvdXQgQk9NXG4gKi9cbmNvbnN0IHN0cmlwQk9NID0gKGNvbnRlbnQpID0+IHtcbiAgaWYgKGNvbnRlbnQuY2hhckNvZGVBdCgwKSA9PT0gMHhGRUZGKSB7XG4gICAgY29udGVudCA9IGNvbnRlbnQuc2xpY2UoMSk7XG4gIH1cbiAgcmV0dXJuIGNvbnRlbnQ7XG59O1xuXG4vKipcbiAqIEluaGVyaXQgdGhlIHByb3RvdHlwZSBtZXRob2RzIGZyb20gb25lIGNvbnN0cnVjdG9yIGludG8gYW5vdGhlclxuICogQHBhcmFtIHtmdW5jdGlvbn0gY29uc3RydWN0b3JcbiAqIEBwYXJhbSB7ZnVuY3Rpb259IHN1cGVyQ29uc3RydWN0b3JcbiAqIEBwYXJhbSB7b2JqZWN0fSBbcHJvcHNdXG4gKiBAcGFyYW0ge29iamVjdH0gW2Rlc2NyaXB0b3JzXVxuICpcbiAqIEByZXR1cm5zIHt2b2lkfVxuICovXG5jb25zdCBpbmhlcml0cyA9IChjb25zdHJ1Y3Rvciwgc3VwZXJDb25zdHJ1Y3RvciwgcHJvcHMsIGRlc2NyaXB0b3JzKSA9PiB7XG4gIGNvbnN0cnVjdG9yLnByb3RvdHlwZSA9IE9iamVjdC5jcmVhdGUoc3VwZXJDb25zdHJ1Y3Rvci5wcm90b3R5cGUsIGRlc2NyaXB0b3JzKTtcbiAgY29uc3RydWN0b3IucHJvdG90eXBlLmNvbnN0cnVjdG9yID0gY29uc3RydWN0b3I7XG4gIE9iamVjdC5kZWZpbmVQcm9wZXJ0eShjb25zdHJ1Y3RvciwgJ3N1cGVyJywge1xuICAgIHZhbHVlOiBzdXBlckNvbnN0cnVjdG9yLnByb3RvdHlwZVxuICB9KTtcbiAgcHJvcHMgJiYgT2JqZWN0LmFzc2lnbihjb25zdHJ1Y3Rvci5wcm90b3R5cGUsIHByb3BzKTtcbn07XG5cbi8qKlxuICogUmVzb2x2ZSBvYmplY3Qgd2l0aCBkZWVwIHByb3RvdHlwZSBjaGFpbiB0byBhIGZsYXQgb2JqZWN0XG4gKiBAcGFyYW0ge09iamVjdH0gc291cmNlT2JqIHNvdXJjZSBvYmplY3RcbiAqIEBwYXJhbSB7T2JqZWN0fSBbZGVzdE9ial1cbiAqIEBwYXJhbSB7RnVuY3Rpb258Qm9vbGVhbn0gW2ZpbHRlcl1cbiAqIEBwYXJhbSB7RnVuY3Rpb259IFtwcm9wRmlsdGVyXVxuICpcbiAqIEByZXR1cm5zIHtPYmplY3R9XG4gKi9cbmNvbnN0IHRvRmxhdE9iamVjdCA9IChzb3VyY2VPYmosIGRlc3RPYmosIGZpbHRlciwgcHJvcEZpbHRlcikgPT4ge1xuICBsZXQgcHJvcHM7XG4gIGxldCBpO1xuICBsZXQgcHJvcDtcbiAgY29uc3QgbWVyZ2VkID0ge307XG5cbiAgZGVzdE9iaiA9IGRlc3RPYmogfHwge307XG4gIC8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBuby1lcS1udWxsLGVxZXFlcVxuICBpZiAoc291cmNlT2JqID09IG51bGwpIHJldHVybiBkZXN0T2JqO1xuXG4gIGRvIHtcbiAgICBwcm9wcyA9IE9iamVjdC5nZXRPd25Qcm9wZXJ0eU5hbWVzKHNvdXJjZU9iaik7XG4gICAgaSA9IHByb3BzLmxlbmd0aDtcbiAgICB3aGlsZSAoaS0tID4gMCkge1xuICAgICAgcHJvcCA9IHByb3BzW2ldO1xuICAgICAgaWYgKCghcHJvcEZpbHRlciB8fCBwcm9wRmlsdGVyKHByb3AsIHNvdXJjZU9iaiwgZGVzdE9iaikpICYmICFtZXJnZWRbcHJvcF0pIHtcbiAgICAgICAgZGVzdE9ialtwcm9wXSA9IHNvdXJjZU9ialtwcm9wXTtcbiAgICAgICAgbWVyZ2VkW3Byb3BdID0gdHJ1ZTtcbiAgICAgIH1cbiAgICB9XG4gICAgc291cmNlT2JqID0gZmlsdGVyICE9PSBmYWxzZSAmJiBnZXRQcm90b3R5cGVPZihzb3VyY2VPYmopO1xuICB9IHdoaWxlIChzb3VyY2VPYmogJiYgKCFmaWx0ZXIgfHwgZmlsdGVyKHNvdXJjZU9iaiwgZGVzdE9iaikpICYmIHNvdXJjZU9iaiAhPT0gT2JqZWN0LnByb3RvdHlwZSk7XG5cbiAgcmV0dXJuIGRlc3RPYmo7XG59O1xuXG4vKipcbiAqIERldGVybWluZXMgd2hldGhlciBhIHN0cmluZyBlbmRzIHdpdGggdGhlIGNoYXJhY3RlcnMgb2YgYSBzcGVjaWZpZWQgc3RyaW5nXG4gKlxuICogQHBhcmFtIHtTdHJpbmd9IHN0clxuICogQHBhcmFtIHtTdHJpbmd9IHNlYXJjaFN0cmluZ1xuICogQHBhcmFtIHtOdW1iZXJ9IFtwb3NpdGlvbj0gMF1cbiAqXG4gKiBAcmV0dXJucyB7Ym9vbGVhbn1cbiAqL1xuY29uc3QgZW5kc1dpdGggPSAoc3RyLCBzZWFyY2hTdHJpbmcsIHBvc2l0aW9uKSA9PiB7XG4gIHN0ciA9IFN0cmluZyhzdHIpO1xuICBpZiAocG9zaXRpb24gPT09IHVuZGVmaW5lZCB8fCBwb3NpdGlvbiA+IHN0ci5sZW5ndGgpIHtcbiAgICBwb3NpdGlvbiA9IHN0ci5sZW5ndGg7XG4gIH1cbiAgcG9zaXRpb24gLT0gc2VhcmNoU3RyaW5nLmxlbmd0aDtcbiAgY29uc3QgbGFzdEluZGV4ID0gc3RyLmluZGV4T2Yoc2VhcmNoU3RyaW5nLCBwb3NpdGlvbik7XG4gIHJldHVybiBsYXN0SW5kZXggIT09IC0xICYmIGxhc3RJbmRleCA9PT0gcG9zaXRpb247XG59O1xuXG5cbi8qKlxuICogUmV0dXJucyBuZXcgYXJyYXkgZnJvbSBhcnJheSBsaWtlIG9iamVjdCBvciBudWxsIGlmIGZhaWxlZFxuICpcbiAqIEBwYXJhbSB7Kn0gW3RoaW5nXVxuICpcbiAqIEByZXR1cm5zIHs/QXJyYXl9XG4gKi9cbmNvbnN0IHRvQXJyYXkgPSAodGhpbmcpID0+IHtcbiAgaWYgKCF0aGluZykgcmV0dXJuIG51bGw7XG4gIGlmIChpc0FycmF5KHRoaW5nKSkgcmV0dXJuIHRoaW5nO1xuICBsZXQgaSA9IHRoaW5nLmxlbmd0aDtcbiAgaWYgKCFpc051bWJlcihpKSkgcmV0dXJuIG51bGw7XG4gIGNvbnN0IGFyciA9IG5ldyBBcnJheShpKTtcbiAgd2hpbGUgKGktLSA+IDApIHtcbiAgICBhcnJbaV0gPSB0aGluZ1tpXTtcbiAgfVxuICByZXR1cm4gYXJyO1xufTtcblxuLyoqXG4gKiBDaGVja2luZyBpZiB0aGUgVWludDhBcnJheSBleGlzdHMgYW5kIGlmIGl0IGRvZXMsIGl0IHJldHVybnMgYSBmdW5jdGlvbiB0aGF0IGNoZWNrcyBpZiB0aGVcbiAqIHRoaW5nIHBhc3NlZCBpbiBpcyBhbiBpbnN0YW5jZSBvZiBVaW50OEFycmF5XG4gKlxuICogQHBhcmFtIHtUeXBlZEFycmF5fVxuICpcbiAqIEByZXR1cm5zIHtBcnJheX1cbiAqL1xuLy8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIGZ1bmMtbmFtZXNcbmNvbnN0IGlzVHlwZWRBcnJheSA9IChUeXBlZEFycmF5ID0+IHtcbiAgLy8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIGZ1bmMtbmFtZXNcbiAgcmV0dXJuIHRoaW5nID0+IHtcbiAgICByZXR1cm4gVHlwZWRBcnJheSAmJiB0aGluZyBpbnN0YW5jZW9mIFR5cGVkQXJyYXk7XG4gIH07XG59KSh0eXBlb2YgVWludDhBcnJheSAhPT0gJ3VuZGVmaW5lZCcgJiYgZ2V0UHJvdG90eXBlT2YoVWludDhBcnJheSkpO1xuXG4vKipcbiAqIEZvciBlYWNoIGVudHJ5IGluIHRoZSBvYmplY3QsIGNhbGwgdGhlIGZ1bmN0aW9uIHdpdGggdGhlIGtleSBhbmQgdmFsdWUuXG4gKlxuICogQHBhcmFtIHtPYmplY3Q8YW55LCBhbnk+fSBvYmogLSBUaGUgb2JqZWN0IHRvIGl0ZXJhdGUgb3Zlci5cbiAqIEBwYXJhbSB7RnVuY3Rpb259IGZuIC0gVGhlIGZ1bmN0aW9uIHRvIGNhbGwgZm9yIGVhY2ggZW50cnkuXG4gKlxuICogQHJldHVybnMge3ZvaWR9XG4gKi9cbmNvbnN0IGZvckVhY2hFbnRyeSA9IChvYmosIGZuKSA9PiB7XG4gIGNvbnN0IGdlbmVyYXRvciA9IG9iaiAmJiBvYmpbU3ltYm9sLml0ZXJhdG9yXTtcblxuICBjb25zdCBpdGVyYXRvciA9IGdlbmVyYXRvci5jYWxsKG9iaik7XG5cbiAgbGV0IHJlc3VsdDtcblxuICB3aGlsZSAoKHJlc3VsdCA9IGl0ZXJhdG9yLm5leHQoKSkgJiYgIXJlc3VsdC5kb25lKSB7XG4gICAgY29uc3QgcGFpciA9IHJlc3VsdC52YWx1ZTtcbiAgICBmbi5jYWxsKG9iaiwgcGFpclswXSwgcGFpclsxXSk7XG4gIH1cbn07XG5cbi8qKlxuICogSXQgdGFrZXMgYSByZWd1bGFyIGV4cHJlc3Npb24gYW5kIGEgc3RyaW5nLCBhbmQgcmV0dXJucyBhbiBhcnJheSBvZiBhbGwgdGhlIG1hdGNoZXNcbiAqXG4gKiBAcGFyYW0ge3N0cmluZ30gcmVnRXhwIC0gVGhlIHJlZ3VsYXIgZXhwcmVzc2lvbiB0byBtYXRjaCBhZ2FpbnN0LlxuICogQHBhcmFtIHtzdHJpbmd9IHN0ciAtIFRoZSBzdHJpbmcgdG8gc2VhcmNoLlxuICpcbiAqIEByZXR1cm5zIHtBcnJheTxib29sZWFuPn1cbiAqL1xuY29uc3QgbWF0Y2hBbGwgPSAocmVnRXhwLCBzdHIpID0+IHtcbiAgbGV0IG1hdGNoZXM7XG4gIGNvbnN0IGFyciA9IFtdO1xuXG4gIHdoaWxlICgobWF0Y2hlcyA9IHJlZ0V4cC5leGVjKHN0cikpICE9PSBudWxsKSB7XG4gICAgYXJyLnB1c2gobWF0Y2hlcyk7XG4gIH1cblxuICByZXR1cm4gYXJyO1xufTtcblxuLyogQ2hlY2tpbmcgaWYgdGhlIGtpbmRPZlRlc3QgZnVuY3Rpb24gcmV0dXJucyB0cnVlIHdoZW4gcGFzc2VkIGFuIEhUTUxGb3JtRWxlbWVudC4gKi9cbmNvbnN0IGlzSFRNTEZvcm0gPSBraW5kT2ZUZXN0KCdIVE1MRm9ybUVsZW1lbnQnKTtcblxuY29uc3QgdG9DYW1lbENhc2UgPSBzdHIgPT4ge1xuICByZXR1cm4gc3RyLnRvTG93ZXJDYXNlKCkucmVwbGFjZSgvWy1fXFxzXShbYS16XFxkXSkoXFx3KikvZyxcbiAgICBmdW5jdGlvbiByZXBsYWNlcihtLCBwMSwgcDIpIHtcbiAgICAgIHJldHVybiBwMS50b1VwcGVyQ2FzZSgpICsgcDI7XG4gICAgfVxuICApO1xufTtcblxuLyogQ3JlYXRpbmcgYSBmdW5jdGlvbiB0aGF0IHdpbGwgY2hlY2sgaWYgYW4gb2JqZWN0IGhhcyBhIHByb3BlcnR5LiAqL1xuY29uc3QgaGFzT3duUHJvcGVydHkgPSAoKHtoYXNPd25Qcm9wZXJ0eX0pID0+IChvYmosIHByb3ApID0+IGhhc093blByb3BlcnR5LmNhbGwob2JqLCBwcm9wKSkoT2JqZWN0LnByb3RvdHlwZSk7XG5cbi8qKlxuICogRGV0ZXJtaW5lIGlmIGEgdmFsdWUgaXMgYSBSZWdFeHAgb2JqZWN0XG4gKlxuICogQHBhcmFtIHsqfSB2YWwgVGhlIHZhbHVlIHRvIHRlc3RcbiAqXG4gKiBAcmV0dXJucyB7Ym9vbGVhbn0gVHJ1ZSBpZiB2YWx1ZSBpcyBhIFJlZ0V4cCBvYmplY3QsIG90aGVyd2lzZSBmYWxzZVxuICovXG5jb25zdCBpc1JlZ0V4cCA9IGtpbmRPZlRlc3QoJ1JlZ0V4cCcpO1xuXG5jb25zdCByZWR1Y2VEZXNjcmlwdG9ycyA9IChvYmosIHJlZHVjZXIpID0+IHtcbiAgY29uc3QgZGVzY3JpcHRvcnMgPSBPYmplY3QuZ2V0T3duUHJvcGVydHlEZXNjcmlwdG9ycyhvYmopO1xuICBjb25zdCByZWR1Y2VkRGVzY3JpcHRvcnMgPSB7fTtcblxuICBmb3JFYWNoKGRlc2NyaXB0b3JzLCAoZGVzY3JpcHRvciwgbmFtZSkgPT4ge1xuICAgIGxldCByZXQ7XG4gICAgaWYgKChyZXQgPSByZWR1Y2VyKGRlc2NyaXB0b3IsIG5hbWUsIG9iaikpICE9PSBmYWxzZSkge1xuICAgICAgcmVkdWNlZERlc2NyaXB0b3JzW25hbWVdID0gcmV0IHx8IGRlc2NyaXB0b3I7XG4gICAgfVxuICB9KTtcblxuICBPYmplY3QuZGVmaW5lUHJvcGVydGllcyhvYmosIHJlZHVjZWREZXNjcmlwdG9ycyk7XG59O1xuXG4vKipcbiAqIE1ha2VzIGFsbCBtZXRob2RzIHJlYWQtb25seVxuICogQHBhcmFtIHtPYmplY3R9IG9ialxuICovXG5cbmNvbnN0IGZyZWV6ZU1ldGhvZHMgPSAob2JqKSA9PiB7XG4gIHJlZHVjZURlc2NyaXB0b3JzKG9iaiwgKGRlc2NyaXB0b3IsIG5hbWUpID0+IHtcbiAgICAvLyBza2lwIHJlc3RyaWN0ZWQgcHJvcHMgaW4gc3RyaWN0IG1vZGVcbiAgICBpZiAoaXNGdW5jdGlvbihvYmopICYmIFsnYXJndW1lbnRzJywgJ2NhbGxlcicsICdjYWxsZWUnXS5pbmRleE9mKG5hbWUpICE9PSAtMSkge1xuICAgICAgcmV0dXJuIGZhbHNlO1xuICAgIH1cblxuICAgIGNvbnN0IHZhbHVlID0gb2JqW25hbWVdO1xuXG4gICAgaWYgKCFpc0Z1bmN0aW9uKHZhbHVlKSkgcmV0dXJuO1xuXG4gICAgZGVzY3JpcHRvci5lbnVtZXJhYmxlID0gZmFsc2U7XG5cbiAgICBpZiAoJ3dyaXRhYmxlJyBpbiBkZXNjcmlwdG9yKSB7XG4gICAgICBkZXNjcmlwdG9yLndyaXRhYmxlID0gZmFsc2U7XG4gICAgICByZXR1cm47XG4gICAgfVxuXG4gICAgaWYgKCFkZXNjcmlwdG9yLnNldCkge1xuICAgICAgZGVzY3JpcHRvci5zZXQgPSAoKSA9PiB7XG4gICAgICAgIHRocm93IEVycm9yKCdDYW4gbm90IHJld3JpdGUgcmVhZC1vbmx5IG1ldGhvZCBcXCcnICsgbmFtZSArICdcXCcnKTtcbiAgICAgIH07XG4gICAgfVxuICB9KTtcbn07XG5cbmNvbnN0IHRvT2JqZWN0U2V0ID0gKGFycmF5T3JTdHJpbmcsIGRlbGltaXRlcikgPT4ge1xuICBjb25zdCBvYmogPSB7fTtcblxuICBjb25zdCBkZWZpbmUgPSAoYXJyKSA9PiB7XG4gICAgYXJyLmZvckVhY2godmFsdWUgPT4ge1xuICAgICAgb2JqW3ZhbHVlXSA9IHRydWU7XG4gICAgfSk7XG4gIH07XG5cbiAgaXNBcnJheShhcnJheU9yU3RyaW5nKSA/IGRlZmluZShhcnJheU9yU3RyaW5nKSA6IGRlZmluZShTdHJpbmcoYXJyYXlPclN0cmluZykuc3BsaXQoZGVsaW1pdGVyKSk7XG5cbiAgcmV0dXJuIG9iajtcbn07XG5cbmNvbnN0IG5vb3AgPSAoKSA9PiB7fTtcblxuY29uc3QgdG9GaW5pdGVOdW1iZXIgPSAodmFsdWUsIGRlZmF1bHRWYWx1ZSkgPT4ge1xuICB2YWx1ZSA9ICt2YWx1ZTtcbiAgcmV0dXJuIE51bWJlci5pc0Zpbml0ZSh2YWx1ZSkgPyB2YWx1ZSA6IGRlZmF1bHRWYWx1ZTtcbn07XG5cbmNvbnN0IEFMUEhBID0gJ2FiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6JztcblxuY29uc3QgRElHSVQgPSAnMDEyMzQ1Njc4OSc7XG5cbmNvbnN0IEFMUEhBQkVUID0ge1xuICBESUdJVCxcbiAgQUxQSEEsXG4gIEFMUEhBX0RJR0lUOiBBTFBIQSArIEFMUEhBLnRvVXBwZXJDYXNlKCkgKyBESUdJVFxufTtcblxuY29uc3QgZ2VuZXJhdGVTdHJpbmcgPSAoc2l6ZSA9IDE2LCBhbHBoYWJldCA9IEFMUEhBQkVULkFMUEhBX0RJR0lUKSA9PiB7XG4gIGxldCBzdHIgPSAnJztcbiAgY29uc3Qge2xlbmd0aH0gPSBhbHBoYWJldDtcbiAgd2hpbGUgKHNpemUtLSkge1xuICAgIHN0ciArPSBhbHBoYWJldFtNYXRoLnJhbmRvbSgpICogbGVuZ3RofDBdO1xuICB9XG5cbiAgcmV0dXJuIHN0cjtcbn07XG5cbi8qKlxuICogSWYgdGhlIHRoaW5nIGlzIGEgRm9ybURhdGEgb2JqZWN0LCByZXR1cm4gdHJ1ZSwgb3RoZXJ3aXNlIHJldHVybiBmYWxzZS5cbiAqXG4gKiBAcGFyYW0ge3Vua25vd259IHRoaW5nIC0gVGhlIHRoaW5nIHRvIGNoZWNrLlxuICpcbiAqIEByZXR1cm5zIHtib29sZWFufVxuICovXG5mdW5jdGlvbiBpc1NwZWNDb21wbGlhbnRGb3JtKHRoaW5nKSB7XG4gIHJldHVybiAhISh0aGluZyAmJiBpc0Z1bmN0aW9uKHRoaW5nLmFwcGVuZCkgJiYgdGhpbmdbU3ltYm9sLnRvU3RyaW5nVGFnXSA9PT0gJ0Zvcm1EYXRhJyAmJiB0aGluZ1tTeW1ib2wuaXRlcmF0b3JdKTtcbn1cblxuY29uc3QgdG9KU09OT2JqZWN0ID0gKG9iaikgPT4ge1xuICBjb25zdCBzdGFjayA9IG5ldyBBcnJheSgxMCk7XG5cbiAgY29uc3QgdmlzaXQgPSAoc291cmNlLCBpKSA9PiB7XG5cbiAgICBpZiAoaXNPYmplY3Qoc291cmNlKSkge1xuICAgICAgaWYgKHN0YWNrLmluZGV4T2Yoc291cmNlKSA+PSAwKSB7XG4gICAgICAgIHJldHVybjtcbiAgICAgIH1cblxuICAgICAgaWYoISgndG9KU09OJyBpbiBzb3VyY2UpKSB7XG4gICAgICAgIHN0YWNrW2ldID0gc291cmNlO1xuICAgICAgICBjb25zdCB0YXJnZXQgPSBpc0FycmF5KHNvdXJjZSkgPyBbXSA6IHt9O1xuXG4gICAgICAgIGZvckVhY2goc291cmNlLCAodmFsdWUsIGtleSkgPT4ge1xuICAgICAgICAgIGNvbnN0IHJlZHVjZWRWYWx1ZSA9IHZpc2l0KHZhbHVlLCBpICsgMSk7XG4gICAgICAgICAgIWlzVW5kZWZpbmVkKHJlZHVjZWRWYWx1ZSkgJiYgKHRhcmdldFtrZXldID0gcmVkdWNlZFZhbHVlKTtcbiAgICAgICAgfSk7XG5cbiAgICAgICAgc3RhY2tbaV0gPSB1bmRlZmluZWQ7XG5cbiAgICAgICAgcmV0dXJuIHRhcmdldDtcbiAgICAgIH1cbiAgICB9XG5cbiAgICByZXR1cm4gc291cmNlO1xuICB9O1xuXG4gIHJldHVybiB2aXNpdChvYmosIDApO1xufTtcblxuY29uc3QgaXNBc3luY0ZuID0ga2luZE9mVGVzdCgnQXN5bmNGdW5jdGlvbicpO1xuXG5jb25zdCBpc1RoZW5hYmxlID0gKHRoaW5nKSA9PlxuICB0aGluZyAmJiAoaXNPYmplY3QodGhpbmcpIHx8IGlzRnVuY3Rpb24odGhpbmcpKSAmJiBpc0Z1bmN0aW9uKHRoaW5nLnRoZW4pICYmIGlzRnVuY3Rpb24odGhpbmcuY2F0Y2gpO1xuXG5jb25zdCB1dGlscyA9IHtcbiAgaXNBcnJheSxcbiAgaXNBcnJheUJ1ZmZlcixcbiAgaXNCdWZmZXIsXG4gIGlzRm9ybURhdGEsXG4gIGlzQXJyYXlCdWZmZXJWaWV3LFxuICBpc1N0cmluZyxcbiAgaXNOdW1iZXIsXG4gIGlzQm9vbGVhbixcbiAgaXNPYmplY3QsXG4gIGlzUGxhaW5PYmplY3QsXG4gIGlzVW5kZWZpbmVkLFxuICBpc0RhdGUsXG4gIGlzRmlsZSxcbiAgaXNCbG9iLFxuICBpc1JlZ0V4cCxcbiAgaXNGdW5jdGlvbixcbiAgaXNTdHJlYW0sXG4gIGlzVVJMU2VhcmNoUGFyYW1zLFxuICBpc1R5cGVkQXJyYXksXG4gIGlzRmlsZUxpc3QsXG4gIGZvckVhY2gsXG4gIG1lcmdlLFxuICBleHRlbmQsXG4gIHRyaW0sXG4gIHN0cmlwQk9NLFxuICBpbmhlcml0cyxcbiAgdG9GbGF0T2JqZWN0LFxuICBraW5kT2YsXG4gIGtpbmRPZlRlc3QsXG4gIGVuZHNXaXRoLFxuICB0b0FycmF5LFxuICBmb3JFYWNoRW50cnksXG4gIG1hdGNoQWxsLFxuICBpc0hUTUxGb3JtLFxuICBoYXNPd25Qcm9wZXJ0eSxcbiAgaGFzT3duUHJvcDogaGFzT3duUHJvcGVydHksIC8vIGFuIGFsaWFzIHRvIGF2b2lkIEVTTGludCBuby1wcm90b3R5cGUtYnVpbHRpbnMgZGV0ZWN0aW9uXG4gIHJlZHVjZURlc2NyaXB0b3JzLFxuICBmcmVlemVNZXRob2RzLFxuICB0b09iamVjdFNldCxcbiAgdG9DYW1lbENhc2UsXG4gIG5vb3AsXG4gIHRvRmluaXRlTnVtYmVyLFxuICBmaW5kS2V5LFxuICBnbG9iYWw6IF9nbG9iYWwsXG4gIGlzQ29udGV4dERlZmluZWQsXG4gIEFMUEhBQkVULFxuICBnZW5lcmF0ZVN0cmluZyxcbiAgaXNTcGVjQ29tcGxpYW50Rm9ybSxcbiAgdG9KU09OT2JqZWN0LFxuICBpc0FzeW5jRm4sXG4gIGlzVGhlbmFibGVcbn07XG5cbi8qKlxuICogQ3JlYXRlIGFuIEVycm9yIHdpdGggdGhlIHNwZWNpZmllZCBtZXNzYWdlLCBjb25maWcsIGVycm9yIGNvZGUsIHJlcXVlc3QgYW5kIHJlc3BvbnNlLlxuICpcbiAqIEBwYXJhbSB7c3RyaW5nfSBtZXNzYWdlIFRoZSBlcnJvciBtZXNzYWdlLlxuICogQHBhcmFtIHtzdHJpbmd9IFtjb2RlXSBUaGUgZXJyb3IgY29kZSAoZm9yIGV4YW1wbGUsICdFQ09OTkFCT1JURUQnKS5cbiAqIEBwYXJhbSB7T2JqZWN0fSBbY29uZmlnXSBUaGUgY29uZmlnLlxuICogQHBhcmFtIHtPYmplY3R9IFtyZXF1ZXN0XSBUaGUgcmVxdWVzdC5cbiAqIEBwYXJhbSB7T2JqZWN0fSBbcmVzcG9uc2VdIFRoZSByZXNwb25zZS5cbiAqXG4gKiBAcmV0dXJucyB7RXJyb3J9IFRoZSBjcmVhdGVkIGVycm9yLlxuICovXG5mdW5jdGlvbiBBeGlvc0Vycm9yKG1lc3NhZ2UsIGNvZGUsIGNvbmZpZywgcmVxdWVzdCwgcmVzcG9uc2UpIHtcbiAgRXJyb3IuY2FsbCh0aGlzKTtcblxuICBpZiAoRXJyb3IuY2FwdHVyZVN0YWNrVHJhY2UpIHtcbiAgICBFcnJvci5jYXB0dXJlU3RhY2tUcmFjZSh0aGlzLCB0aGlzLmNvbnN0cnVjdG9yKTtcbiAgfSBlbHNlIHtcbiAgICB0aGlzLnN0YWNrID0gKG5ldyBFcnJvcigpKS5zdGFjaztcbiAgfVxuXG4gIHRoaXMubWVzc2FnZSA9IG1lc3NhZ2U7XG4gIHRoaXMubmFtZSA9ICdBeGlvc0Vycm9yJztcbiAgY29kZSAmJiAodGhpcy5jb2RlID0gY29kZSk7XG4gIGNvbmZpZyAmJiAodGhpcy5jb25maWcgPSBjb25maWcpO1xuICByZXF1ZXN0ICYmICh0aGlzLnJlcXVlc3QgPSByZXF1ZXN0KTtcbiAgcmVzcG9uc2UgJiYgKHRoaXMucmVzcG9uc2UgPSByZXNwb25zZSk7XG59XG5cbnV0aWxzLmluaGVyaXRzKEF4aW9zRXJyb3IsIEVycm9yLCB7XG4gIHRvSlNPTjogZnVuY3Rpb24gdG9KU09OKCkge1xuICAgIHJldHVybiB7XG4gICAgICAvLyBTdGFuZGFyZFxuICAgICAgbWVzc2FnZTogdGhpcy5tZXNzYWdlLFxuICAgICAgbmFtZTogdGhpcy5uYW1lLFxuICAgICAgLy8gTWljcm9zb2Z0XG4gICAgICBkZXNjcmlwdGlvbjogdGhpcy5kZXNjcmlwdGlvbixcbiAgICAgIG51bWJlcjogdGhpcy5udW1iZXIsXG4gICAgICAvLyBNb3ppbGxhXG4gICAgICBmaWxlTmFtZTogdGhpcy5maWxlTmFtZSxcbiAgICAgIGxpbmVOdW1iZXI6IHRoaXMubGluZU51bWJlcixcbiAgICAgIGNvbHVtbk51bWJlcjogdGhpcy5jb2x1bW5OdW1iZXIsXG4gICAgICBzdGFjazogdGhpcy5zdGFjayxcbiAgICAgIC8vIEF4aW9zXG4gICAgICBjb25maWc6IHV0aWxzLnRvSlNPTk9iamVjdCh0aGlzLmNvbmZpZyksXG4gICAgICBjb2RlOiB0aGlzLmNvZGUsXG4gICAgICBzdGF0dXM6IHRoaXMucmVzcG9uc2UgJiYgdGhpcy5yZXNwb25zZS5zdGF0dXMgPyB0aGlzLnJlc3BvbnNlLnN0YXR1cyA6IG51bGxcbiAgICB9O1xuICB9XG59KTtcblxuY29uc3QgcHJvdG90eXBlJDEgPSBBeGlvc0Vycm9yLnByb3RvdHlwZTtcbmNvbnN0IGRlc2NyaXB0b3JzID0ge307XG5cbltcbiAgJ0VSUl9CQURfT1BUSU9OX1ZBTFVFJyxcbiAgJ0VSUl9CQURfT1BUSU9OJyxcbiAgJ0VDT05OQUJPUlRFRCcsXG4gICdFVElNRURPVVQnLFxuICAnRVJSX05FVFdPUksnLFxuICAnRVJSX0ZSX1RPT19NQU5ZX1JFRElSRUNUUycsXG4gICdFUlJfREVQUkVDQVRFRCcsXG4gICdFUlJfQkFEX1JFU1BPTlNFJyxcbiAgJ0VSUl9CQURfUkVRVUVTVCcsXG4gICdFUlJfQ0FOQ0VMRUQnLFxuICAnRVJSX05PVF9TVVBQT1JUJyxcbiAgJ0VSUl9JTlZBTElEX1VSTCdcbi8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBmdW5jLW5hbWVzXG5dLmZvckVhY2goY29kZSA9PiB7XG4gIGRlc2NyaXB0b3JzW2NvZGVdID0ge3ZhbHVlOiBjb2RlfTtcbn0pO1xuXG5PYmplY3QuZGVmaW5lUHJvcGVydGllcyhBeGlvc0Vycm9yLCBkZXNjcmlwdG9ycyk7XG5PYmplY3QuZGVmaW5lUHJvcGVydHkocHJvdG90eXBlJDEsICdpc0F4aW9zRXJyb3InLCB7dmFsdWU6IHRydWV9KTtcblxuLy8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIGZ1bmMtbmFtZXNcbkF4aW9zRXJyb3IuZnJvbSA9IChlcnJvciwgY29kZSwgY29uZmlnLCByZXF1ZXN0LCByZXNwb25zZSwgY3VzdG9tUHJvcHMpID0+IHtcbiAgY29uc3QgYXhpb3NFcnJvciA9IE9iamVjdC5jcmVhdGUocHJvdG90eXBlJDEpO1xuXG4gIHV0aWxzLnRvRmxhdE9iamVjdChlcnJvciwgYXhpb3NFcnJvciwgZnVuY3Rpb24gZmlsdGVyKG9iaikge1xuICAgIHJldHVybiBvYmogIT09IEVycm9yLnByb3RvdHlwZTtcbiAgfSwgcHJvcCA9PiB7XG4gICAgcmV0dXJuIHByb3AgIT09ICdpc0F4aW9zRXJyb3InO1xuICB9KTtcblxuICBBeGlvc0Vycm9yLmNhbGwoYXhpb3NFcnJvciwgZXJyb3IubWVzc2FnZSwgY29kZSwgY29uZmlnLCByZXF1ZXN0LCByZXNwb25zZSk7XG5cbiAgYXhpb3NFcnJvci5jYXVzZSA9IGVycm9yO1xuXG4gIGF4aW9zRXJyb3IubmFtZSA9IGVycm9yLm5hbWU7XG5cbiAgY3VzdG9tUHJvcHMgJiYgT2JqZWN0LmFzc2lnbihheGlvc0Vycm9yLCBjdXN0b21Qcm9wcyk7XG5cbiAgcmV0dXJuIGF4aW9zRXJyb3I7XG59O1xuXG4vKipcbiAqIERldGVybWluZXMgaWYgdGhlIGdpdmVuIHRoaW5nIGlzIGEgYXJyYXkgb3IganMgb2JqZWN0LlxuICpcbiAqIEBwYXJhbSB7c3RyaW5nfSB0aGluZyAtIFRoZSBvYmplY3Qgb3IgYXJyYXkgdG8gYmUgdmlzaXRlZC5cbiAqXG4gKiBAcmV0dXJucyB7Ym9vbGVhbn1cbiAqL1xuZnVuY3Rpb24gaXNWaXNpdGFibGUodGhpbmcpIHtcbiAgcmV0dXJuIHV0aWxzLmlzUGxhaW5PYmplY3QodGhpbmcpIHx8IHV0aWxzLmlzQXJyYXkodGhpbmcpO1xufVxuXG4vKipcbiAqIEl0IHJlbW92ZXMgdGhlIGJyYWNrZXRzIGZyb20gdGhlIGVuZCBvZiBhIHN0cmluZ1xuICpcbiAqIEBwYXJhbSB7c3RyaW5nfSBrZXkgLSBUaGUga2V5IG9mIHRoZSBwYXJhbWV0ZXIuXG4gKlxuICogQHJldHVybnMge3N0cmluZ30gdGhlIGtleSB3aXRob3V0IHRoZSBicmFja2V0cy5cbiAqL1xuZnVuY3Rpb24gcmVtb3ZlQnJhY2tldHMoa2V5KSB7XG4gIHJldHVybiB1dGlscy5lbmRzV2l0aChrZXksICdbXScpID8ga2V5LnNsaWNlKDAsIC0yKSA6IGtleTtcbn1cblxuLyoqXG4gKiBJdCB0YWtlcyBhIHBhdGgsIGEga2V5LCBhbmQgYSBib29sZWFuLCBhbmQgcmV0dXJucyBhIHN0cmluZ1xuICpcbiAqIEBwYXJhbSB7c3RyaW5nfSBwYXRoIC0gVGhlIHBhdGggdG8gdGhlIGN1cnJlbnQga2V5LlxuICogQHBhcmFtIHtzdHJpbmd9IGtleSAtIFRoZSBrZXkgb2YgdGhlIGN1cnJlbnQgb2JqZWN0IGJlaW5nIGl0ZXJhdGVkIG92ZXIuXG4gKiBAcGFyYW0ge3N0cmluZ30gZG90cyAtIElmIHRydWUsIHRoZSBrZXkgd2lsbCBiZSByZW5kZXJlZCB3aXRoIGRvdHMgaW5zdGVhZCBvZiBicmFja2V0cy5cbiAqXG4gKiBAcmV0dXJucyB7c3RyaW5nfSBUaGUgcGF0aCB0byB0aGUgY3VycmVudCBrZXkuXG4gKi9cbmZ1bmN0aW9uIHJlbmRlcktleShwYXRoLCBrZXksIGRvdHMpIHtcbiAgaWYgKCFwYXRoKSByZXR1cm4ga2V5O1xuICByZXR1cm4gcGF0aC5jb25jYXQoa2V5KS5tYXAoZnVuY3Rpb24gZWFjaCh0b2tlbiwgaSkge1xuICAgIC8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBuby1wYXJhbS1yZWFzc2lnblxuICAgIHRva2VuID0gcmVtb3ZlQnJhY2tldHModG9rZW4pO1xuICAgIHJldHVybiAhZG90cyAmJiBpID8gJ1snICsgdG9rZW4gKyAnXScgOiB0b2tlbjtcbiAgfSkuam9pbihkb3RzID8gJy4nIDogJycpO1xufVxuXG4vKipcbiAqIElmIHRoZSBhcnJheSBpcyBhbiBhcnJheSBhbmQgbm9uZSBvZiBpdHMgZWxlbWVudHMgYXJlIHZpc2l0YWJsZSwgdGhlbiBpdCdzIGEgZmxhdCBhcnJheS5cbiAqXG4gKiBAcGFyYW0ge0FycmF5PGFueT59IGFyciAtIFRoZSBhcnJheSB0byBjaGVja1xuICpcbiAqIEByZXR1cm5zIHtib29sZWFufVxuICovXG5mdW5jdGlvbiBpc0ZsYXRBcnJheShhcnIpIHtcbiAgcmV0dXJuIHV0aWxzLmlzQXJyYXkoYXJyKSAmJiAhYXJyLnNvbWUoaXNWaXNpdGFibGUpO1xufVxuXG5jb25zdCBwcmVkaWNhdGVzID0gdXRpbHMudG9GbGF0T2JqZWN0KHV0aWxzLCB7fSwgbnVsbCwgZnVuY3Rpb24gZmlsdGVyKHByb3ApIHtcbiAgcmV0dXJuIC9eaXNbQS1aXS8udGVzdChwcm9wKTtcbn0pO1xuXG4vKipcbiAqIENvbnZlcnQgYSBkYXRhIG9iamVjdCB0byBGb3JtRGF0YVxuICpcbiAqIEBwYXJhbSB7T2JqZWN0fSBvYmpcbiAqIEBwYXJhbSB7P09iamVjdH0gW2Zvcm1EYXRhXVxuICogQHBhcmFtIHs/T2JqZWN0fSBbb3B0aW9uc11cbiAqIEBwYXJhbSB7RnVuY3Rpb259IFtvcHRpb25zLnZpc2l0b3JdXG4gKiBAcGFyYW0ge0Jvb2xlYW59IFtvcHRpb25zLm1ldGFUb2tlbnMgPSB0cnVlXVxuICogQHBhcmFtIHtCb29sZWFufSBbb3B0aW9ucy5kb3RzID0gZmFsc2VdXG4gKiBAcGFyYW0gez9Cb29sZWFufSBbb3B0aW9ucy5pbmRleGVzID0gZmFsc2VdXG4gKlxuICogQHJldHVybnMge09iamVjdH1cbiAqKi9cblxuLyoqXG4gKiBJdCBjb252ZXJ0cyBhbiBvYmplY3QgaW50byBhIEZvcm1EYXRhIG9iamVjdFxuICpcbiAqIEBwYXJhbSB7T2JqZWN0PGFueSwgYW55Pn0gb2JqIC0gVGhlIG9iamVjdCB0byBjb252ZXJ0IHRvIGZvcm0gZGF0YS5cbiAqIEBwYXJhbSB7c3RyaW5nfSBmb3JtRGF0YSAtIFRoZSBGb3JtRGF0YSBvYmplY3QgdG8gYXBwZW5kIHRvLlxuICogQHBhcmFtIHtPYmplY3Q8c3RyaW5nLCBhbnk+fSBvcHRpb25zXG4gKlxuICogQHJldHVybnNcbiAqL1xuZnVuY3Rpb24gdG9Gb3JtRGF0YShvYmosIGZvcm1EYXRhLCBvcHRpb25zKSB7XG4gIGlmICghdXRpbHMuaXNPYmplY3Qob2JqKSkge1xuICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ3RhcmdldCBtdXN0IGJlIGFuIG9iamVjdCcpO1xuICB9XG5cbiAgLy8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIG5vLXBhcmFtLXJlYXNzaWduXG4gIGZvcm1EYXRhID0gZm9ybURhdGEgfHwgbmV3IChGb3JtRGF0YV9fZGVmYXVsdFtcImRlZmF1bHRcIl0gfHwgRm9ybURhdGEpKCk7XG5cbiAgLy8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIG5vLXBhcmFtLXJlYXNzaWduXG4gIG9wdGlvbnMgPSB1dGlscy50b0ZsYXRPYmplY3Qob3B0aW9ucywge1xuICAgIG1ldGFUb2tlbnM6IHRydWUsXG4gICAgZG90czogZmFsc2UsXG4gICAgaW5kZXhlczogZmFsc2VcbiAgfSwgZmFsc2UsIGZ1bmN0aW9uIGRlZmluZWQob3B0aW9uLCBzb3VyY2UpIHtcbiAgICAvLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgbm8tZXEtbnVsbCxlcWVxZXFcbiAgICByZXR1cm4gIXV0aWxzLmlzVW5kZWZpbmVkKHNvdXJjZVtvcHRpb25dKTtcbiAgfSk7XG5cbiAgY29uc3QgbWV0YVRva2VucyA9IG9wdGlvbnMubWV0YVRva2VucztcbiAgLy8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIG5vLXVzZS1iZWZvcmUtZGVmaW5lXG4gIGNvbnN0IHZpc2l0b3IgPSBvcHRpb25zLnZpc2l0b3IgfHwgZGVmYXVsdFZpc2l0b3I7XG4gIGNvbnN0IGRvdHMgPSBvcHRpb25zLmRvdHM7XG4gIGNvbnN0IGluZGV4ZXMgPSBvcHRpb25zLmluZGV4ZXM7XG4gIGNvbnN0IF9CbG9iID0gb3B0aW9ucy5CbG9iIHx8IHR5cGVvZiBCbG9iICE9PSAndW5kZWZpbmVkJyAmJiBCbG9iO1xuICBjb25zdCB1c2VCbG9iID0gX0Jsb2IgJiYgdXRpbHMuaXNTcGVjQ29tcGxpYW50Rm9ybShmb3JtRGF0YSk7XG5cbiAgaWYgKCF1dGlscy5pc0Z1bmN0aW9uKHZpc2l0b3IpKSB7XG4gICAgdGhyb3cgbmV3IFR5cGVFcnJvcigndmlzaXRvciBtdXN0IGJlIGEgZnVuY3Rpb24nKTtcbiAgfVxuXG4gIGZ1bmN0aW9uIGNvbnZlcnRWYWx1ZSh2YWx1ZSkge1xuICAgIGlmICh2YWx1ZSA9PT0gbnVsbCkgcmV0dXJuICcnO1xuXG4gICAgaWYgKHV0aWxzLmlzRGF0ZSh2YWx1ZSkpIHtcbiAgICAgIHJldHVybiB2YWx1ZS50b0lTT1N0cmluZygpO1xuICAgIH1cblxuICAgIGlmICghdXNlQmxvYiAmJiB1dGlscy5pc0Jsb2IodmFsdWUpKSB7XG4gICAgICB0aHJvdyBuZXcgQXhpb3NFcnJvcignQmxvYiBpcyBub3Qgc3VwcG9ydGVkLiBVc2UgYSBCdWZmZXIgaW5zdGVhZC4nKTtcbiAgICB9XG5cbiAgICBpZiAodXRpbHMuaXNBcnJheUJ1ZmZlcih2YWx1ZSkgfHwgdXRpbHMuaXNUeXBlZEFycmF5KHZhbHVlKSkge1xuICAgICAgcmV0dXJuIHVzZUJsb2IgJiYgdHlwZW9mIEJsb2IgPT09ICdmdW5jdGlvbicgPyBuZXcgQmxvYihbdmFsdWVdKSA6IEJ1ZmZlci5mcm9tKHZhbHVlKTtcbiAgICB9XG5cbiAgICByZXR1cm4gdmFsdWU7XG4gIH1cblxuICAvKipcbiAgICogRGVmYXVsdCB2aXNpdG9yLlxuICAgKlxuICAgKiBAcGFyYW0geyp9IHZhbHVlXG4gICAqIEBwYXJhbSB7U3RyaW5nfE51bWJlcn0ga2V5XG4gICAqIEBwYXJhbSB7QXJyYXk8U3RyaW5nfE51bWJlcj59IHBhdGhcbiAgICogQHRoaXMge0Zvcm1EYXRhfVxuICAgKlxuICAgKiBAcmV0dXJucyB7Ym9vbGVhbn0gcmV0dXJuIHRydWUgdG8gdmlzaXQgdGhlIGVhY2ggcHJvcCBvZiB0aGUgdmFsdWUgcmVjdXJzaXZlbHlcbiAgICovXG4gIGZ1bmN0aW9uIGRlZmF1bHRWaXNpdG9yKHZhbHVlLCBrZXksIHBhdGgpIHtcbiAgICBsZXQgYXJyID0gdmFsdWU7XG5cbiAgICBpZiAodmFsdWUgJiYgIXBhdGggJiYgdHlwZW9mIHZhbHVlID09PSAnb2JqZWN0Jykge1xuICAgICAgaWYgKHV0aWxzLmVuZHNXaXRoKGtleSwgJ3t9JykpIHtcbiAgICAgICAgLy8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIG5vLXBhcmFtLXJlYXNzaWduXG4gICAgICAgIGtleSA9IG1ldGFUb2tlbnMgPyBrZXkgOiBrZXkuc2xpY2UoMCwgLTIpO1xuICAgICAgICAvLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgbm8tcGFyYW0tcmVhc3NpZ25cbiAgICAgICAgdmFsdWUgPSBKU09OLnN0cmluZ2lmeSh2YWx1ZSk7XG4gICAgICB9IGVsc2UgaWYgKFxuICAgICAgICAodXRpbHMuaXNBcnJheSh2YWx1ZSkgJiYgaXNGbGF0QXJyYXkodmFsdWUpKSB8fFxuICAgICAgICAoKHV0aWxzLmlzRmlsZUxpc3QodmFsdWUpIHx8IHV0aWxzLmVuZHNXaXRoKGtleSwgJ1tdJykpICYmIChhcnIgPSB1dGlscy50b0FycmF5KHZhbHVlKSlcbiAgICAgICAgKSkge1xuICAgICAgICAvLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgbm8tcGFyYW0tcmVhc3NpZ25cbiAgICAgICAga2V5ID0gcmVtb3ZlQnJhY2tldHMoa2V5KTtcblxuICAgICAgICBhcnIuZm9yRWFjaChmdW5jdGlvbiBlYWNoKGVsLCBpbmRleCkge1xuICAgICAgICAgICEodXRpbHMuaXNVbmRlZmluZWQoZWwpIHx8IGVsID09PSBudWxsKSAmJiBmb3JtRGF0YS5hcHBlbmQoXG4gICAgICAgICAgICAvLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgbm8tbmVzdGVkLXRlcm5hcnlcbiAgICAgICAgICAgIGluZGV4ZXMgPT09IHRydWUgPyByZW5kZXJLZXkoW2tleV0sIGluZGV4LCBkb3RzKSA6IChpbmRleGVzID09PSBudWxsID8ga2V5IDoga2V5ICsgJ1tdJyksXG4gICAgICAgICAgICBjb252ZXJ0VmFsdWUoZWwpXG4gICAgICAgICAgKTtcbiAgICAgICAgfSk7XG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICAgIH1cbiAgICB9XG5cbiAgICBpZiAoaXNWaXNpdGFibGUodmFsdWUpKSB7XG4gICAgICByZXR1cm4gdHJ1ZTtcbiAgICB9XG5cbiAgICBmb3JtRGF0YS5hcHBlbmQocmVuZGVyS2V5KHBhdGgsIGtleSwgZG90cyksIGNvbnZlcnRWYWx1ZSh2YWx1ZSkpO1xuXG4gICAgcmV0dXJuIGZhbHNlO1xuICB9XG5cbiAgY29uc3Qgc3RhY2sgPSBbXTtcblxuICBjb25zdCBleHBvc2VkSGVscGVycyA9IE9iamVjdC5hc3NpZ24ocHJlZGljYXRlcywge1xuICAgIGRlZmF1bHRWaXNpdG9yLFxuICAgIGNvbnZlcnRWYWx1ZSxcbiAgICBpc1Zpc2l0YWJsZVxuICB9KTtcblxuICBmdW5jdGlvbiBidWlsZCh2YWx1ZSwgcGF0aCkge1xuICAgIGlmICh1dGlscy5pc1VuZGVmaW5lZCh2YWx1ZSkpIHJldHVybjtcblxuICAgIGlmIChzdGFjay5pbmRleE9mKHZhbHVlKSAhPT0gLTEpIHtcbiAgICAgIHRocm93IEVycm9yKCdDaXJjdWxhciByZWZlcmVuY2UgZGV0ZWN0ZWQgaW4gJyArIHBhdGguam9pbignLicpKTtcbiAgICB9XG5cbiAgICBzdGFjay5wdXNoKHZhbHVlKTtcblxuICAgIHV0aWxzLmZvckVhY2godmFsdWUsIGZ1bmN0aW9uIGVhY2goZWwsIGtleSkge1xuICAgICAgY29uc3QgcmVzdWx0ID0gISh1dGlscy5pc1VuZGVmaW5lZChlbCkgfHwgZWwgPT09IG51bGwpICYmIHZpc2l0b3IuY2FsbChcbiAgICAgICAgZm9ybURhdGEsIGVsLCB1dGlscy5pc1N0cmluZyhrZXkpID8ga2V5LnRyaW0oKSA6IGtleSwgcGF0aCwgZXhwb3NlZEhlbHBlcnNcbiAgICAgICk7XG5cbiAgICAgIGlmIChyZXN1bHQgPT09IHRydWUpIHtcbiAgICAgICAgYnVpbGQoZWwsIHBhdGggPyBwYXRoLmNvbmNhdChrZXkpIDogW2tleV0pO1xuICAgICAgfVxuICAgIH0pO1xuXG4gICAgc3RhY2sucG9wKCk7XG4gIH1cblxuICBpZiAoIXV0aWxzLmlzT2JqZWN0KG9iaikpIHtcbiAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdkYXRhIG11c3QgYmUgYW4gb2JqZWN0Jyk7XG4gIH1cblxuICBidWlsZChvYmopO1xuXG4gIHJldHVybiBmb3JtRGF0YTtcbn1cblxuLyoqXG4gKiBJdCBlbmNvZGVzIGEgc3RyaW5nIGJ5IHJlcGxhY2luZyBhbGwgY2hhcmFjdGVycyB0aGF0IGFyZSBub3QgaW4gdGhlIHVucmVzZXJ2ZWQgc2V0IHdpdGhcbiAqIHRoZWlyIHBlcmNlbnQtZW5jb2RlZCBlcXVpdmFsZW50c1xuICpcbiAqIEBwYXJhbSB7c3RyaW5nfSBzdHIgLSBUaGUgc3RyaW5nIHRvIGVuY29kZS5cbiAqXG4gKiBAcmV0dXJucyB7c3RyaW5nfSBUaGUgZW5jb2RlZCBzdHJpbmcuXG4gKi9cbmZ1bmN0aW9uIGVuY29kZSQxKHN0cikge1xuICBjb25zdCBjaGFyTWFwID0ge1xuICAgICchJzogJyUyMScsXG4gICAgXCInXCI6ICclMjcnLFxuICAgICcoJzogJyUyOCcsXG4gICAgJyknOiAnJTI5JyxcbiAgICAnfic6ICclN0UnLFxuICAgICclMjAnOiAnKycsXG4gICAgJyUwMCc6ICdcXHgwMCdcbiAgfTtcbiAgcmV0dXJuIGVuY29kZVVSSUNvbXBvbmVudChzdHIpLnJlcGxhY2UoL1shJygpfl18JTIwfCUwMC9nLCBmdW5jdGlvbiByZXBsYWNlcihtYXRjaCkge1xuICAgIHJldHVybiBjaGFyTWFwW21hdGNoXTtcbiAgfSk7XG59XG5cbi8qKlxuICogSXQgdGFrZXMgYSBwYXJhbXMgb2JqZWN0IGFuZCBjb252ZXJ0cyBpdCB0byBhIEZvcm1EYXRhIG9iamVjdFxuICpcbiAqIEBwYXJhbSB7T2JqZWN0PHN0cmluZywgYW55Pn0gcGFyYW1zIC0gVGhlIHBhcmFtZXRlcnMgdG8gYmUgY29udmVydGVkIHRvIGEgRm9ybURhdGEgb2JqZWN0LlxuICogQHBhcmFtIHtPYmplY3Q8c3RyaW5nLCBhbnk+fSBvcHRpb25zIC0gVGhlIG9wdGlvbnMgb2JqZWN0IHBhc3NlZCB0byB0aGUgQXhpb3MgY29uc3RydWN0b3IuXG4gKlxuICogQHJldHVybnMge3ZvaWR9XG4gKi9cbmZ1bmN0aW9uIEF4aW9zVVJMU2VhcmNoUGFyYW1zKHBhcmFtcywgb3B0aW9ucykge1xuICB0aGlzLl9wYWlycyA9IFtdO1xuXG4gIHBhcmFtcyAmJiB0b0Zvcm1EYXRhKHBhcmFtcywgdGhpcywgb3B0aW9ucyk7XG59XG5cbmNvbnN0IHByb3RvdHlwZSA9IEF4aW9zVVJMU2VhcmNoUGFyYW1zLnByb3RvdHlwZTtcblxucHJvdG90eXBlLmFwcGVuZCA9IGZ1bmN0aW9uIGFwcGVuZChuYW1lLCB2YWx1ZSkge1xuICB0aGlzLl9wYWlycy5wdXNoKFtuYW1lLCB2YWx1ZV0pO1xufTtcblxucHJvdG90eXBlLnRvU3RyaW5nID0gZnVuY3Rpb24gdG9TdHJpbmcoZW5jb2Rlcikge1xuICBjb25zdCBfZW5jb2RlID0gZW5jb2RlciA/IGZ1bmN0aW9uKHZhbHVlKSB7XG4gICAgcmV0dXJuIGVuY29kZXIuY2FsbCh0aGlzLCB2YWx1ZSwgZW5jb2RlJDEpO1xuICB9IDogZW5jb2RlJDE7XG5cbiAgcmV0dXJuIHRoaXMuX3BhaXJzLm1hcChmdW5jdGlvbiBlYWNoKHBhaXIpIHtcbiAgICByZXR1cm4gX2VuY29kZShwYWlyWzBdKSArICc9JyArIF9lbmNvZGUocGFpclsxXSk7XG4gIH0sICcnKS5qb2luKCcmJyk7XG59O1xuXG4vKipcbiAqIEl0IHJlcGxhY2VzIGFsbCBpbnN0YW5jZXMgb2YgdGhlIGNoYXJhY3RlcnMgYDpgLCBgJGAsIGAsYCwgYCtgLCBgW2AsIGFuZCBgXWAgd2l0aCB0aGVpclxuICogVVJJIGVuY29kZWQgY291bnRlcnBhcnRzXG4gKlxuICogQHBhcmFtIHtzdHJpbmd9IHZhbCBUaGUgdmFsdWUgdG8gYmUgZW5jb2RlZC5cbiAqXG4gKiBAcmV0dXJucyB7c3RyaW5nfSBUaGUgZW5jb2RlZCB2YWx1ZS5cbiAqL1xuZnVuY3Rpb24gZW5jb2RlKHZhbCkge1xuICByZXR1cm4gZW5jb2RlVVJJQ29tcG9uZW50KHZhbCkuXG4gICAgcmVwbGFjZSgvJTNBL2dpLCAnOicpLlxuICAgIHJlcGxhY2UoLyUyNC9nLCAnJCcpLlxuICAgIHJlcGxhY2UoLyUyQy9naSwgJywnKS5cbiAgICByZXBsYWNlKC8lMjAvZywgJysnKS5cbiAgICByZXBsYWNlKC8lNUIvZ2ksICdbJykuXG4gICAgcmVwbGFjZSgvJTVEL2dpLCAnXScpO1xufVxuXG4vKipcbiAqIEJ1aWxkIGEgVVJMIGJ5IGFwcGVuZGluZyBwYXJhbXMgdG8gdGhlIGVuZFxuICpcbiAqIEBwYXJhbSB7c3RyaW5nfSB1cmwgVGhlIGJhc2Ugb2YgdGhlIHVybCAoZS5nLiwgaHR0cDovL3d3dy5nb29nbGUuY29tKVxuICogQHBhcmFtIHtvYmplY3R9IFtwYXJhbXNdIFRoZSBwYXJhbXMgdG8gYmUgYXBwZW5kZWRcbiAqIEBwYXJhbSB7P29iamVjdH0gb3B0aW9uc1xuICpcbiAqIEByZXR1cm5zIHtzdHJpbmd9IFRoZSBmb3JtYXR0ZWQgdXJsXG4gKi9cbmZ1bmN0aW9uIGJ1aWxkVVJMKHVybCwgcGFyYW1zLCBvcHRpb25zKSB7XG4gIC8qZXNsaW50IG5vLXBhcmFtLXJlYXNzaWduOjAqL1xuICBpZiAoIXBhcmFtcykge1xuICAgIHJldHVybiB1cmw7XG4gIH1cbiAgXG4gIGNvbnN0IF9lbmNvZGUgPSBvcHRpb25zICYmIG9wdGlvbnMuZW5jb2RlIHx8IGVuY29kZTtcblxuICBjb25zdCBzZXJpYWxpemVGbiA9IG9wdGlvbnMgJiYgb3B0aW9ucy5zZXJpYWxpemU7XG5cbiAgbGV0IHNlcmlhbGl6ZWRQYXJhbXM7XG5cbiAgaWYgKHNlcmlhbGl6ZUZuKSB7XG4gICAgc2VyaWFsaXplZFBhcmFtcyA9IHNlcmlhbGl6ZUZuKHBhcmFtcywgb3B0aW9ucyk7XG4gIH0gZWxzZSB7XG4gICAgc2VyaWFsaXplZFBhcmFtcyA9IHV0aWxzLmlzVVJMU2VhcmNoUGFyYW1zKHBhcmFtcykgP1xuICAgICAgcGFyYW1zLnRvU3RyaW5nKCkgOlxuICAgICAgbmV3IEF4aW9zVVJMU2VhcmNoUGFyYW1zKHBhcmFtcywgb3B0aW9ucykudG9TdHJpbmcoX2VuY29kZSk7XG4gIH1cblxuICBpZiAoc2VyaWFsaXplZFBhcmFtcykge1xuICAgIGNvbnN0IGhhc2htYXJrSW5kZXggPSB1cmwuaW5kZXhPZihcIiNcIik7XG5cbiAgICBpZiAoaGFzaG1hcmtJbmRleCAhPT0gLTEpIHtcbiAgICAgIHVybCA9IHVybC5zbGljZSgwLCBoYXNobWFya0luZGV4KTtcbiAgICB9XG4gICAgdXJsICs9ICh1cmwuaW5kZXhPZignPycpID09PSAtMSA/ICc/JyA6ICcmJykgKyBzZXJpYWxpemVkUGFyYW1zO1xuICB9XG5cbiAgcmV0dXJuIHVybDtcbn1cblxuY2xhc3MgSW50ZXJjZXB0b3JNYW5hZ2VyIHtcbiAgY29uc3RydWN0b3IoKSB7XG4gICAgdGhpcy5oYW5kbGVycyA9IFtdO1xuICB9XG5cbiAgLyoqXG4gICAqIEFkZCBhIG5ldyBpbnRlcmNlcHRvciB0byB0aGUgc3RhY2tcbiAgICpcbiAgICogQHBhcmFtIHtGdW5jdGlvbn0gZnVsZmlsbGVkIFRoZSBmdW5jdGlvbiB0byBoYW5kbGUgYHRoZW5gIGZvciBhIGBQcm9taXNlYFxuICAgKiBAcGFyYW0ge0Z1bmN0aW9ufSByZWplY3RlZCBUaGUgZnVuY3Rpb24gdG8gaGFuZGxlIGByZWplY3RgIGZvciBhIGBQcm9taXNlYFxuICAgKlxuICAgKiBAcmV0dXJuIHtOdW1iZXJ9IEFuIElEIHVzZWQgdG8gcmVtb3ZlIGludGVyY2VwdG9yIGxhdGVyXG4gICAqL1xuICB1c2UoZnVsZmlsbGVkLCByZWplY3RlZCwgb3B0aW9ucykge1xuICAgIHRoaXMuaGFuZGxlcnMucHVzaCh7XG4gICAgICBmdWxmaWxsZWQsXG4gICAgICByZWplY3RlZCxcbiAgICAgIHN5bmNocm9ub3VzOiBvcHRpb25zID8gb3B0aW9ucy5zeW5jaHJvbm91cyA6IGZhbHNlLFxuICAgICAgcnVuV2hlbjogb3B0aW9ucyA/IG9wdGlvbnMucnVuV2hlbiA6IG51bGxcbiAgICB9KTtcbiAgICByZXR1cm4gdGhpcy5oYW5kbGVycy5sZW5ndGggLSAxO1xuICB9XG5cbiAgLyoqXG4gICAqIFJlbW92ZSBhbiBpbnRlcmNlcHRvciBmcm9tIHRoZSBzdGFja1xuICAgKlxuICAgKiBAcGFyYW0ge051bWJlcn0gaWQgVGhlIElEIHRoYXQgd2FzIHJldHVybmVkIGJ5IGB1c2VgXG4gICAqXG4gICAqIEByZXR1cm5zIHtCb29sZWFufSBgdHJ1ZWAgaWYgdGhlIGludGVyY2VwdG9yIHdhcyByZW1vdmVkLCBgZmFsc2VgIG90aGVyd2lzZVxuICAgKi9cbiAgZWplY3QoaWQpIHtcbiAgICBpZiAodGhpcy5oYW5kbGVyc1tpZF0pIHtcbiAgICAgIHRoaXMuaGFuZGxlcnNbaWRdID0gbnVsbDtcbiAgICB9XG4gIH1cblxuICAvKipcbiAgICogQ2xlYXIgYWxsIGludGVyY2VwdG9ycyBmcm9tIHRoZSBzdGFja1xuICAgKlxuICAgKiBAcmV0dXJucyB7dm9pZH1cbiAgICovXG4gIGNsZWFyKCkge1xuICAgIGlmICh0aGlzLmhhbmRsZXJzKSB7XG4gICAgICB0aGlzLmhhbmRsZXJzID0gW107XG4gICAgfVxuICB9XG5cbiAgLyoqXG4gICAqIEl0ZXJhdGUgb3ZlciBhbGwgdGhlIHJlZ2lzdGVyZWQgaW50ZXJjZXB0b3JzXG4gICAqXG4gICAqIFRoaXMgbWV0aG9kIGlzIHBhcnRpY3VsYXJseSB1c2VmdWwgZm9yIHNraXBwaW5nIG92ZXIgYW55XG4gICAqIGludGVyY2VwdG9ycyB0aGF0IG1heSBoYXZlIGJlY29tZSBgbnVsbGAgY2FsbGluZyBgZWplY3RgLlxuICAgKlxuICAgKiBAcGFyYW0ge0Z1bmN0aW9ufSBmbiBUaGUgZnVuY3Rpb24gdG8gY2FsbCBmb3IgZWFjaCBpbnRlcmNlcHRvclxuICAgKlxuICAgKiBAcmV0dXJucyB7dm9pZH1cbiAgICovXG4gIGZvckVhY2goZm4pIHtcbiAgICB1dGlscy5mb3JFYWNoKHRoaXMuaGFuZGxlcnMsIGZ1bmN0aW9uIGZvckVhY2hIYW5kbGVyKGgpIHtcbiAgICAgIGlmIChoICE9PSBudWxsKSB7XG4gICAgICAgIGZuKGgpO1xuICAgICAgfVxuICAgIH0pO1xuICB9XG59XG5cbmNvbnN0IEludGVyY2VwdG9yTWFuYWdlciQxID0gSW50ZXJjZXB0b3JNYW5hZ2VyO1xuXG5jb25zdCB0cmFuc2l0aW9uYWxEZWZhdWx0cyA9IHtcbiAgc2lsZW50SlNPTlBhcnNpbmc6IHRydWUsXG4gIGZvcmNlZEpTT05QYXJzaW5nOiB0cnVlLFxuICBjbGFyaWZ5VGltZW91dEVycm9yOiBmYWxzZVxufTtcblxuY29uc3QgVVJMU2VhcmNoUGFyYW1zID0gdXJsX19kZWZhdWx0W1wiZGVmYXVsdFwiXS5VUkxTZWFyY2hQYXJhbXM7XG5cbmNvbnN0IHBsYXRmb3JtID0ge1xuICBpc05vZGU6IHRydWUsXG4gIGNsYXNzZXM6IHtcbiAgICBVUkxTZWFyY2hQYXJhbXMsXG4gICAgRm9ybURhdGE6IEZvcm1EYXRhX19kZWZhdWx0W1wiZGVmYXVsdFwiXSxcbiAgICBCbG9iOiB0eXBlb2YgQmxvYiAhPT0gJ3VuZGVmaW5lZCcgJiYgQmxvYiB8fCBudWxsXG4gIH0sXG4gIHByb3RvY29sczogWyAnaHR0cCcsICdodHRwcycsICdmaWxlJywgJ2RhdGEnIF1cbn07XG5cbmZ1bmN0aW9uIHRvVVJMRW5jb2RlZEZvcm0oZGF0YSwgb3B0aW9ucykge1xuICByZXR1cm4gdG9Gb3JtRGF0YShkYXRhLCBuZXcgcGxhdGZvcm0uY2xhc3Nlcy5VUkxTZWFyY2hQYXJhbXMoKSwgT2JqZWN0LmFzc2lnbih7XG4gICAgdmlzaXRvcjogZnVuY3Rpb24odmFsdWUsIGtleSwgcGF0aCwgaGVscGVycykge1xuICAgICAgaWYgKHV0aWxzLmlzQnVmZmVyKHZhbHVlKSkge1xuICAgICAgICB0aGlzLmFwcGVuZChrZXksIHZhbHVlLnRvU3RyaW5nKCdiYXNlNjQnKSk7XG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICAgIH1cblxuICAgICAgcmV0dXJuIGhlbHBlcnMuZGVmYXVsdFZpc2l0b3IuYXBwbHkodGhpcywgYXJndW1lbnRzKTtcbiAgICB9XG4gIH0sIG9wdGlvbnMpKTtcbn1cblxuLyoqXG4gKiBJdCB0YWtlcyBhIHN0cmluZyBsaWtlIGBmb29beF1beV1bel1gIGFuZCByZXR1cm5zIGFuIGFycmF5IGxpa2UgYFsnZm9vJywgJ3gnLCAneScsICd6J11cbiAqXG4gKiBAcGFyYW0ge3N0cmluZ30gbmFtZSAtIFRoZSBuYW1lIG9mIHRoZSBwcm9wZXJ0eSB0byBnZXQuXG4gKlxuICogQHJldHVybnMgQW4gYXJyYXkgb2Ygc3RyaW5ncy5cbiAqL1xuZnVuY3Rpb24gcGFyc2VQcm9wUGF0aChuYW1lKSB7XG4gIC8vIGZvb1t4XVt5XVt6XVxuICAvLyBmb28ueC55LnpcbiAgLy8gZm9vLXgteS16XG4gIC8vIGZvbyB4IHkgelxuICByZXR1cm4gdXRpbHMubWF0Y2hBbGwoL1xcdyt8XFxbKFxcdyopXS9nLCBuYW1lKS5tYXAobWF0Y2ggPT4ge1xuICAgIHJldHVybiBtYXRjaFswXSA9PT0gJ1tdJyA/ICcnIDogbWF0Y2hbMV0gfHwgbWF0Y2hbMF07XG4gIH0pO1xufVxuXG4vKipcbiAqIENvbnZlcnQgYW4gYXJyYXkgdG8gYW4gb2JqZWN0LlxuICpcbiAqIEBwYXJhbSB7QXJyYXk8YW55Pn0gYXJyIC0gVGhlIGFycmF5IHRvIGNvbnZlcnQgdG8gYW4gb2JqZWN0LlxuICpcbiAqIEByZXR1cm5zIEFuIG9iamVjdCB3aXRoIHRoZSBzYW1lIGtleXMgYW5kIHZhbHVlcyBhcyB0aGUgYXJyYXkuXG4gKi9cbmZ1bmN0aW9uIGFycmF5VG9PYmplY3QoYXJyKSB7XG4gIGNvbnN0IG9iaiA9IHt9O1xuICBjb25zdCBrZXlzID0gT2JqZWN0LmtleXMoYXJyKTtcbiAgbGV0IGk7XG4gIGNvbnN0IGxlbiA9IGtleXMubGVuZ3RoO1xuICBsZXQga2V5O1xuICBmb3IgKGkgPSAwOyBpIDwgbGVuOyBpKyspIHtcbiAgICBrZXkgPSBrZXlzW2ldO1xuICAgIG9ialtrZXldID0gYXJyW2tleV07XG4gIH1cbiAgcmV0dXJuIG9iajtcbn1cblxuLyoqXG4gKiBJdCB0YWtlcyBhIEZvcm1EYXRhIG9iamVjdCBhbmQgcmV0dXJucyBhIEphdmFTY3JpcHQgb2JqZWN0XG4gKlxuICogQHBhcmFtIHtzdHJpbmd9IGZvcm1EYXRhIFRoZSBGb3JtRGF0YSBvYmplY3QgdG8gY29udmVydCB0byBKU09OLlxuICpcbiAqIEByZXR1cm5zIHtPYmplY3Q8c3RyaW5nLCBhbnk+IHwgbnVsbH0gVGhlIGNvbnZlcnRlZCBvYmplY3QuXG4gKi9cbmZ1bmN0aW9uIGZvcm1EYXRhVG9KU09OKGZvcm1EYXRhKSB7XG4gIGZ1bmN0aW9uIGJ1aWxkUGF0aChwYXRoLCB2YWx1ZSwgdGFyZ2V0LCBpbmRleCkge1xuICAgIGxldCBuYW1lID0gcGF0aFtpbmRleCsrXTtcbiAgICBjb25zdCBpc051bWVyaWNLZXkgPSBOdW1iZXIuaXNGaW5pdGUoK25hbWUpO1xuICAgIGNvbnN0IGlzTGFzdCA9IGluZGV4ID49IHBhdGgubGVuZ3RoO1xuICAgIG5hbWUgPSAhbmFtZSAmJiB1dGlscy5pc0FycmF5KHRhcmdldCkgPyB0YXJnZXQubGVuZ3RoIDogbmFtZTtcblxuICAgIGlmIChpc0xhc3QpIHtcbiAgICAgIGlmICh1dGlscy5oYXNPd25Qcm9wKHRhcmdldCwgbmFtZSkpIHtcbiAgICAgICAgdGFyZ2V0W25hbWVdID0gW3RhcmdldFtuYW1lXSwgdmFsdWVdO1xuICAgICAgfSBlbHNlIHtcbiAgICAgICAgdGFyZ2V0W25hbWVdID0gdmFsdWU7XG4gICAgICB9XG5cbiAgICAgIHJldHVybiAhaXNOdW1lcmljS2V5O1xuICAgIH1cblxuICAgIGlmICghdGFyZ2V0W25hbWVdIHx8ICF1dGlscy5pc09iamVjdCh0YXJnZXRbbmFtZV0pKSB7XG4gICAgICB0YXJnZXRbbmFtZV0gPSBbXTtcbiAgICB9XG5cbiAgICBjb25zdCByZXN1bHQgPSBidWlsZFBhdGgocGF0aCwgdmFsdWUsIHRhcmdldFtuYW1lXSwgaW5kZXgpO1xuXG4gICAgaWYgKHJlc3VsdCAmJiB1dGlscy5pc0FycmF5KHRhcmdldFtuYW1lXSkpIHtcbiAgICAgIHRhcmdldFtuYW1lXSA9IGFycmF5VG9PYmplY3QodGFyZ2V0W25hbWVdKTtcbiAgICB9XG5cbiAgICByZXR1cm4gIWlzTnVtZXJpY0tleTtcbiAgfVxuXG4gIGlmICh1dGlscy5pc0Zvcm1EYXRhKGZvcm1EYXRhKSAmJiB1dGlscy5pc0Z1bmN0aW9uKGZvcm1EYXRhLmVudHJpZXMpKSB7XG4gICAgY29uc3Qgb2JqID0ge307XG5cbiAgICB1dGlscy5mb3JFYWNoRW50cnkoZm9ybURhdGEsIChuYW1lLCB2YWx1ZSkgPT4ge1xuICAgICAgYnVpbGRQYXRoKHBhcnNlUHJvcFBhdGgobmFtZSksIHZhbHVlLCBvYmosIDApO1xuICAgIH0pO1xuXG4gICAgcmV0dXJuIG9iajtcbiAgfVxuXG4gIHJldHVybiBudWxsO1xufVxuXG4vKipcbiAqIEl0IHRha2VzIGEgc3RyaW5nLCB0cmllcyB0byBwYXJzZSBpdCwgYW5kIGlmIGl0IGZhaWxzLCBpdCByZXR1cm5zIHRoZSBzdHJpbmdpZmllZCB2ZXJzaW9uXG4gKiBvZiB0aGUgaW5wdXRcbiAqXG4gKiBAcGFyYW0ge2FueX0gcmF3VmFsdWUgLSBUaGUgdmFsdWUgdG8gYmUgc3RyaW5naWZpZWQuXG4gKiBAcGFyYW0ge0Z1bmN0aW9ufSBwYXJzZXIgLSBBIGZ1bmN0aW9uIHRoYXQgcGFyc2VzIGEgc3RyaW5nIGludG8gYSBKYXZhU2NyaXB0IG9iamVjdC5cbiAqIEBwYXJhbSB7RnVuY3Rpb259IGVuY29kZXIgLSBBIGZ1bmN0aW9uIHRoYXQgdGFrZXMgYSB2YWx1ZSBhbmQgcmV0dXJucyBhIHN0cmluZy5cbiAqXG4gKiBAcmV0dXJucyB7c3RyaW5nfSBBIHN0cmluZ2lmaWVkIHZlcnNpb24gb2YgdGhlIHJhd1ZhbHVlLlxuICovXG5mdW5jdGlvbiBzdHJpbmdpZnlTYWZlbHkocmF3VmFsdWUsIHBhcnNlciwgZW5jb2Rlcikge1xuICBpZiAodXRpbHMuaXNTdHJpbmcocmF3VmFsdWUpKSB7XG4gICAgdHJ5IHtcbiAgICAgIChwYXJzZXIgfHwgSlNPTi5wYXJzZSkocmF3VmFsdWUpO1xuICAgICAgcmV0dXJuIHV0aWxzLnRyaW0ocmF3VmFsdWUpO1xuICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgIGlmIChlLm5hbWUgIT09ICdTeW50YXhFcnJvcicpIHtcbiAgICAgICAgdGhyb3cgZTtcbiAgICAgIH1cbiAgICB9XG4gIH1cblxuICByZXR1cm4gKGVuY29kZXIgfHwgSlNPTi5zdHJpbmdpZnkpKHJhd1ZhbHVlKTtcbn1cblxuY29uc3QgZGVmYXVsdHMgPSB7XG5cbiAgdHJhbnNpdGlvbmFsOiB0cmFuc2l0aW9uYWxEZWZhdWx0cyxcblxuICBhZGFwdGVyOiBbJ3hocicsICdodHRwJ10sXG5cbiAgdHJhbnNmb3JtUmVxdWVzdDogW2Z1bmN0aW9uIHRyYW5zZm9ybVJlcXVlc3QoZGF0YSwgaGVhZGVycykge1xuICAgIGNvbnN0IGNvbnRlbnRUeXBlID0gaGVhZGVycy5nZXRDb250ZW50VHlwZSgpIHx8ICcnO1xuICAgIGNvbnN0IGhhc0pTT05Db250ZW50VHlwZSA9IGNvbnRlbnRUeXBlLmluZGV4T2YoJ2FwcGxpY2F0aW9uL2pzb24nKSA+IC0xO1xuICAgIGNvbnN0IGlzT2JqZWN0UGF5bG9hZCA9IHV0aWxzLmlzT2JqZWN0KGRhdGEpO1xuXG4gICAgaWYgKGlzT2JqZWN0UGF5bG9hZCAmJiB1dGlscy5pc0hUTUxGb3JtKGRhdGEpKSB7XG4gICAgICBkYXRhID0gbmV3IEZvcm1EYXRhKGRhdGEpO1xuICAgIH1cblxuICAgIGNvbnN0IGlzRm9ybURhdGEgPSB1dGlscy5pc0Zvcm1EYXRhKGRhdGEpO1xuXG4gICAgaWYgKGlzRm9ybURhdGEpIHtcbiAgICAgIGlmICghaGFzSlNPTkNvbnRlbnRUeXBlKSB7XG4gICAgICAgIHJldHVybiBkYXRhO1xuICAgICAgfVxuICAgICAgcmV0dXJuIGhhc0pTT05Db250ZW50VHlwZSA/IEpTT04uc3RyaW5naWZ5KGZvcm1EYXRhVG9KU09OKGRhdGEpKSA6IGRhdGE7XG4gICAgfVxuXG4gICAgaWYgKHV0aWxzLmlzQXJyYXlCdWZmZXIoZGF0YSkgfHxcbiAgICAgIHV0aWxzLmlzQnVmZmVyKGRhdGEpIHx8XG4gICAgICB1dGlscy5pc1N0cmVhbShkYXRhKSB8fFxuICAgICAgdXRpbHMuaXNGaWxlKGRhdGEpIHx8XG4gICAgICB1dGlscy5pc0Jsb2IoZGF0YSlcbiAgICApIHtcbiAgICAgIHJldHVybiBkYXRhO1xuICAgIH1cbiAgICBpZiAodXRpbHMuaXNBcnJheUJ1ZmZlclZpZXcoZGF0YSkpIHtcbiAgICAgIHJldHVybiBkYXRhLmJ1ZmZlcjtcbiAgICB9XG4gICAgaWYgKHV0aWxzLmlzVVJMU2VhcmNoUGFyYW1zKGRhdGEpKSB7XG4gICAgICBoZWFkZXJzLnNldENvbnRlbnRUeXBlKCdhcHBsaWNhdGlvbi94LXd3dy1mb3JtLXVybGVuY29kZWQ7Y2hhcnNldD11dGYtOCcsIGZhbHNlKTtcbiAgICAgIHJldHVybiBkYXRhLnRvU3RyaW5nKCk7XG4gICAgfVxuXG4gICAgbGV0IGlzRmlsZUxpc3Q7XG5cbiAgICBpZiAoaXNPYmplY3RQYXlsb2FkKSB7XG4gICAgICBpZiAoY29udGVudFR5cGUuaW5kZXhPZignYXBwbGljYXRpb24veC13d3ctZm9ybS11cmxlbmNvZGVkJykgPiAtMSkge1xuICAgICAgICByZXR1cm4gdG9VUkxFbmNvZGVkRm9ybShkYXRhLCB0aGlzLmZvcm1TZXJpYWxpemVyKS50b1N0cmluZygpO1xuICAgICAgfVxuXG4gICAgICBpZiAoKGlzRmlsZUxpc3QgPSB1dGlscy5pc0ZpbGVMaXN0KGRhdGEpKSB8fCBjb250ZW50VHlwZS5pbmRleE9mKCdtdWx0aXBhcnQvZm9ybS1kYXRhJykgPiAtMSkge1xuICAgICAgICBjb25zdCBfRm9ybURhdGEgPSB0aGlzLmVudiAmJiB0aGlzLmVudi5Gb3JtRGF0YTtcblxuICAgICAgICByZXR1cm4gdG9Gb3JtRGF0YShcbiAgICAgICAgICBpc0ZpbGVMaXN0ID8geydmaWxlc1tdJzogZGF0YX0gOiBkYXRhLFxuICAgICAgICAgIF9Gb3JtRGF0YSAmJiBuZXcgX0Zvcm1EYXRhKCksXG4gICAgICAgICAgdGhpcy5mb3JtU2VyaWFsaXplclxuICAgICAgICApO1xuICAgICAgfVxuICAgIH1cblxuICAgIGlmIChpc09iamVjdFBheWxvYWQgfHwgaGFzSlNPTkNvbnRlbnRUeXBlICkge1xuICAgICAgaGVhZGVycy5zZXRDb250ZW50VHlwZSgnYXBwbGljYXRpb24vanNvbicsIGZhbHNlKTtcbiAgICAgIHJldHVybiBzdHJpbmdpZnlTYWZlbHkoZGF0YSk7XG4gICAgfVxuXG4gICAgcmV0dXJuIGRhdGE7XG4gIH1dLFxuXG4gIHRyYW5zZm9ybVJlc3BvbnNlOiBbZnVuY3Rpb24gdHJhbnNmb3JtUmVzcG9uc2UoZGF0YSkge1xuICAgIGNvbnN0IHRyYW5zaXRpb25hbCA9IHRoaXMudHJhbnNpdGlvbmFsIHx8IGRlZmF1bHRzLnRyYW5zaXRpb25hbDtcbiAgICBjb25zdCBmb3JjZWRKU09OUGFyc2luZyA9IHRyYW5zaXRpb25hbCAmJiB0cmFuc2l0aW9uYWwuZm9yY2VkSlNPTlBhcnNpbmc7XG4gICAgY29uc3QgSlNPTlJlcXVlc3RlZCA9IHRoaXMucmVzcG9uc2VUeXBlID09PSAnanNvbic7XG5cbiAgICBpZiAoZGF0YSAmJiB1dGlscy5pc1N0cmluZyhkYXRhKSAmJiAoKGZvcmNlZEpTT05QYXJzaW5nICYmICF0aGlzLnJlc3BvbnNlVHlwZSkgfHwgSlNPTlJlcXVlc3RlZCkpIHtcbiAgICAgIGNvbnN0IHNpbGVudEpTT05QYXJzaW5nID0gdHJhbnNpdGlvbmFsICYmIHRyYW5zaXRpb25hbC5zaWxlbnRKU09OUGFyc2luZztcbiAgICAgIGNvbnN0IHN0cmljdEpTT05QYXJzaW5nID0gIXNpbGVudEpTT05QYXJzaW5nICYmIEpTT05SZXF1ZXN0ZWQ7XG5cbiAgICAgIHRyeSB7XG4gICAgICAgIHJldHVybiBKU09OLnBhcnNlKGRhdGEpO1xuICAgICAgfSBjYXRjaCAoZSkge1xuICAgICAgICBpZiAoc3RyaWN0SlNPTlBhcnNpbmcpIHtcbiAgICAgICAgICBpZiAoZS5uYW1lID09PSAnU3ludGF4RXJyb3InKSB7XG4gICAgICAgICAgICB0aHJvdyBBeGlvc0Vycm9yLmZyb20oZSwgQXhpb3NFcnJvci5FUlJfQkFEX1JFU1BPTlNFLCB0aGlzLCBudWxsLCB0aGlzLnJlc3BvbnNlKTtcbiAgICAgICAgICB9XG4gICAgICAgICAgdGhyb3cgZTtcbiAgICAgICAgfVxuICAgICAgfVxuICAgIH1cblxuICAgIHJldHVybiBkYXRhO1xuICB9XSxcblxuICAvKipcbiAgICogQSB0aW1lb3V0IGluIG1pbGxpc2Vjb25kcyB0byBhYm9ydCBhIHJlcXVlc3QuIElmIHNldCB0byAwIChkZWZhdWx0KSBhXG4gICAqIHRpbWVvdXQgaXMgbm90IGNyZWF0ZWQuXG4gICAqL1xuICB0aW1lb3V0OiAwLFxuXG4gIHhzcmZDb29raWVOYW1lOiAnWFNSRi1UT0tFTicsXG4gIHhzcmZIZWFkZXJOYW1lOiAnWC1YU1JGLVRPS0VOJyxcblxuICBtYXhDb250ZW50TGVuZ3RoOiAtMSxcbiAgbWF4Qm9keUxlbmd0aDogLTEsXG5cbiAgZW52OiB7XG4gICAgRm9ybURhdGE6IHBsYXRmb3JtLmNsYXNzZXMuRm9ybURhdGEsXG4gICAgQmxvYjogcGxhdGZvcm0uY2xhc3Nlcy5CbG9iXG4gIH0sXG5cbiAgdmFsaWRhdGVTdGF0dXM6IGZ1bmN0aW9uIHZhbGlkYXRlU3RhdHVzKHN0YXR1cykge1xuICAgIHJldHVybiBzdGF0dXMgPj0gMjAwICYmIHN0YXR1cyA8IDMwMDtcbiAgfSxcblxuICBoZWFkZXJzOiB7XG4gICAgY29tbW9uOiB7XG4gICAgICAnQWNjZXB0JzogJ2FwcGxpY2F0aW9uL2pzb24sIHRleHQvcGxhaW4sICovKicsXG4gICAgICAnQ29udGVudC1UeXBlJzogdW5kZWZpbmVkXG4gICAgfVxuICB9XG59O1xuXG51dGlscy5mb3JFYWNoKFsnZGVsZXRlJywgJ2dldCcsICdoZWFkJywgJ3Bvc3QnLCAncHV0JywgJ3BhdGNoJ10sIChtZXRob2QpID0+IHtcbiAgZGVmYXVsdHMuaGVhZGVyc1ttZXRob2RdID0ge307XG59KTtcblxuY29uc3QgZGVmYXVsdHMkMSA9IGRlZmF1bHRzO1xuXG4vLyBSYXdBeGlvc0hlYWRlcnMgd2hvc2UgZHVwbGljYXRlcyBhcmUgaWdub3JlZCBieSBub2RlXG4vLyBjLmYuIGh0dHBzOi8vbm9kZWpzLm9yZy9hcGkvaHR0cC5odG1sI2h0dHBfbWVzc2FnZV9oZWFkZXJzXG5jb25zdCBpZ25vcmVEdXBsaWNhdGVPZiA9IHV0aWxzLnRvT2JqZWN0U2V0KFtcbiAgJ2FnZScsICdhdXRob3JpemF0aW9uJywgJ2NvbnRlbnQtbGVuZ3RoJywgJ2NvbnRlbnQtdHlwZScsICdldGFnJyxcbiAgJ2V4cGlyZXMnLCAnZnJvbScsICdob3N0JywgJ2lmLW1vZGlmaWVkLXNpbmNlJywgJ2lmLXVubW9kaWZpZWQtc2luY2UnLFxuICAnbGFzdC1tb2RpZmllZCcsICdsb2NhdGlvbicsICdtYXgtZm9yd2FyZHMnLCAncHJveHktYXV0aG9yaXphdGlvbicsXG4gICdyZWZlcmVyJywgJ3JldHJ5LWFmdGVyJywgJ3VzZXItYWdlbnQnXG5dKTtcblxuLyoqXG4gKiBQYXJzZSBoZWFkZXJzIGludG8gYW4gb2JqZWN0XG4gKlxuICogYGBgXG4gKiBEYXRlOiBXZWQsIDI3IEF1ZyAyMDE0IDA4OjU4OjQ5IEdNVFxuICogQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi9qc29uXG4gKiBDb25uZWN0aW9uOiBrZWVwLWFsaXZlXG4gKiBUcmFuc2Zlci1FbmNvZGluZzogY2h1bmtlZFxuICogYGBgXG4gKlxuICogQHBhcmFtIHtTdHJpbmd9IHJhd0hlYWRlcnMgSGVhZGVycyBuZWVkaW5nIHRvIGJlIHBhcnNlZFxuICpcbiAqIEByZXR1cm5zIHtPYmplY3R9IEhlYWRlcnMgcGFyc2VkIGludG8gYW4gb2JqZWN0XG4gKi9cbmNvbnN0IHBhcnNlSGVhZGVycyA9IHJhd0hlYWRlcnMgPT4ge1xuICBjb25zdCBwYXJzZWQgPSB7fTtcbiAgbGV0IGtleTtcbiAgbGV0IHZhbDtcbiAgbGV0IGk7XG5cbiAgcmF3SGVhZGVycyAmJiByYXdIZWFkZXJzLnNwbGl0KCdcXG4nKS5mb3JFYWNoKGZ1bmN0aW9uIHBhcnNlcihsaW5lKSB7XG4gICAgaSA9IGxpbmUuaW5kZXhPZignOicpO1xuICAgIGtleSA9IGxpbmUuc3Vic3RyaW5nKDAsIGkpLnRyaW0oKS50b0xvd2VyQ2FzZSgpO1xuICAgIHZhbCA9IGxpbmUuc3Vic3RyaW5nKGkgKyAxKS50cmltKCk7XG5cbiAgICBpZiAoIWtleSB8fCAocGFyc2VkW2tleV0gJiYgaWdub3JlRHVwbGljYXRlT2Zba2V5XSkpIHtcbiAgICAgIHJldHVybjtcbiAgICB9XG5cbiAgICBpZiAoa2V5ID09PSAnc2V0LWNvb2tpZScpIHtcbiAgICAgIGlmIChwYXJzZWRba2V5XSkge1xuICAgICAgICBwYXJzZWRba2V5XS5wdXNoKHZhbCk7XG4gICAgICB9IGVsc2Uge1xuICAgICAgICBwYXJzZWRba2V5XSA9IFt2YWxdO1xuICAgICAgfVxuICAgIH0gZWxzZSB7XG4gICAgICBwYXJzZWRba2V5XSA9IHBhcnNlZFtrZXldID8gcGFyc2VkW2tleV0gKyAnLCAnICsgdmFsIDogdmFsO1xuICAgIH1cbiAgfSk7XG5cbiAgcmV0dXJuIHBhcnNlZDtcbn07XG5cbmNvbnN0ICRpbnRlcm5hbHMgPSBTeW1ib2woJ2ludGVybmFscycpO1xuXG5mdW5jdGlvbiBub3JtYWxpemVIZWFkZXIoaGVhZGVyKSB7XG4gIHJldHVybiBoZWFkZXIgJiYgU3RyaW5nKGhlYWRlcikudHJpbSgpLnRvTG93ZXJDYXNlKCk7XG59XG5cbmZ1bmN0aW9uIG5vcm1hbGl6ZVZhbHVlKHZhbHVlKSB7XG4gIGlmICh2YWx1ZSA9PT0gZmFsc2UgfHwgdmFsdWUgPT0gbnVsbCkge1xuICAgIHJldHVybiB2YWx1ZTtcbiAgfVxuXG4gIHJldHVybiB1dGlscy5pc0FycmF5KHZhbHVlKSA/IHZhbHVlLm1hcChub3JtYWxpemVWYWx1ZSkgOiBTdHJpbmcodmFsdWUpO1xufVxuXG5mdW5jdGlvbiBwYXJzZVRva2VucyhzdHIpIHtcbiAgY29uc3QgdG9rZW5zID0gT2JqZWN0LmNyZWF0ZShudWxsKTtcbiAgY29uc3QgdG9rZW5zUkUgPSAvKFteXFxzLDs9XSspXFxzKig/Oj1cXHMqKFteLDtdKykpPy9nO1xuICBsZXQgbWF0Y2g7XG5cbiAgd2hpbGUgKChtYXRjaCA9IHRva2Vuc1JFLmV4ZWMoc3RyKSkpIHtcbiAgICB0b2tlbnNbbWF0Y2hbMV1dID0gbWF0Y2hbMl07XG4gIH1cblxuICByZXR1cm4gdG9rZW5zO1xufVxuXG5jb25zdCBpc1ZhbGlkSGVhZGVyTmFtZSA9IChzdHIpID0+IC9eWy1fYS16QS1aMC05XmB8fiwhIyQlJicqKy5dKyQvLnRlc3Qoc3RyLnRyaW0oKSk7XG5cbmZ1bmN0aW9uIG1hdGNoSGVhZGVyVmFsdWUoY29udGV4dCwgdmFsdWUsIGhlYWRlciwgZmlsdGVyLCBpc0hlYWRlck5hbWVGaWx0ZXIpIHtcbiAgaWYgKHV0aWxzLmlzRnVuY3Rpb24oZmlsdGVyKSkge1xuICAgIHJldHVybiBmaWx0ZXIuY2FsbCh0aGlzLCB2YWx1ZSwgaGVhZGVyKTtcbiAgfVxuXG4gIGlmIChpc0hlYWRlck5hbWVGaWx0ZXIpIHtcbiAgICB2YWx1ZSA9IGhlYWRlcjtcbiAgfVxuXG4gIGlmICghdXRpbHMuaXNTdHJpbmcodmFsdWUpKSByZXR1cm47XG5cbiAgaWYgKHV0aWxzLmlzU3RyaW5nKGZpbHRlcikpIHtcbiAgICByZXR1cm4gdmFsdWUuaW5kZXhPZihmaWx0ZXIpICE9PSAtMTtcbiAgfVxuXG4gIGlmICh1dGlscy5pc1JlZ0V4cChmaWx0ZXIpKSB7XG4gICAgcmV0dXJuIGZpbHRlci50ZXN0KHZhbHVlKTtcbiAgfVxufVxuXG5mdW5jdGlvbiBmb3JtYXRIZWFkZXIoaGVhZGVyKSB7XG4gIHJldHVybiBoZWFkZXIudHJpbSgpXG4gICAgLnRvTG93ZXJDYXNlKCkucmVwbGFjZSgvKFthLXpcXGRdKShcXHcqKS9nLCAodywgY2hhciwgc3RyKSA9PiB7XG4gICAgICByZXR1cm4gY2hhci50b1VwcGVyQ2FzZSgpICsgc3RyO1xuICAgIH0pO1xufVxuXG5mdW5jdGlvbiBidWlsZEFjY2Vzc29ycyhvYmosIGhlYWRlcikge1xuICBjb25zdCBhY2Nlc3Nvck5hbWUgPSB1dGlscy50b0NhbWVsQ2FzZSgnICcgKyBoZWFkZXIpO1xuXG4gIFsnZ2V0JywgJ3NldCcsICdoYXMnXS5mb3JFYWNoKG1ldGhvZE5hbWUgPT4ge1xuICAgIE9iamVjdC5kZWZpbmVQcm9wZXJ0eShvYmosIG1ldGhvZE5hbWUgKyBhY2Nlc3Nvck5hbWUsIHtcbiAgICAgIHZhbHVlOiBmdW5jdGlvbihhcmcxLCBhcmcyLCBhcmczKSB7XG4gICAgICAgIHJldHVybiB0aGlzW21ldGhvZE5hbWVdLmNhbGwodGhpcywgaGVhZGVyLCBhcmcxLCBhcmcyLCBhcmczKTtcbiAgICAgIH0sXG4gICAgICBjb25maWd1cmFibGU6IHRydWVcbiAgICB9KTtcbiAgfSk7XG59XG5cbmNsYXNzIEF4aW9zSGVhZGVycyB7XG4gIGNvbnN0cnVjdG9yKGhlYWRlcnMpIHtcbiAgICBoZWFkZXJzICYmIHRoaXMuc2V0KGhlYWRlcnMpO1xuICB9XG5cbiAgc2V0KGhlYWRlciwgdmFsdWVPclJld3JpdGUsIHJld3JpdGUpIHtcbiAgICBjb25zdCBzZWxmID0gdGhpcztcblxuICAgIGZ1bmN0aW9uIHNldEhlYWRlcihfdmFsdWUsIF9oZWFkZXIsIF9yZXdyaXRlKSB7XG4gICAgICBjb25zdCBsSGVhZGVyID0gbm9ybWFsaXplSGVhZGVyKF9oZWFkZXIpO1xuXG4gICAgICBpZiAoIWxIZWFkZXIpIHtcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKCdoZWFkZXIgbmFtZSBtdXN0IGJlIGEgbm9uLWVtcHR5IHN0cmluZycpO1xuICAgICAgfVxuXG4gICAgICBjb25zdCBrZXkgPSB1dGlscy5maW5kS2V5KHNlbGYsIGxIZWFkZXIpO1xuXG4gICAgICBpZigha2V5IHx8IHNlbGZba2V5XSA9PT0gdW5kZWZpbmVkIHx8IF9yZXdyaXRlID09PSB0cnVlIHx8IChfcmV3cml0ZSA9PT0gdW5kZWZpbmVkICYmIHNlbGZba2V5XSAhPT0gZmFsc2UpKSB7XG4gICAgICAgIHNlbGZba2V5IHx8IF9oZWFkZXJdID0gbm9ybWFsaXplVmFsdWUoX3ZhbHVlKTtcbiAgICAgIH1cbiAgICB9XG5cbiAgICBjb25zdCBzZXRIZWFkZXJzID0gKGhlYWRlcnMsIF9yZXdyaXRlKSA9PlxuICAgICAgdXRpbHMuZm9yRWFjaChoZWFkZXJzLCAoX3ZhbHVlLCBfaGVhZGVyKSA9PiBzZXRIZWFkZXIoX3ZhbHVlLCBfaGVhZGVyLCBfcmV3cml0ZSkpO1xuXG4gICAgaWYgKHV0aWxzLmlzUGxhaW5PYmplY3QoaGVhZGVyKSB8fCBoZWFkZXIgaW5zdGFuY2VvZiB0aGlzLmNvbnN0cnVjdG9yKSB7XG4gICAgICBzZXRIZWFkZXJzKGhlYWRlciwgdmFsdWVPclJld3JpdGUpO1xuICAgIH0gZWxzZSBpZih1dGlscy5pc1N0cmluZyhoZWFkZXIpICYmIChoZWFkZXIgPSBoZWFkZXIudHJpbSgpKSAmJiAhaXNWYWxpZEhlYWRlck5hbWUoaGVhZGVyKSkge1xuICAgICAgc2V0SGVhZGVycyhwYXJzZUhlYWRlcnMoaGVhZGVyKSwgdmFsdWVPclJld3JpdGUpO1xuICAgIH0gZWxzZSB7XG4gICAgICBoZWFkZXIgIT0gbnVsbCAmJiBzZXRIZWFkZXIodmFsdWVPclJld3JpdGUsIGhlYWRlciwgcmV3cml0ZSk7XG4gICAgfVxuXG4gICAgcmV0dXJuIHRoaXM7XG4gIH1cblxuICBnZXQoaGVhZGVyLCBwYXJzZXIpIHtcbiAgICBoZWFkZXIgPSBub3JtYWxpemVIZWFkZXIoaGVhZGVyKTtcblxuICAgIGlmIChoZWFkZXIpIHtcbiAgICAgIGNvbnN0IGtleSA9IHV0aWxzLmZpbmRLZXkodGhpcywgaGVhZGVyKTtcblxuICAgICAgaWYgKGtleSkge1xuICAgICAgICBjb25zdCB2YWx1ZSA9IHRoaXNba2V5XTtcblxuICAgICAgICBpZiAoIXBhcnNlcikge1xuICAgICAgICAgIHJldHVybiB2YWx1ZTtcbiAgICAgICAgfVxuXG4gICAgICAgIGlmIChwYXJzZXIgPT09IHRydWUpIHtcbiAgICAgICAgICByZXR1cm4gcGFyc2VUb2tlbnModmFsdWUpO1xuICAgICAgICB9XG5cbiAgICAgICAgaWYgKHV0aWxzLmlzRnVuY3Rpb24ocGFyc2VyKSkge1xuICAgICAgICAgIHJldHVybiBwYXJzZXIuY2FsbCh0aGlzLCB2YWx1ZSwga2V5KTtcbiAgICAgICAgfVxuXG4gICAgICAgIGlmICh1dGlscy5pc1JlZ0V4cChwYXJzZXIpKSB7XG4gICAgICAgICAgcmV0dXJuIHBhcnNlci5leGVjKHZhbHVlKTtcbiAgICAgICAgfVxuXG4gICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ3BhcnNlciBtdXN0IGJlIGJvb2xlYW58cmVnZXhwfGZ1bmN0aW9uJyk7XG4gICAgICB9XG4gICAgfVxuICB9XG5cbiAgaGFzKGhlYWRlciwgbWF0Y2hlcikge1xuICAgIGhlYWRlciA9IG5vcm1hbGl6ZUhlYWRlcihoZWFkZXIpO1xuXG4gICAgaWYgKGhlYWRlcikge1xuICAgICAgY29uc3Qga2V5ID0gdXRpbHMuZmluZEtleSh0aGlzLCBoZWFkZXIpO1xuXG4gICAgICByZXR1cm4gISEoa2V5ICYmIHRoaXNba2V5XSAhPT0gdW5kZWZpbmVkICYmICghbWF0Y2hlciB8fCBtYXRjaEhlYWRlclZhbHVlKHRoaXMsIHRoaXNba2V5XSwga2V5LCBtYXRjaGVyKSkpO1xuICAgIH1cblxuICAgIHJldHVybiBmYWxzZTtcbiAgfVxuXG4gIGRlbGV0ZShoZWFkZXIsIG1hdGNoZXIpIHtcbiAgICBjb25zdCBzZWxmID0gdGhpcztcbiAgICBsZXQgZGVsZXRlZCA9IGZhbHNlO1xuXG4gICAgZnVuY3Rpb24gZGVsZXRlSGVhZGVyKF9oZWFkZXIpIHtcbiAgICAgIF9oZWFkZXIgPSBub3JtYWxpemVIZWFkZXIoX2hlYWRlcik7XG5cbiAgICAgIGlmIChfaGVhZGVyKSB7XG4gICAgICAgIGNvbnN0IGtleSA9IHV0aWxzLmZpbmRLZXkoc2VsZiwgX2hlYWRlcik7XG5cbiAgICAgICAgaWYgKGtleSAmJiAoIW1hdGNoZXIgfHwgbWF0Y2hIZWFkZXJWYWx1ZShzZWxmLCBzZWxmW2tleV0sIGtleSwgbWF0Y2hlcikpKSB7XG4gICAgICAgICAgZGVsZXRlIHNlbGZba2V5XTtcblxuICAgICAgICAgIGRlbGV0ZWQgPSB0cnVlO1xuICAgICAgICB9XG4gICAgICB9XG4gICAgfVxuXG4gICAgaWYgKHV0aWxzLmlzQXJyYXkoaGVhZGVyKSkge1xuICAgICAgaGVhZGVyLmZvckVhY2goZGVsZXRlSGVhZGVyKTtcbiAgICB9IGVsc2Uge1xuICAgICAgZGVsZXRlSGVhZGVyKGhlYWRlcik7XG4gICAgfVxuXG4gICAgcmV0dXJuIGRlbGV0ZWQ7XG4gIH1cblxuICBjbGVhcihtYXRjaGVyKSB7XG4gICAgY29uc3Qga2V5cyA9IE9iamVjdC5rZXlzKHRoaXMpO1xuICAgIGxldCBpID0ga2V5cy5sZW5ndGg7XG4gICAgbGV0IGRlbGV0ZWQgPSBmYWxzZTtcblxuICAgIHdoaWxlIChpLS0pIHtcbiAgICAgIGNvbnN0IGtleSA9IGtleXNbaV07XG4gICAgICBpZighbWF0Y2hlciB8fCBtYXRjaEhlYWRlclZhbHVlKHRoaXMsIHRoaXNba2V5XSwga2V5LCBtYXRjaGVyLCB0cnVlKSkge1xuICAgICAgICBkZWxldGUgdGhpc1trZXldO1xuICAgICAgICBkZWxldGVkID0gdHJ1ZTtcbiAgICAgIH1cbiAgICB9XG5cbiAgICByZXR1cm4gZGVsZXRlZDtcbiAgfVxuXG4gIG5vcm1hbGl6ZShmb3JtYXQpIHtcbiAgICBjb25zdCBzZWxmID0gdGhpcztcbiAgICBjb25zdCBoZWFkZXJzID0ge307XG5cbiAgICB1dGlscy5mb3JFYWNoKHRoaXMsICh2YWx1ZSwgaGVhZGVyKSA9PiB7XG4gICAgICBjb25zdCBrZXkgPSB1dGlscy5maW5kS2V5KGhlYWRlcnMsIGhlYWRlcik7XG5cbiAgICAgIGlmIChrZXkpIHtcbiAgICAgICAgc2VsZltrZXldID0gbm9ybWFsaXplVmFsdWUodmFsdWUpO1xuICAgICAgICBkZWxldGUgc2VsZltoZWFkZXJdO1xuICAgICAgICByZXR1cm47XG4gICAgICB9XG5cbiAgICAgIGNvbnN0IG5vcm1hbGl6ZWQgPSBmb3JtYXQgPyBmb3JtYXRIZWFkZXIoaGVhZGVyKSA6IFN0cmluZyhoZWFkZXIpLnRyaW0oKTtcblxuICAgICAgaWYgKG5vcm1hbGl6ZWQgIT09IGhlYWRlcikge1xuICAgICAgICBkZWxldGUgc2VsZltoZWFkZXJdO1xuICAgICAgfVxuXG4gICAgICBzZWxmW25vcm1hbGl6ZWRdID0gbm9ybWFsaXplVmFsdWUodmFsdWUpO1xuXG4gICAgICBoZWFkZXJzW25vcm1hbGl6ZWRdID0gdHJ1ZTtcbiAgICB9KTtcblxuICAgIHJldHVybiB0aGlzO1xuICB9XG5cbiAgY29uY2F0KC4uLnRhcmdldHMpIHtcbiAgICByZXR1cm4gdGhpcy5jb25zdHJ1Y3Rvci5jb25jYXQodGhpcywgLi4udGFyZ2V0cyk7XG4gIH1cblxuICB0b0pTT04oYXNTdHJpbmdzKSB7XG4gICAgY29uc3Qgb2JqID0gT2JqZWN0LmNyZWF0ZShudWxsKTtcblxuICAgIHV0aWxzLmZvckVhY2godGhpcywgKHZhbHVlLCBoZWFkZXIpID0+IHtcbiAgICAgIHZhbHVlICE9IG51bGwgJiYgdmFsdWUgIT09IGZhbHNlICYmIChvYmpbaGVhZGVyXSA9IGFzU3RyaW5ncyAmJiB1dGlscy5pc0FycmF5KHZhbHVlKSA/IHZhbHVlLmpvaW4oJywgJykgOiB2YWx1ZSk7XG4gICAgfSk7XG5cbiAgICByZXR1cm4gb2JqO1xuICB9XG5cbiAgW1N5bWJvbC5pdGVyYXRvcl0oKSB7XG4gICAgcmV0dXJuIE9iamVjdC5lbnRyaWVzKHRoaXMudG9KU09OKCkpW1N5bWJvbC5pdGVyYXRvcl0oKTtcbiAgfVxuXG4gIHRvU3RyaW5nKCkge1xuICAgIHJldHVybiBPYmplY3QuZW50cmllcyh0aGlzLnRvSlNPTigpKS5tYXAoKFtoZWFkZXIsIHZhbHVlXSkgPT4gaGVhZGVyICsgJzogJyArIHZhbHVlKS5qb2luKCdcXG4nKTtcbiAgfVxuXG4gIGdldCBbU3ltYm9sLnRvU3RyaW5nVGFnXSgpIHtcbiAgICByZXR1cm4gJ0F4aW9zSGVhZGVycyc7XG4gIH1cblxuICBzdGF0aWMgZnJvbSh0aGluZykge1xuICAgIHJldHVybiB0aGluZyBpbnN0YW5jZW9mIHRoaXMgPyB0aGluZyA6IG5ldyB0aGlzKHRoaW5nKTtcbiAgfVxuXG4gIHN0YXRpYyBjb25jYXQoZmlyc3QsIC4uLnRhcmdldHMpIHtcbiAgICBjb25zdCBjb21wdXRlZCA9IG5ldyB0aGlzKGZpcnN0KTtcblxuICAgIHRhcmdldHMuZm9yRWFjaCgodGFyZ2V0KSA9PiBjb21wdXRlZC5zZXQodGFyZ2V0KSk7XG5cbiAgICByZXR1cm4gY29tcHV0ZWQ7XG4gIH1cblxuICBzdGF0aWMgYWNjZXNzb3IoaGVhZGVyKSB7XG4gICAgY29uc3QgaW50ZXJuYWxzID0gdGhpc1skaW50ZXJuYWxzXSA9ICh0aGlzWyRpbnRlcm5hbHNdID0ge1xuICAgICAgYWNjZXNzb3JzOiB7fVxuICAgIH0pO1xuXG4gICAgY29uc3QgYWNjZXNzb3JzID0gaW50ZXJuYWxzLmFjY2Vzc29ycztcbiAgICBjb25zdCBwcm90b3R5cGUgPSB0aGlzLnByb3RvdHlwZTtcblxuICAgIGZ1bmN0aW9uIGRlZmluZUFjY2Vzc29yKF9oZWFkZXIpIHtcbiAgICAgIGNvbnN0IGxIZWFkZXIgPSBub3JtYWxpemVIZWFkZXIoX2hlYWRlcik7XG5cbiAgICAgIGlmICghYWNjZXNzb3JzW2xIZWFkZXJdKSB7XG4gICAgICAgIGJ1aWxkQWNjZXNzb3JzKHByb3RvdHlwZSwgX2hlYWRlcik7XG4gICAgICAgIGFjY2Vzc29yc1tsSGVhZGVyXSA9IHRydWU7XG4gICAgICB9XG4gICAgfVxuXG4gICAgdXRpbHMuaXNBcnJheShoZWFkZXIpID8gaGVhZGVyLmZvckVhY2goZGVmaW5lQWNjZXNzb3IpIDogZGVmaW5lQWNjZXNzb3IoaGVhZGVyKTtcblxuICAgIHJldHVybiB0aGlzO1xuICB9XG59XG5cbkF4aW9zSGVhZGVycy5hY2Nlc3NvcihbJ0NvbnRlbnQtVHlwZScsICdDb250ZW50LUxlbmd0aCcsICdBY2NlcHQnLCAnQWNjZXB0LUVuY29kaW5nJywgJ1VzZXItQWdlbnQnLCAnQXV0aG9yaXphdGlvbiddKTtcblxuLy8gcmVzZXJ2ZWQgbmFtZXMgaG90Zml4XG51dGlscy5yZWR1Y2VEZXNjcmlwdG9ycyhBeGlvc0hlYWRlcnMucHJvdG90eXBlLCAoe3ZhbHVlfSwga2V5KSA9PiB7XG4gIGxldCBtYXBwZWQgPSBrZXlbMF0udG9VcHBlckNhc2UoKSArIGtleS5zbGljZSgxKTsgLy8gbWFwIGBzZXRgID0+IGBTZXRgXG4gIHJldHVybiB7XG4gICAgZ2V0OiAoKSA9PiB2YWx1ZSxcbiAgICBzZXQoaGVhZGVyVmFsdWUpIHtcbiAgICAgIHRoaXNbbWFwcGVkXSA9IGhlYWRlclZhbHVlO1xuICAgIH1cbiAgfVxufSk7XG5cbnV0aWxzLmZyZWV6ZU1ldGhvZHMoQXhpb3NIZWFkZXJzKTtcblxuY29uc3QgQXhpb3NIZWFkZXJzJDEgPSBBeGlvc0hlYWRlcnM7XG5cbi8qKlxuICogVHJhbnNmb3JtIHRoZSBkYXRhIGZvciBhIHJlcXVlc3Qgb3IgYSByZXNwb25zZVxuICpcbiAqIEBwYXJhbSB7QXJyYXl8RnVuY3Rpb259IGZucyBBIHNpbmdsZSBmdW5jdGlvbiBvciBBcnJheSBvZiBmdW5jdGlvbnNcbiAqIEBwYXJhbSB7P09iamVjdH0gcmVzcG9uc2UgVGhlIHJlc3BvbnNlIG9iamVjdFxuICpcbiAqIEByZXR1cm5zIHsqfSBUaGUgcmVzdWx0aW5nIHRyYW5zZm9ybWVkIGRhdGFcbiAqL1xuZnVuY3Rpb24gdHJhbnNmb3JtRGF0YShmbnMsIHJlc3BvbnNlKSB7XG4gIGNvbnN0IGNvbmZpZyA9IHRoaXMgfHwgZGVmYXVsdHMkMTtcbiAgY29uc3QgY29udGV4dCA9IHJlc3BvbnNlIHx8IGNvbmZpZztcbiAgY29uc3QgaGVhZGVycyA9IEF4aW9zSGVhZGVycyQxLmZyb20oY29udGV4dC5oZWFkZXJzKTtcbiAgbGV0IGRhdGEgPSBjb250ZXh0LmRhdGE7XG5cbiAgdXRpbHMuZm9yRWFjaChmbnMsIGZ1bmN0aW9uIHRyYW5zZm9ybShmbikge1xuICAgIGRhdGEgPSBmbi5jYWxsKGNvbmZpZywgZGF0YSwgaGVhZGVycy5ub3JtYWxpemUoKSwgcmVzcG9uc2UgPyByZXNwb25zZS5zdGF0dXMgOiB1bmRlZmluZWQpO1xuICB9KTtcblxuICBoZWFkZXJzLm5vcm1hbGl6ZSgpO1xuXG4gIHJldHVybiBkYXRhO1xufVxuXG5mdW5jdGlvbiBpc0NhbmNlbCh2YWx1ZSkge1xuICByZXR1cm4gISEodmFsdWUgJiYgdmFsdWUuX19DQU5DRUxfXyk7XG59XG5cbi8qKlxuICogQSBgQ2FuY2VsZWRFcnJvcmAgaXMgYW4gb2JqZWN0IHRoYXQgaXMgdGhyb3duIHdoZW4gYW4gb3BlcmF0aW9uIGlzIGNhbmNlbGVkLlxuICpcbiAqIEBwYXJhbSB7c3RyaW5nPX0gbWVzc2FnZSBUaGUgbWVzc2FnZS5cbiAqIEBwYXJhbSB7T2JqZWN0PX0gY29uZmlnIFRoZSBjb25maWcuXG4gKiBAcGFyYW0ge09iamVjdD19IHJlcXVlc3QgVGhlIHJlcXVlc3QuXG4gKlxuICogQHJldHVybnMge0NhbmNlbGVkRXJyb3J9IFRoZSBjcmVhdGVkIGVycm9yLlxuICovXG5mdW5jdGlvbiBDYW5jZWxlZEVycm9yKG1lc3NhZ2UsIGNvbmZpZywgcmVxdWVzdCkge1xuICAvLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgbm8tZXEtbnVsbCxlcWVxZXFcbiAgQXhpb3NFcnJvci5jYWxsKHRoaXMsIG1lc3NhZ2UgPT0gbnVsbCA/ICdjYW5jZWxlZCcgOiBtZXNzYWdlLCBBeGlvc0Vycm9yLkVSUl9DQU5DRUxFRCwgY29uZmlnLCByZXF1ZXN0KTtcbiAgdGhpcy5uYW1lID0gJ0NhbmNlbGVkRXJyb3InO1xufVxuXG51dGlscy5pbmhlcml0cyhDYW5jZWxlZEVycm9yLCBBeGlvc0Vycm9yLCB7XG4gIF9fQ0FOQ0VMX186IHRydWVcbn0pO1xuXG4vKipcbiAqIFJlc29sdmUgb3IgcmVqZWN0IGEgUHJvbWlzZSBiYXNlZCBvbiByZXNwb25zZSBzdGF0dXMuXG4gKlxuICogQHBhcmFtIHtGdW5jdGlvbn0gcmVzb2x2ZSBBIGZ1bmN0aW9uIHRoYXQgcmVzb2x2ZXMgdGhlIHByb21pc2UuXG4gKiBAcGFyYW0ge0Z1bmN0aW9ufSByZWplY3QgQSBmdW5jdGlvbiB0aGF0IHJlamVjdHMgdGhlIHByb21pc2UuXG4gKiBAcGFyYW0ge29iamVjdH0gcmVzcG9uc2UgVGhlIHJlc3BvbnNlLlxuICpcbiAqIEByZXR1cm5zIHtvYmplY3R9IFRoZSByZXNwb25zZS5cbiAqL1xuZnVuY3Rpb24gc2V0dGxlKHJlc29sdmUsIHJlamVjdCwgcmVzcG9uc2UpIHtcbiAgY29uc3QgdmFsaWRhdGVTdGF0dXMgPSByZXNwb25zZS5jb25maWcudmFsaWRhdGVTdGF0dXM7XG4gIGlmICghcmVzcG9uc2Uuc3RhdHVzIHx8ICF2YWxpZGF0ZVN0YXR1cyB8fCB2YWxpZGF0ZVN0YXR1cyhyZXNwb25zZS5zdGF0dXMpKSB7XG4gICAgcmVzb2x2ZShyZXNwb25zZSk7XG4gIH0gZWxzZSB7XG4gICAgcmVqZWN0KG5ldyBBeGlvc0Vycm9yKFxuICAgICAgJ1JlcXVlc3QgZmFpbGVkIHdpdGggc3RhdHVzIGNvZGUgJyArIHJlc3BvbnNlLnN0YXR1cyxcbiAgICAgIFtBeGlvc0Vycm9yLkVSUl9CQURfUkVRVUVTVCwgQXhpb3NFcnJvci5FUlJfQkFEX1JFU1BPTlNFXVtNYXRoLmZsb29yKHJlc3BvbnNlLnN0YXR1cyAvIDEwMCkgLSA0XSxcbiAgICAgIHJlc3BvbnNlLmNvbmZpZyxcbiAgICAgIHJlc3BvbnNlLnJlcXVlc3QsXG4gICAgICByZXNwb25zZVxuICAgICkpO1xuICB9XG59XG5cbi8qKlxuICogRGV0ZXJtaW5lcyB3aGV0aGVyIHRoZSBzcGVjaWZpZWQgVVJMIGlzIGFic29sdXRlXG4gKlxuICogQHBhcmFtIHtzdHJpbmd9IHVybCBUaGUgVVJMIHRvIHRlc3RcbiAqXG4gKiBAcmV0dXJucyB7Ym9vbGVhbn0gVHJ1ZSBpZiB0aGUgc3BlY2lmaWVkIFVSTCBpcyBhYnNvbHV0ZSwgb3RoZXJ3aXNlIGZhbHNlXG4gKi9cbmZ1bmN0aW9uIGlzQWJzb2x1dGVVUkwodXJsKSB7XG4gIC8vIEEgVVJMIGlzIGNvbnNpZGVyZWQgYWJzb2x1dGUgaWYgaXQgYmVnaW5zIHdpdGggXCI8c2NoZW1lPjovL1wiIG9yIFwiLy9cIiAocHJvdG9jb2wtcmVsYXRpdmUgVVJMKS5cbiAgLy8gUkZDIDM5ODYgZGVmaW5lcyBzY2hlbWUgbmFtZSBhcyBhIHNlcXVlbmNlIG9mIGNoYXJhY3RlcnMgYmVnaW5uaW5nIHdpdGggYSBsZXR0ZXIgYW5kIGZvbGxvd2VkXG4gIC8vIGJ5IGFueSBjb21iaW5hdGlvbiBvZiBsZXR0ZXJzLCBkaWdpdHMsIHBsdXMsIHBlcmlvZCwgb3IgaHlwaGVuLlxuICByZXR1cm4gL14oW2Etel1bYS16XFxkK1xcLS5dKjopP1xcL1xcLy9pLnRlc3QodXJsKTtcbn1cblxuLyoqXG4gKiBDcmVhdGVzIGEgbmV3IFVSTCBieSBjb21iaW5pbmcgdGhlIHNwZWNpZmllZCBVUkxzXG4gKlxuICogQHBhcmFtIHtzdHJpbmd9IGJhc2VVUkwgVGhlIGJhc2UgVVJMXG4gKiBAcGFyYW0ge3N0cmluZ30gcmVsYXRpdmVVUkwgVGhlIHJlbGF0aXZlIFVSTFxuICpcbiAqIEByZXR1cm5zIHtzdHJpbmd9IFRoZSBjb21iaW5lZCBVUkxcbiAqL1xuZnVuY3Rpb24gY29tYmluZVVSTHMoYmFzZVVSTCwgcmVsYXRpdmVVUkwpIHtcbiAgcmV0dXJuIHJlbGF0aXZlVVJMXG4gICAgPyBiYXNlVVJMLnJlcGxhY2UoL1xcLyskLywgJycpICsgJy8nICsgcmVsYXRpdmVVUkwucmVwbGFjZSgvXlxcLysvLCAnJylcbiAgICA6IGJhc2VVUkw7XG59XG5cbi8qKlxuICogQ3JlYXRlcyBhIG5ldyBVUkwgYnkgY29tYmluaW5nIHRoZSBiYXNlVVJMIHdpdGggdGhlIHJlcXVlc3RlZFVSTCxcbiAqIG9ubHkgd2hlbiB0aGUgcmVxdWVzdGVkVVJMIGlzIG5vdCBhbHJlYWR5IGFuIGFic29sdXRlIFVSTC5cbiAqIElmIHRoZSByZXF1ZXN0VVJMIGlzIGFic29sdXRlLCB0aGlzIGZ1bmN0aW9uIHJldHVybnMgdGhlIHJlcXVlc3RlZFVSTCB1bnRvdWNoZWQuXG4gKlxuICogQHBhcmFtIHtzdHJpbmd9IGJhc2VVUkwgVGhlIGJhc2UgVVJMXG4gKiBAcGFyYW0ge3N0cmluZ30gcmVxdWVzdGVkVVJMIEFic29sdXRlIG9yIHJlbGF0aXZlIFVSTCB0byBjb21iaW5lXG4gKlxuICogQHJldHVybnMge3N0cmluZ30gVGhlIGNvbWJpbmVkIGZ1bGwgcGF0aFxuICovXG5mdW5jdGlvbiBidWlsZEZ1bGxQYXRoKGJhc2VVUkwsIHJlcXVlc3RlZFVSTCkge1xuICBpZiAoYmFzZVVSTCAmJiAhaXNBYnNvbHV0ZVVSTChyZXF1ZXN0ZWRVUkwpKSB7XG4gICAgcmV0dXJuIGNvbWJpbmVVUkxzKGJhc2VVUkwsIHJlcXVlc3RlZFVSTCk7XG4gIH1cbiAgcmV0dXJuIHJlcXVlc3RlZFVSTDtcbn1cblxuY29uc3QgVkVSU0lPTiA9IFwiMS42LjBcIjtcblxuZnVuY3Rpb24gcGFyc2VQcm90b2NvbCh1cmwpIHtcbiAgY29uc3QgbWF0Y2ggPSAvXihbLStcXHddezEsMjV9KSg6P1xcL1xcL3w6KS8uZXhlYyh1cmwpO1xuICByZXR1cm4gbWF0Y2ggJiYgbWF0Y2hbMV0gfHwgJyc7XG59XG5cbmNvbnN0IERBVEFfVVJMX1BBVFRFUk4gPSAvXig/OihbXjtdKyk7KT8oPzpbXjtdKzspPyhiYXNlNjR8KSwoW1xcc1xcU10qKSQvO1xuXG4vKipcbiAqIFBhcnNlIGRhdGEgdXJpIHRvIGEgQnVmZmVyIG9yIEJsb2JcbiAqXG4gKiBAcGFyYW0ge1N0cmluZ30gdXJpXG4gKiBAcGFyYW0gez9Cb29sZWFufSBhc0Jsb2JcbiAqIEBwYXJhbSB7P09iamVjdH0gb3B0aW9uc1xuICogQHBhcmFtIHs/RnVuY3Rpb259IG9wdGlvbnMuQmxvYlxuICpcbiAqIEByZXR1cm5zIHtCdWZmZXJ8QmxvYn1cbiAqL1xuZnVuY3Rpb24gZnJvbURhdGFVUkkodXJpLCBhc0Jsb2IsIG9wdGlvbnMpIHtcbiAgY29uc3QgX0Jsb2IgPSBvcHRpb25zICYmIG9wdGlvbnMuQmxvYiB8fCBwbGF0Zm9ybS5jbGFzc2VzLkJsb2I7XG4gIGNvbnN0IHByb3RvY29sID0gcGFyc2VQcm90b2NvbCh1cmkpO1xuXG4gIGlmIChhc0Jsb2IgPT09IHVuZGVmaW5lZCAmJiBfQmxvYikge1xuICAgIGFzQmxvYiA9IHRydWU7XG4gIH1cblxuICBpZiAocHJvdG9jb2wgPT09ICdkYXRhJykge1xuICAgIHVyaSA9IHByb3RvY29sLmxlbmd0aCA/IHVyaS5zbGljZShwcm90b2NvbC5sZW5ndGggKyAxKSA6IHVyaTtcblxuICAgIGNvbnN0IG1hdGNoID0gREFUQV9VUkxfUEFUVEVSTi5leGVjKHVyaSk7XG5cbiAgICBpZiAoIW1hdGNoKSB7XG4gICAgICB0aHJvdyBuZXcgQXhpb3NFcnJvcignSW52YWxpZCBVUkwnLCBBeGlvc0Vycm9yLkVSUl9JTlZBTElEX1VSTCk7XG4gICAgfVxuXG4gICAgY29uc3QgbWltZSA9IG1hdGNoWzFdO1xuICAgIGNvbnN0IGlzQmFzZTY0ID0gbWF0Y2hbMl07XG4gICAgY29uc3QgYm9keSA9IG1hdGNoWzNdO1xuICAgIGNvbnN0IGJ1ZmZlciA9IEJ1ZmZlci5mcm9tKGRlY29kZVVSSUNvbXBvbmVudChib2R5KSwgaXNCYXNlNjQgPyAnYmFzZTY0JyA6ICd1dGY4Jyk7XG5cbiAgICBpZiAoYXNCbG9iKSB7XG4gICAgICBpZiAoIV9CbG9iKSB7XG4gICAgICAgIHRocm93IG5ldyBBeGlvc0Vycm9yKCdCbG9iIGlzIG5vdCBzdXBwb3J0ZWQnLCBBeGlvc0Vycm9yLkVSUl9OT1RfU1VQUE9SVCk7XG4gICAgICB9XG5cbiAgICAgIHJldHVybiBuZXcgX0Jsb2IoW2J1ZmZlcl0sIHt0eXBlOiBtaW1lfSk7XG4gICAgfVxuXG4gICAgcmV0dXJuIGJ1ZmZlcjtcbiAgfVxuXG4gIHRocm93IG5ldyBBeGlvc0Vycm9yKCdVbnN1cHBvcnRlZCBwcm90b2NvbCAnICsgcHJvdG9jb2wsIEF4aW9zRXJyb3IuRVJSX05PVF9TVVBQT1JUKTtcbn1cblxuLyoqXG4gKiBUaHJvdHRsZSBkZWNvcmF0b3JcbiAqIEBwYXJhbSB7RnVuY3Rpb259IGZuXG4gKiBAcGFyYW0ge051bWJlcn0gZnJlcVxuICogQHJldHVybiB7RnVuY3Rpb259XG4gKi9cbmZ1bmN0aW9uIHRocm90dGxlKGZuLCBmcmVxKSB7XG4gIGxldCB0aW1lc3RhbXAgPSAwO1xuICBjb25zdCB0aHJlc2hvbGQgPSAxMDAwIC8gZnJlcTtcbiAgbGV0IHRpbWVyID0gbnVsbDtcbiAgcmV0dXJuIGZ1bmN0aW9uIHRocm90dGxlZChmb3JjZSwgYXJncykge1xuICAgIGNvbnN0IG5vdyA9IERhdGUubm93KCk7XG4gICAgaWYgKGZvcmNlIHx8IG5vdyAtIHRpbWVzdGFtcCA+IHRocmVzaG9sZCkge1xuICAgICAgaWYgKHRpbWVyKSB7XG4gICAgICAgIGNsZWFyVGltZW91dCh0aW1lcik7XG4gICAgICAgIHRpbWVyID0gbnVsbDtcbiAgICAgIH1cbiAgICAgIHRpbWVzdGFtcCA9IG5vdztcbiAgICAgIHJldHVybiBmbi5hcHBseShudWxsLCBhcmdzKTtcbiAgICB9XG4gICAgaWYgKCF0aW1lcikge1xuICAgICAgdGltZXIgPSBzZXRUaW1lb3V0KCgpID0+IHtcbiAgICAgICAgdGltZXIgPSBudWxsO1xuICAgICAgICB0aW1lc3RhbXAgPSBEYXRlLm5vdygpO1xuICAgICAgICByZXR1cm4gZm4uYXBwbHkobnVsbCwgYXJncyk7XG4gICAgICB9LCB0aHJlc2hvbGQgLSAobm93IC0gdGltZXN0YW1wKSk7XG4gICAgfVxuICB9O1xufVxuXG4vKipcbiAqIENhbGN1bGF0ZSBkYXRhIG1heFJhdGVcbiAqIEBwYXJhbSB7TnVtYmVyfSBbc2FtcGxlc0NvdW50PSAxMF1cbiAqIEBwYXJhbSB7TnVtYmVyfSBbbWluPSAxMDAwXVxuICogQHJldHVybnMge0Z1bmN0aW9ufVxuICovXG5mdW5jdGlvbiBzcGVlZG9tZXRlcihzYW1wbGVzQ291bnQsIG1pbikge1xuICBzYW1wbGVzQ291bnQgPSBzYW1wbGVzQ291bnQgfHwgMTA7XG4gIGNvbnN0IGJ5dGVzID0gbmV3IEFycmF5KHNhbXBsZXNDb3VudCk7XG4gIGNvbnN0IHRpbWVzdGFtcHMgPSBuZXcgQXJyYXkoc2FtcGxlc0NvdW50KTtcbiAgbGV0IGhlYWQgPSAwO1xuICBsZXQgdGFpbCA9IDA7XG4gIGxldCBmaXJzdFNhbXBsZVRTO1xuXG4gIG1pbiA9IG1pbiAhPT0gdW5kZWZpbmVkID8gbWluIDogMTAwMDtcblxuICByZXR1cm4gZnVuY3Rpb24gcHVzaChjaHVua0xlbmd0aCkge1xuICAgIGNvbnN0IG5vdyA9IERhdGUubm93KCk7XG5cbiAgICBjb25zdCBzdGFydGVkQXQgPSB0aW1lc3RhbXBzW3RhaWxdO1xuXG4gICAgaWYgKCFmaXJzdFNhbXBsZVRTKSB7XG4gICAgICBmaXJzdFNhbXBsZVRTID0gbm93O1xuICAgIH1cblxuICAgIGJ5dGVzW2hlYWRdID0gY2h1bmtMZW5ndGg7XG4gICAgdGltZXN0YW1wc1toZWFkXSA9IG5vdztcblxuICAgIGxldCBpID0gdGFpbDtcbiAgICBsZXQgYnl0ZXNDb3VudCA9IDA7XG5cbiAgICB3aGlsZSAoaSAhPT0gaGVhZCkge1xuICAgICAgYnl0ZXNDb3VudCArPSBieXRlc1tpKytdO1xuICAgICAgaSA9IGkgJSBzYW1wbGVzQ291bnQ7XG4gICAgfVxuXG4gICAgaGVhZCA9IChoZWFkICsgMSkgJSBzYW1wbGVzQ291bnQ7XG5cbiAgICBpZiAoaGVhZCA9PT0gdGFpbCkge1xuICAgICAgdGFpbCA9ICh0YWlsICsgMSkgJSBzYW1wbGVzQ291bnQ7XG4gICAgfVxuXG4gICAgaWYgKG5vdyAtIGZpcnN0U2FtcGxlVFMgPCBtaW4pIHtcbiAgICAgIHJldHVybjtcbiAgICB9XG5cbiAgICBjb25zdCBwYXNzZWQgPSBzdGFydGVkQXQgJiYgbm93IC0gc3RhcnRlZEF0O1xuXG4gICAgcmV0dXJuIHBhc3NlZCA/IE1hdGgucm91bmQoYnl0ZXNDb3VudCAqIDEwMDAgLyBwYXNzZWQpIDogdW5kZWZpbmVkO1xuICB9O1xufVxuXG5jb25zdCBrSW50ZXJuYWxzID0gU3ltYm9sKCdpbnRlcm5hbHMnKTtcblxuY2xhc3MgQXhpb3NUcmFuc2Zvcm1TdHJlYW0gZXh0ZW5kcyBzdHJlYW1fX2RlZmF1bHRbXCJkZWZhdWx0XCJdLlRyYW5zZm9ybXtcbiAgY29uc3RydWN0b3Iob3B0aW9ucykge1xuICAgIG9wdGlvbnMgPSB1dGlscy50b0ZsYXRPYmplY3Qob3B0aW9ucywge1xuICAgICAgbWF4UmF0ZTogMCxcbiAgICAgIGNodW5rU2l6ZTogNjQgKiAxMDI0LFxuICAgICAgbWluQ2h1bmtTaXplOiAxMDAsXG4gICAgICB0aW1lV2luZG93OiA1MDAsXG4gICAgICB0aWNrc1JhdGU6IDIsXG4gICAgICBzYW1wbGVzQ291bnQ6IDE1XG4gICAgfSwgbnVsbCwgKHByb3AsIHNvdXJjZSkgPT4ge1xuICAgICAgcmV0dXJuICF1dGlscy5pc1VuZGVmaW5lZChzb3VyY2VbcHJvcF0pO1xuICAgIH0pO1xuXG4gICAgc3VwZXIoe1xuICAgICAgcmVhZGFibGVIaWdoV2F0ZXJNYXJrOiBvcHRpb25zLmNodW5rU2l6ZVxuICAgIH0pO1xuXG4gICAgY29uc3Qgc2VsZiA9IHRoaXM7XG5cbiAgICBjb25zdCBpbnRlcm5hbHMgPSB0aGlzW2tJbnRlcm5hbHNdID0ge1xuICAgICAgbGVuZ3RoOiBvcHRpb25zLmxlbmd0aCxcbiAgICAgIHRpbWVXaW5kb3c6IG9wdGlvbnMudGltZVdpbmRvdyxcbiAgICAgIHRpY2tzUmF0ZTogb3B0aW9ucy50aWNrc1JhdGUsXG4gICAgICBjaHVua1NpemU6IG9wdGlvbnMuY2h1bmtTaXplLFxuICAgICAgbWF4UmF0ZTogb3B0aW9ucy5tYXhSYXRlLFxuICAgICAgbWluQ2h1bmtTaXplOiBvcHRpb25zLm1pbkNodW5rU2l6ZSxcbiAgICAgIGJ5dGVzU2VlbjogMCxcbiAgICAgIGlzQ2FwdHVyZWQ6IGZhbHNlLFxuICAgICAgbm90aWZpZWRCeXRlc0xvYWRlZDogMCxcbiAgICAgIHRzOiBEYXRlLm5vdygpLFxuICAgICAgYnl0ZXM6IDAsXG4gICAgICBvblJlYWRDYWxsYmFjazogbnVsbFxuICAgIH07XG5cbiAgICBjb25zdCBfc3BlZWRvbWV0ZXIgPSBzcGVlZG9tZXRlcihpbnRlcm5hbHMudGlja3NSYXRlICogb3B0aW9ucy5zYW1wbGVzQ291bnQsIGludGVybmFscy50aW1lV2luZG93KTtcblxuICAgIHRoaXMub24oJ25ld0xpc3RlbmVyJywgZXZlbnQgPT4ge1xuICAgICAgaWYgKGV2ZW50ID09PSAncHJvZ3Jlc3MnKSB7XG4gICAgICAgIGlmICghaW50ZXJuYWxzLmlzQ2FwdHVyZWQpIHtcbiAgICAgICAgICBpbnRlcm5hbHMuaXNDYXB0dXJlZCA9IHRydWU7XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9KTtcblxuICAgIGxldCBieXRlc05vdGlmaWVkID0gMDtcblxuICAgIGludGVybmFscy51cGRhdGVQcm9ncmVzcyA9IHRocm90dGxlKGZ1bmN0aW9uIHRocm90dGxlZEhhbmRsZXIoKSB7XG4gICAgICBjb25zdCB0b3RhbEJ5dGVzID0gaW50ZXJuYWxzLmxlbmd0aDtcbiAgICAgIGNvbnN0IGJ5dGVzVHJhbnNmZXJyZWQgPSBpbnRlcm5hbHMuYnl0ZXNTZWVuO1xuICAgICAgY29uc3QgcHJvZ3Jlc3NCeXRlcyA9IGJ5dGVzVHJhbnNmZXJyZWQgLSBieXRlc05vdGlmaWVkO1xuICAgICAgaWYgKCFwcm9ncmVzc0J5dGVzIHx8IHNlbGYuZGVzdHJveWVkKSByZXR1cm47XG5cbiAgICAgIGNvbnN0IHJhdGUgPSBfc3BlZWRvbWV0ZXIocHJvZ3Jlc3NCeXRlcyk7XG5cbiAgICAgIGJ5dGVzTm90aWZpZWQgPSBieXRlc1RyYW5zZmVycmVkO1xuXG4gICAgICBwcm9jZXNzLm5leHRUaWNrKCgpID0+IHtcbiAgICAgICAgc2VsZi5lbWl0KCdwcm9ncmVzcycsIHtcbiAgICAgICAgICAnbG9hZGVkJzogYnl0ZXNUcmFuc2ZlcnJlZCxcbiAgICAgICAgICAndG90YWwnOiB0b3RhbEJ5dGVzLFxuICAgICAgICAgICdwcm9ncmVzcyc6IHRvdGFsQnl0ZXMgPyAoYnl0ZXNUcmFuc2ZlcnJlZCAvIHRvdGFsQnl0ZXMpIDogdW5kZWZpbmVkLFxuICAgICAgICAgICdieXRlcyc6IHByb2dyZXNzQnl0ZXMsXG4gICAgICAgICAgJ3JhdGUnOiByYXRlID8gcmF0ZSA6IHVuZGVmaW5lZCxcbiAgICAgICAgICAnZXN0aW1hdGVkJzogcmF0ZSAmJiB0b3RhbEJ5dGVzICYmIGJ5dGVzVHJhbnNmZXJyZWQgPD0gdG90YWxCeXRlcyA/XG4gICAgICAgICAgICAodG90YWxCeXRlcyAtIGJ5dGVzVHJhbnNmZXJyZWQpIC8gcmF0ZSA6IHVuZGVmaW5lZFxuICAgICAgICB9KTtcbiAgICAgIH0pO1xuICAgIH0sIGludGVybmFscy50aWNrc1JhdGUpO1xuXG4gICAgY29uc3Qgb25GaW5pc2ggPSAoKSA9PiB7XG4gICAgICBpbnRlcm5hbHMudXBkYXRlUHJvZ3Jlc3ModHJ1ZSk7XG4gICAgfTtcblxuICAgIHRoaXMub25jZSgnZW5kJywgb25GaW5pc2gpO1xuICAgIHRoaXMub25jZSgnZXJyb3InLCBvbkZpbmlzaCk7XG4gIH1cblxuICBfcmVhZChzaXplKSB7XG4gICAgY29uc3QgaW50ZXJuYWxzID0gdGhpc1trSW50ZXJuYWxzXTtcblxuICAgIGlmIChpbnRlcm5hbHMub25SZWFkQ2FsbGJhY2spIHtcbiAgICAgIGludGVybmFscy5vblJlYWRDYWxsYmFjaygpO1xuICAgIH1cblxuICAgIHJldHVybiBzdXBlci5fcmVhZChzaXplKTtcbiAgfVxuXG4gIF90cmFuc2Zvcm0oY2h1bmssIGVuY29kaW5nLCBjYWxsYmFjaykge1xuICAgIGNvbnN0IHNlbGYgPSB0aGlzO1xuICAgIGNvbnN0IGludGVybmFscyA9IHRoaXNba0ludGVybmFsc107XG4gICAgY29uc3QgbWF4UmF0ZSA9IGludGVybmFscy5tYXhSYXRlO1xuXG4gICAgY29uc3QgcmVhZGFibGVIaWdoV2F0ZXJNYXJrID0gdGhpcy5yZWFkYWJsZUhpZ2hXYXRlck1hcms7XG5cbiAgICBjb25zdCB0aW1lV2luZG93ID0gaW50ZXJuYWxzLnRpbWVXaW5kb3c7XG5cbiAgICBjb25zdCBkaXZpZGVyID0gMTAwMCAvIHRpbWVXaW5kb3c7XG4gICAgY29uc3QgYnl0ZXNUaHJlc2hvbGQgPSAobWF4UmF0ZSAvIGRpdmlkZXIpO1xuICAgIGNvbnN0IG1pbkNodW5rU2l6ZSA9IGludGVybmFscy5taW5DaHVua1NpemUgIT09IGZhbHNlID8gTWF0aC5tYXgoaW50ZXJuYWxzLm1pbkNodW5rU2l6ZSwgYnl0ZXNUaHJlc2hvbGQgKiAwLjAxKSA6IDA7XG5cbiAgICBmdW5jdGlvbiBwdXNoQ2h1bmsoX2NodW5rLCBfY2FsbGJhY2spIHtcbiAgICAgIGNvbnN0IGJ5dGVzID0gQnVmZmVyLmJ5dGVMZW5ndGgoX2NodW5rKTtcbiAgICAgIGludGVybmFscy5ieXRlc1NlZW4gKz0gYnl0ZXM7XG4gICAgICBpbnRlcm5hbHMuYnl0ZXMgKz0gYnl0ZXM7XG5cbiAgICAgIGlmIChpbnRlcm5hbHMuaXNDYXB0dXJlZCkge1xuICAgICAgICBpbnRlcm5hbHMudXBkYXRlUHJvZ3Jlc3MoKTtcbiAgICAgIH1cblxuICAgICAgaWYgKHNlbGYucHVzaChfY2h1bmspKSB7XG4gICAgICAgIHByb2Nlc3MubmV4dFRpY2soX2NhbGxiYWNrKTtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIGludGVybmFscy5vblJlYWRDYWxsYmFjayA9ICgpID0+IHtcbiAgICAgICAgICBpbnRlcm5hbHMub25SZWFkQ2FsbGJhY2sgPSBudWxsO1xuICAgICAgICAgIHByb2Nlc3MubmV4dFRpY2soX2NhbGxiYWNrKTtcbiAgICAgICAgfTtcbiAgICAgIH1cbiAgICB9XG5cbiAgICBjb25zdCB0cmFuc2Zvcm1DaHVuayA9IChfY2h1bmssIF9jYWxsYmFjaykgPT4ge1xuICAgICAgY29uc3QgY2h1bmtTaXplID0gQnVmZmVyLmJ5dGVMZW5ndGgoX2NodW5rKTtcbiAgICAgIGxldCBjaHVua1JlbWFpbmRlciA9IG51bGw7XG4gICAgICBsZXQgbWF4Q2h1bmtTaXplID0gcmVhZGFibGVIaWdoV2F0ZXJNYXJrO1xuICAgICAgbGV0IGJ5dGVzTGVmdDtcbiAgICAgIGxldCBwYXNzZWQgPSAwO1xuXG4gICAgICBpZiAobWF4UmF0ZSkge1xuICAgICAgICBjb25zdCBub3cgPSBEYXRlLm5vdygpO1xuXG4gICAgICAgIGlmICghaW50ZXJuYWxzLnRzIHx8IChwYXNzZWQgPSAobm93IC0gaW50ZXJuYWxzLnRzKSkgPj0gdGltZVdpbmRvdykge1xuICAgICAgICAgIGludGVybmFscy50cyA9IG5vdztcbiAgICAgICAgICBieXRlc0xlZnQgPSBieXRlc1RocmVzaG9sZCAtIGludGVybmFscy5ieXRlcztcbiAgICAgICAgICBpbnRlcm5hbHMuYnl0ZXMgPSBieXRlc0xlZnQgPCAwID8gLWJ5dGVzTGVmdCA6IDA7XG4gICAgICAgICAgcGFzc2VkID0gMDtcbiAgICAgICAgfVxuXG4gICAgICAgIGJ5dGVzTGVmdCA9IGJ5dGVzVGhyZXNob2xkIC0gaW50ZXJuYWxzLmJ5dGVzO1xuICAgICAgfVxuXG4gICAgICBpZiAobWF4UmF0ZSkge1xuICAgICAgICBpZiAoYnl0ZXNMZWZ0IDw9IDApIHtcbiAgICAgICAgICAvLyBuZXh0IHRpbWUgd2luZG93XG4gICAgICAgICAgcmV0dXJuIHNldFRpbWVvdXQoKCkgPT4ge1xuICAgICAgICAgICAgX2NhbGxiYWNrKG51bGwsIF9jaHVuayk7XG4gICAgICAgICAgfSwgdGltZVdpbmRvdyAtIHBhc3NlZCk7XG4gICAgICAgIH1cblxuICAgICAgICBpZiAoYnl0ZXNMZWZ0IDwgbWF4Q2h1bmtTaXplKSB7XG4gICAgICAgICAgbWF4Q2h1bmtTaXplID0gYnl0ZXNMZWZ0O1xuICAgICAgICB9XG4gICAgICB9XG5cbiAgICAgIGlmIChtYXhDaHVua1NpemUgJiYgY2h1bmtTaXplID4gbWF4Q2h1bmtTaXplICYmIChjaHVua1NpemUgLSBtYXhDaHVua1NpemUpID4gbWluQ2h1bmtTaXplKSB7XG4gICAgICAgIGNodW5rUmVtYWluZGVyID0gX2NodW5rLnN1YmFycmF5KG1heENodW5rU2l6ZSk7XG4gICAgICAgIF9jaHVuayA9IF9jaHVuay5zdWJhcnJheSgwLCBtYXhDaHVua1NpemUpO1xuICAgICAgfVxuXG4gICAgICBwdXNoQ2h1bmsoX2NodW5rLCBjaHVua1JlbWFpbmRlciA/ICgpID0+IHtcbiAgICAgICAgcHJvY2Vzcy5uZXh0VGljayhfY2FsbGJhY2ssIG51bGwsIGNodW5rUmVtYWluZGVyKTtcbiAgICAgIH0gOiBfY2FsbGJhY2spO1xuICAgIH07XG5cbiAgICB0cmFuc2Zvcm1DaHVuayhjaHVuaywgZnVuY3Rpb24gdHJhbnNmb3JtTmV4dENodW5rKGVyciwgX2NodW5rKSB7XG4gICAgICBpZiAoZXJyKSB7XG4gICAgICAgIHJldHVybiBjYWxsYmFjayhlcnIpO1xuICAgICAgfVxuXG4gICAgICBpZiAoX2NodW5rKSB7XG4gICAgICAgIHRyYW5zZm9ybUNodW5rKF9jaHVuaywgdHJhbnNmb3JtTmV4dENodW5rKTtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIGNhbGxiYWNrKG51bGwpO1xuICAgICAgfVxuICAgIH0pO1xuICB9XG5cbiAgc2V0TGVuZ3RoKGxlbmd0aCkge1xuICAgIHRoaXNba0ludGVybmFsc10ubGVuZ3RoID0gK2xlbmd0aDtcbiAgICByZXR1cm4gdGhpcztcbiAgfVxufVxuXG5jb25zdCBBeGlvc1RyYW5zZm9ybVN0cmVhbSQxID0gQXhpb3NUcmFuc2Zvcm1TdHJlYW07XG5cbmNvbnN0IHthc3luY0l0ZXJhdG9yfSA9IFN5bWJvbDtcblxuY29uc3QgcmVhZEJsb2IgPSBhc3luYyBmdW5jdGlvbiogKGJsb2IpIHtcbiAgaWYgKGJsb2Iuc3RyZWFtKSB7XG4gICAgeWllbGQqIGJsb2Iuc3RyZWFtKCk7XG4gIH0gZWxzZSBpZiAoYmxvYi5hcnJheUJ1ZmZlcikge1xuICAgIHlpZWxkIGF3YWl0IGJsb2IuYXJyYXlCdWZmZXIoKTtcbiAgfSBlbHNlIGlmIChibG9iW2FzeW5jSXRlcmF0b3JdKSB7XG4gICAgeWllbGQqIGJsb2JbYXN5bmNJdGVyYXRvcl0oKTtcbiAgfSBlbHNlIHtcbiAgICB5aWVsZCBibG9iO1xuICB9XG59O1xuXG5jb25zdCByZWFkQmxvYiQxID0gcmVhZEJsb2I7XG5cbmNvbnN0IEJPVU5EQVJZX0FMUEhBQkVUID0gdXRpbHMuQUxQSEFCRVQuQUxQSEFfRElHSVQgKyAnLV8nO1xuXG5jb25zdCB0ZXh0RW5jb2RlciA9IG5ldyB1dGlsLlRleHRFbmNvZGVyKCk7XG5cbmNvbnN0IENSTEYgPSAnXFxyXFxuJztcbmNvbnN0IENSTEZfQllURVMgPSB0ZXh0RW5jb2Rlci5lbmNvZGUoQ1JMRik7XG5jb25zdCBDUkxGX0JZVEVTX0NPVU5UID0gMjtcblxuY2xhc3MgRm9ybURhdGFQYXJ0IHtcbiAgY29uc3RydWN0b3IobmFtZSwgdmFsdWUpIHtcbiAgICBjb25zdCB7ZXNjYXBlTmFtZX0gPSB0aGlzLmNvbnN0cnVjdG9yO1xuICAgIGNvbnN0IGlzU3RyaW5nVmFsdWUgPSB1dGlscy5pc1N0cmluZyh2YWx1ZSk7XG5cbiAgICBsZXQgaGVhZGVycyA9IGBDb250ZW50LURpc3Bvc2l0aW9uOiBmb3JtLWRhdGE7IG5hbWU9XCIke2VzY2FwZU5hbWUobmFtZSl9XCIke1xuICAgICAgIWlzU3RyaW5nVmFsdWUgJiYgdmFsdWUubmFtZSA/IGA7IGZpbGVuYW1lPVwiJHtlc2NhcGVOYW1lKHZhbHVlLm5hbWUpfVwiYCA6ICcnXG4gICAgfSR7Q1JMRn1gO1xuXG4gICAgaWYgKGlzU3RyaW5nVmFsdWUpIHtcbiAgICAgIHZhbHVlID0gdGV4dEVuY29kZXIuZW5jb2RlKFN0cmluZyh2YWx1ZSkucmVwbGFjZSgvXFxyP1xcbnxcXHJcXG4/L2csIENSTEYpKTtcbiAgICB9IGVsc2Uge1xuICAgICAgaGVhZGVycyArPSBgQ29udGVudC1UeXBlOiAke3ZhbHVlLnR5cGUgfHwgXCJhcHBsaWNhdGlvbi9vY3RldC1zdHJlYW1cIn0ke0NSTEZ9YDtcbiAgICB9XG5cbiAgICB0aGlzLmhlYWRlcnMgPSB0ZXh0RW5jb2Rlci5lbmNvZGUoaGVhZGVycyArIENSTEYpO1xuXG4gICAgdGhpcy5jb250ZW50TGVuZ3RoID0gaXNTdHJpbmdWYWx1ZSA/IHZhbHVlLmJ5dGVMZW5ndGggOiB2YWx1ZS5zaXplO1xuXG4gICAgdGhpcy5zaXplID0gdGhpcy5oZWFkZXJzLmJ5dGVMZW5ndGggKyB0aGlzLmNvbnRlbnRMZW5ndGggKyBDUkxGX0JZVEVTX0NPVU5UO1xuXG4gICAgdGhpcy5uYW1lID0gbmFtZTtcbiAgICB0aGlzLnZhbHVlID0gdmFsdWU7XG4gIH1cblxuICBhc3luYyAqZW5jb2RlKCl7XG4gICAgeWllbGQgdGhpcy5oZWFkZXJzO1xuXG4gICAgY29uc3Qge3ZhbHVlfSA9IHRoaXM7XG5cbiAgICBpZih1dGlscy5pc1R5cGVkQXJyYXkodmFsdWUpKSB7XG4gICAgICB5aWVsZCB2YWx1ZTtcbiAgICB9IGVsc2Uge1xuICAgICAgeWllbGQqIHJlYWRCbG9iJDEodmFsdWUpO1xuICAgIH1cblxuICAgIHlpZWxkIENSTEZfQllURVM7XG4gIH1cblxuICBzdGF0aWMgZXNjYXBlTmFtZShuYW1lKSB7XG4gICAgICByZXR1cm4gU3RyaW5nKG5hbWUpLnJlcGxhY2UoL1tcXHJcXG5cIl0vZywgKG1hdGNoKSA9PiAoe1xuICAgICAgICAnXFxyJyA6ICclMEQnLFxuICAgICAgICAnXFxuJyA6ICclMEEnLFxuICAgICAgICAnXCInIDogJyUyMicsXG4gICAgICB9W21hdGNoXSkpO1xuICB9XG59XG5cbmNvbnN0IGZvcm1EYXRhVG9TdHJlYW0gPSAoZm9ybSwgaGVhZGVyc0hhbmRsZXIsIG9wdGlvbnMpID0+IHtcbiAgY29uc3Qge1xuICAgIHRhZyA9ICdmb3JtLWRhdGEtYm91bmRhcnknLFxuICAgIHNpemUgPSAyNSxcbiAgICBib3VuZGFyeSA9IHRhZyArICctJyArIHV0aWxzLmdlbmVyYXRlU3RyaW5nKHNpemUsIEJPVU5EQVJZX0FMUEhBQkVUKVxuICB9ID0gb3B0aW9ucyB8fCB7fTtcblxuICBpZighdXRpbHMuaXNGb3JtRGF0YShmb3JtKSkge1xuICAgIHRocm93IFR5cGVFcnJvcignRm9ybURhdGEgaW5zdGFuY2UgcmVxdWlyZWQnKTtcbiAgfVxuXG4gIGlmIChib3VuZGFyeS5sZW5ndGggPCAxIHx8IGJvdW5kYXJ5Lmxlbmd0aCA+IDcwKSB7XG4gICAgdGhyb3cgRXJyb3IoJ2JvdW5kYXJ5IG11c3QgYmUgMTAtNzAgY2hhcmFjdGVycyBsb25nJylcbiAgfVxuXG4gIGNvbnN0IGJvdW5kYXJ5Qnl0ZXMgPSB0ZXh0RW5jb2Rlci5lbmNvZGUoJy0tJyArIGJvdW5kYXJ5ICsgQ1JMRik7XG4gIGNvbnN0IGZvb3RlckJ5dGVzID0gdGV4dEVuY29kZXIuZW5jb2RlKCctLScgKyBib3VuZGFyeSArICctLScgKyBDUkxGICsgQ1JMRik7XG4gIGxldCBjb250ZW50TGVuZ3RoID0gZm9vdGVyQnl0ZXMuYnl0ZUxlbmd0aDtcblxuICBjb25zdCBwYXJ0cyA9IEFycmF5LmZyb20oZm9ybS5lbnRyaWVzKCkpLm1hcCgoW25hbWUsIHZhbHVlXSkgPT4ge1xuICAgIGNvbnN0IHBhcnQgPSBuZXcgRm9ybURhdGFQYXJ0KG5hbWUsIHZhbHVlKTtcbiAgICBjb250ZW50TGVuZ3RoICs9IHBhcnQuc2l6ZTtcbiAgICByZXR1cm4gcGFydDtcbiAgfSk7XG5cbiAgY29udGVudExlbmd0aCArPSBib3VuZGFyeUJ5dGVzLmJ5dGVMZW5ndGggKiBwYXJ0cy5sZW5ndGg7XG5cbiAgY29udGVudExlbmd0aCA9IHV0aWxzLnRvRmluaXRlTnVtYmVyKGNvbnRlbnRMZW5ndGgpO1xuXG4gIGNvbnN0IGNvbXB1dGVkSGVhZGVycyA9IHtcbiAgICAnQ29udGVudC1UeXBlJzogYG11bHRpcGFydC9mb3JtLWRhdGE7IGJvdW5kYXJ5PSR7Ym91bmRhcnl9YFxuICB9O1xuXG4gIGlmIChOdW1iZXIuaXNGaW5pdGUoY29udGVudExlbmd0aCkpIHtcbiAgICBjb21wdXRlZEhlYWRlcnNbJ0NvbnRlbnQtTGVuZ3RoJ10gPSBjb250ZW50TGVuZ3RoO1xuICB9XG5cbiAgaGVhZGVyc0hhbmRsZXIgJiYgaGVhZGVyc0hhbmRsZXIoY29tcHV0ZWRIZWFkZXJzKTtcblxuICByZXR1cm4gc3RyZWFtLlJlYWRhYmxlLmZyb20oKGFzeW5jIGZ1bmN0aW9uICooKSB7XG4gICAgZm9yKGNvbnN0IHBhcnQgb2YgcGFydHMpIHtcbiAgICAgIHlpZWxkIGJvdW5kYXJ5Qnl0ZXM7XG4gICAgICB5aWVsZCogcGFydC5lbmNvZGUoKTtcbiAgICB9XG5cbiAgICB5aWVsZCBmb290ZXJCeXRlcztcbiAgfSkoKSk7XG59O1xuXG5jb25zdCBmb3JtRGF0YVRvU3RyZWFtJDEgPSBmb3JtRGF0YVRvU3RyZWFtO1xuXG5jbGFzcyBabGliSGVhZGVyVHJhbnNmb3JtU3RyZWFtIGV4dGVuZHMgc3RyZWFtX19kZWZhdWx0W1wiZGVmYXVsdFwiXS5UcmFuc2Zvcm0ge1xuICBfX3RyYW5zZm9ybShjaHVuaywgZW5jb2RpbmcsIGNhbGxiYWNrKSB7XG4gICAgdGhpcy5wdXNoKGNodW5rKTtcbiAgICBjYWxsYmFjaygpO1xuICB9XG5cbiAgX3RyYW5zZm9ybShjaHVuaywgZW5jb2RpbmcsIGNhbGxiYWNrKSB7XG4gICAgaWYgKGNodW5rLmxlbmd0aCAhPT0gMCkge1xuICAgICAgdGhpcy5fdHJhbnNmb3JtID0gdGhpcy5fX3RyYW5zZm9ybTtcblxuICAgICAgLy8gQWRkIERlZmF1bHQgQ29tcHJlc3Npb24gaGVhZGVycyBpZiBubyB6bGliIGhlYWRlcnMgYXJlIHByZXNlbnRcbiAgICAgIGlmIChjaHVua1swXSAhPT0gMTIwKSB7IC8vIEhleDogNzhcbiAgICAgICAgY29uc3QgaGVhZGVyID0gQnVmZmVyLmFsbG9jKDIpO1xuICAgICAgICBoZWFkZXJbMF0gPSAxMjA7IC8vIEhleDogNzhcbiAgICAgICAgaGVhZGVyWzFdID0gMTU2OyAvLyBIZXg6IDlDIFxuICAgICAgICB0aGlzLnB1c2goaGVhZGVyLCBlbmNvZGluZyk7XG4gICAgICB9XG4gICAgfVxuXG4gICAgdGhpcy5fX3RyYW5zZm9ybShjaHVuaywgZW5jb2RpbmcsIGNhbGxiYWNrKTtcbiAgfVxufVxuXG5jb25zdCBabGliSGVhZGVyVHJhbnNmb3JtU3RyZWFtJDEgPSBabGliSGVhZGVyVHJhbnNmb3JtU3RyZWFtO1xuXG5jb25zdCBjYWxsYmFja2lmeSA9IChmbiwgcmVkdWNlcikgPT4ge1xuICByZXR1cm4gdXRpbHMuaXNBc3luY0ZuKGZuKSA/IGZ1bmN0aW9uICguLi5hcmdzKSB7XG4gICAgY29uc3QgY2IgPSBhcmdzLnBvcCgpO1xuICAgIGZuLmFwcGx5KHRoaXMsIGFyZ3MpLnRoZW4oKHZhbHVlKSA9PiB7XG4gICAgICB0cnkge1xuICAgICAgICByZWR1Y2VyID8gY2IobnVsbCwgLi4ucmVkdWNlcih2YWx1ZSkpIDogY2IobnVsbCwgdmFsdWUpO1xuICAgICAgfSBjYXRjaCAoZXJyKSB7XG4gICAgICAgIGNiKGVycik7XG4gICAgICB9XG4gICAgfSwgY2IpO1xuICB9IDogZm47XG59O1xuXG5jb25zdCBjYWxsYmFja2lmeSQxID0gY2FsbGJhY2tpZnk7XG5cbmNvbnN0IHpsaWJPcHRpb25zID0ge1xuICBmbHVzaDogemxpYl9fZGVmYXVsdFtcImRlZmF1bHRcIl0uY29uc3RhbnRzLlpfU1lOQ19GTFVTSCxcbiAgZmluaXNoRmx1c2g6IHpsaWJfX2RlZmF1bHRbXCJkZWZhdWx0XCJdLmNvbnN0YW50cy5aX1NZTkNfRkxVU0hcbn07XG5cbmNvbnN0IGJyb3RsaU9wdGlvbnMgPSB7XG4gIGZsdXNoOiB6bGliX19kZWZhdWx0W1wiZGVmYXVsdFwiXS5jb25zdGFudHMuQlJPVExJX09QRVJBVElPTl9GTFVTSCxcbiAgZmluaXNoRmx1c2g6IHpsaWJfX2RlZmF1bHRbXCJkZWZhdWx0XCJdLmNvbnN0YW50cy5CUk9UTElfT1BFUkFUSU9OX0ZMVVNIXG59O1xuXG5jb25zdCBpc0Jyb3RsaVN1cHBvcnRlZCA9IHV0aWxzLmlzRnVuY3Rpb24oemxpYl9fZGVmYXVsdFtcImRlZmF1bHRcIl0uY3JlYXRlQnJvdGxpRGVjb21wcmVzcyk7XG5cbmNvbnN0IHtodHRwOiBodHRwRm9sbG93LCBodHRwczogaHR0cHNGb2xsb3d9ID0gZm9sbG93UmVkaXJlY3RzX19kZWZhdWx0W1wiZGVmYXVsdFwiXTtcblxuY29uc3QgaXNIdHRwcyA9IC9odHRwczo/LztcblxuY29uc3Qgc3VwcG9ydGVkUHJvdG9jb2xzID0gcGxhdGZvcm0ucHJvdG9jb2xzLm1hcChwcm90b2NvbCA9PiB7XG4gIHJldHVybiBwcm90b2NvbCArICc6Jztcbn0pO1xuXG4vKipcbiAqIElmIHRoZSBwcm94eSBvciBjb25maWcgYmVmb3JlUmVkaXJlY3RzIGZ1bmN0aW9ucyBhcmUgZGVmaW5lZCwgY2FsbCB0aGVtIHdpdGggdGhlIG9wdGlvbnNcbiAqIG9iamVjdC5cbiAqXG4gKiBAcGFyYW0ge09iamVjdDxzdHJpbmcsIGFueT59IG9wdGlvbnMgLSBUaGUgb3B0aW9ucyBvYmplY3QgdGhhdCB3YXMgcGFzc2VkIHRvIHRoZSByZXF1ZXN0LlxuICpcbiAqIEByZXR1cm5zIHtPYmplY3Q8c3RyaW5nLCBhbnk+fVxuICovXG5mdW5jdGlvbiBkaXNwYXRjaEJlZm9yZVJlZGlyZWN0KG9wdGlvbnMpIHtcbiAgaWYgKG9wdGlvbnMuYmVmb3JlUmVkaXJlY3RzLnByb3h5KSB7XG4gICAgb3B0aW9ucy5iZWZvcmVSZWRpcmVjdHMucHJveHkob3B0aW9ucyk7XG4gIH1cbiAgaWYgKG9wdGlvbnMuYmVmb3JlUmVkaXJlY3RzLmNvbmZpZykge1xuICAgIG9wdGlvbnMuYmVmb3JlUmVkaXJlY3RzLmNvbmZpZyhvcHRpb25zKTtcbiAgfVxufVxuXG4vKipcbiAqIElmIHRoZSBwcm94eSBvciBjb25maWcgYWZ0ZXJSZWRpcmVjdHMgZnVuY3Rpb25zIGFyZSBkZWZpbmVkLCBjYWxsIHRoZW0gd2l0aCB0aGUgb3B0aW9uc1xuICpcbiAqIEBwYXJhbSB7aHR0cC5DbGllbnRSZXF1ZXN0QXJnc30gb3B0aW9uc1xuICogQHBhcmFtIHtBeGlvc1Byb3h5Q29uZmlnfSBjb25maWdQcm94eSBjb25maWd1cmF0aW9uIGZyb20gQXhpb3Mgb3B0aW9ucyBvYmplY3RcbiAqIEBwYXJhbSB7c3RyaW5nfSBsb2NhdGlvblxuICpcbiAqIEByZXR1cm5zIHtodHRwLkNsaWVudFJlcXVlc3RBcmdzfVxuICovXG5mdW5jdGlvbiBzZXRQcm94eShvcHRpb25zLCBjb25maWdQcm94eSwgbG9jYXRpb24pIHtcbiAgbGV0IHByb3h5ID0gY29uZmlnUHJveHk7XG4gIGlmICghcHJveHkgJiYgcHJveHkgIT09IGZhbHNlKSB7XG4gICAgY29uc3QgcHJveHlVcmwgPSBwcm94eUZyb21FbnYuZ2V0UHJveHlGb3JVcmwobG9jYXRpb24pO1xuICAgIGlmIChwcm94eVVybCkge1xuICAgICAgcHJveHkgPSBuZXcgVVJMKHByb3h5VXJsKTtcbiAgICB9XG4gIH1cbiAgaWYgKHByb3h5KSB7XG4gICAgLy8gQmFzaWMgcHJveHkgYXV0aG9yaXphdGlvblxuICAgIGlmIChwcm94eS51c2VybmFtZSkge1xuICAgICAgcHJveHkuYXV0aCA9IChwcm94eS51c2VybmFtZSB8fCAnJykgKyAnOicgKyAocHJveHkucGFzc3dvcmQgfHwgJycpO1xuICAgIH1cblxuICAgIGlmIChwcm94eS5hdXRoKSB7XG4gICAgICAvLyBTdXBwb3J0IHByb3h5IGF1dGggb2JqZWN0IGZvcm1cbiAgICAgIGlmIChwcm94eS5hdXRoLnVzZXJuYW1lIHx8IHByb3h5LmF1dGgucGFzc3dvcmQpIHtcbiAgICAgICAgcHJveHkuYXV0aCA9IChwcm94eS5hdXRoLnVzZXJuYW1lIHx8ICcnKSArICc6JyArIChwcm94eS5hdXRoLnBhc3N3b3JkIHx8ICcnKTtcbiAgICAgIH1cbiAgICAgIGNvbnN0IGJhc2U2NCA9IEJ1ZmZlclxuICAgICAgICAuZnJvbShwcm94eS5hdXRoLCAndXRmOCcpXG4gICAgICAgIC50b1N0cmluZygnYmFzZTY0Jyk7XG4gICAgICBvcHRpb25zLmhlYWRlcnNbJ1Byb3h5LUF1dGhvcml6YXRpb24nXSA9ICdCYXNpYyAnICsgYmFzZTY0O1xuICAgIH1cblxuICAgIG9wdGlvbnMuaGVhZGVycy5ob3N0ID0gb3B0aW9ucy5ob3N0bmFtZSArIChvcHRpb25zLnBvcnQgPyAnOicgKyBvcHRpb25zLnBvcnQgOiAnJyk7XG4gICAgY29uc3QgcHJveHlIb3N0ID0gcHJveHkuaG9zdG5hbWUgfHwgcHJveHkuaG9zdDtcbiAgICBvcHRpb25zLmhvc3RuYW1lID0gcHJveHlIb3N0O1xuICAgIC8vIFJlcGxhY2UgJ2hvc3QnIHNpbmNlIG9wdGlvbnMgaXMgbm90IGEgVVJMIG9iamVjdFxuICAgIG9wdGlvbnMuaG9zdCA9IHByb3h5SG9zdDtcbiAgICBvcHRpb25zLnBvcnQgPSBwcm94eS5wb3J0O1xuICAgIG9wdGlvbnMucGF0aCA9IGxvY2F0aW9uO1xuICAgIGlmIChwcm94eS5wcm90b2NvbCkge1xuICAgICAgb3B0aW9ucy5wcm90b2NvbCA9IHByb3h5LnByb3RvY29sLmluY2x1ZGVzKCc6JykgPyBwcm94eS5wcm90b2NvbCA6IGAke3Byb3h5LnByb3RvY29sfTpgO1xuICAgIH1cbiAgfVxuXG4gIG9wdGlvbnMuYmVmb3JlUmVkaXJlY3RzLnByb3h5ID0gZnVuY3Rpb24gYmVmb3JlUmVkaXJlY3QocmVkaXJlY3RPcHRpb25zKSB7XG4gICAgLy8gQ29uZmlndXJlIHByb3h5IGZvciByZWRpcmVjdGVkIHJlcXVlc3QsIHBhc3NpbmcgdGhlIG9yaWdpbmFsIGNvbmZpZyBwcm94eSB0byBhcHBseVxuICAgIC8vIHRoZSBleGFjdCBzYW1lIGxvZ2ljIGFzIGlmIHRoZSByZWRpcmVjdGVkIHJlcXVlc3Qgd2FzIHBlcmZvcm1lZCBieSBheGlvcyBkaXJlY3RseS5cbiAgICBzZXRQcm94eShyZWRpcmVjdE9wdGlvbnMsIGNvbmZpZ1Byb3h5LCByZWRpcmVjdE9wdGlvbnMuaHJlZik7XG4gIH07XG59XG5cbmNvbnN0IGlzSHR0cEFkYXB0ZXJTdXBwb3J0ZWQgPSB0eXBlb2YgcHJvY2VzcyAhPT0gJ3VuZGVmaW5lZCcgJiYgdXRpbHMua2luZE9mKHByb2Nlc3MpID09PSAncHJvY2Vzcyc7XG5cbi8vIHRlbXBvcmFyeSBob3RmaXhcblxuY29uc3Qgd3JhcEFzeW5jID0gKGFzeW5jRXhlY3V0b3IpID0+IHtcbiAgcmV0dXJuIG5ldyBQcm9taXNlKChyZXNvbHZlLCByZWplY3QpID0+IHtcbiAgICBsZXQgb25Eb25lO1xuICAgIGxldCBpc0RvbmU7XG5cbiAgICBjb25zdCBkb25lID0gKHZhbHVlLCBpc1JlamVjdGVkKSA9PiB7XG4gICAgICBpZiAoaXNEb25lKSByZXR1cm47XG4gICAgICBpc0RvbmUgPSB0cnVlO1xuICAgICAgb25Eb25lICYmIG9uRG9uZSh2YWx1ZSwgaXNSZWplY3RlZCk7XG4gICAgfTtcblxuICAgIGNvbnN0IF9yZXNvbHZlID0gKHZhbHVlKSA9PiB7XG4gICAgICBkb25lKHZhbHVlKTtcbiAgICAgIHJlc29sdmUodmFsdWUpO1xuICAgIH07XG5cbiAgICBjb25zdCBfcmVqZWN0ID0gKHJlYXNvbikgPT4ge1xuICAgICAgZG9uZShyZWFzb24sIHRydWUpO1xuICAgICAgcmVqZWN0KHJlYXNvbik7XG4gICAgfTtcblxuICAgIGFzeW5jRXhlY3V0b3IoX3Jlc29sdmUsIF9yZWplY3QsIChvbkRvbmVIYW5kbGVyKSA9PiAob25Eb25lID0gb25Eb25lSGFuZGxlcikpLmNhdGNoKF9yZWplY3QpO1xuICB9KVxufTtcblxuY29uc3QgcmVzb2x2ZUZhbWlseSA9ICh7YWRkcmVzcywgZmFtaWx5fSkgPT4ge1xuICBpZiAoIXV0aWxzLmlzU3RyaW5nKGFkZHJlc3MpKSB7XG4gICAgdGhyb3cgVHlwZUVycm9yKCdhZGRyZXNzIG11c3QgYmUgYSBzdHJpbmcnKTtcbiAgfVxuICByZXR1cm4gKHtcbiAgICBhZGRyZXNzLFxuICAgIGZhbWlseTogZmFtaWx5IHx8IChhZGRyZXNzLmluZGV4T2YoJy4nKSA8IDAgPyA2IDogNClcbiAgfSk7XG59O1xuXG5jb25zdCBidWlsZEFkZHJlc3NFbnRyeSA9IChhZGRyZXNzLCBmYW1pbHkpID0+IHJlc29sdmVGYW1pbHkodXRpbHMuaXNPYmplY3QoYWRkcmVzcykgPyBhZGRyZXNzIDoge2FkZHJlc3MsIGZhbWlseX0pO1xuXG4vKmVzbGludCBjb25zaXN0ZW50LXJldHVybjowKi9cbmNvbnN0IGh0dHBBZGFwdGVyID0gaXNIdHRwQWRhcHRlclN1cHBvcnRlZCAmJiBmdW5jdGlvbiBodHRwQWRhcHRlcihjb25maWcpIHtcbiAgcmV0dXJuIHdyYXBBc3luYyhhc3luYyBmdW5jdGlvbiBkaXNwYXRjaEh0dHBSZXF1ZXN0KHJlc29sdmUsIHJlamVjdCwgb25Eb25lKSB7XG4gICAgbGV0IHtkYXRhLCBsb29rdXAsIGZhbWlseX0gPSBjb25maWc7XG4gICAgY29uc3Qge3Jlc3BvbnNlVHlwZSwgcmVzcG9uc2VFbmNvZGluZ30gPSBjb25maWc7XG4gICAgY29uc3QgbWV0aG9kID0gY29uZmlnLm1ldGhvZC50b1VwcGVyQ2FzZSgpO1xuICAgIGxldCBpc0RvbmU7XG4gICAgbGV0IHJlamVjdGVkID0gZmFsc2U7XG4gICAgbGV0IHJlcTtcblxuICAgIGlmIChsb29rdXApIHtcbiAgICAgIGNvbnN0IF9sb29rdXAgPSBjYWxsYmFja2lmeSQxKGxvb2t1cCwgKHZhbHVlKSA9PiB1dGlscy5pc0FycmF5KHZhbHVlKSA/IHZhbHVlIDogW3ZhbHVlXSk7XG4gICAgICAvLyBob3RmaXggdG8gc3VwcG9ydCBvcHQuYWxsIG9wdGlvbiB3aGljaCBpcyByZXF1aXJlZCBmb3Igbm9kZSAyMC54XG4gICAgICBsb29rdXAgPSAoaG9zdG5hbWUsIG9wdCwgY2IpID0+IHtcbiAgICAgICAgX2xvb2t1cChob3N0bmFtZSwgb3B0LCAoZXJyLCBhcmcwLCBhcmcxKSA9PiB7XG4gICAgICAgICAgY29uc3QgYWRkcmVzc2VzID0gdXRpbHMuaXNBcnJheShhcmcwKSA/IGFyZzAubWFwKGFkZHIgPT4gYnVpbGRBZGRyZXNzRW50cnkoYWRkcikpIDogW2J1aWxkQWRkcmVzc0VudHJ5KGFyZzAsIGFyZzEpXTtcblxuICAgICAgICAgIG9wdC5hbGwgPyBjYihlcnIsIGFkZHJlc3NlcykgOiBjYihlcnIsIGFkZHJlc3Nlc1swXS5hZGRyZXNzLCBhZGRyZXNzZXNbMF0uZmFtaWx5KTtcbiAgICAgICAgfSk7XG4gICAgICB9O1xuICAgIH1cblxuICAgIC8vIHRlbXBvcmFyeSBpbnRlcm5hbCBlbWl0dGVyIHVudGlsIHRoZSBBeGlvc1JlcXVlc3QgY2xhc3Mgd2lsbCBiZSBpbXBsZW1lbnRlZFxuICAgIGNvbnN0IGVtaXR0ZXIgPSBuZXcgRXZlbnRFbWl0dGVyX19kZWZhdWx0W1wiZGVmYXVsdFwiXSgpO1xuXG4gICAgY29uc3Qgb25GaW5pc2hlZCA9ICgpID0+IHtcbiAgICAgIGlmIChjb25maWcuY2FuY2VsVG9rZW4pIHtcbiAgICAgICAgY29uZmlnLmNhbmNlbFRva2VuLnVuc3Vic2NyaWJlKGFib3J0KTtcbiAgICAgIH1cblxuICAgICAgaWYgKGNvbmZpZy5zaWduYWwpIHtcbiAgICAgICAgY29uZmlnLnNpZ25hbC5yZW1vdmVFdmVudExpc3RlbmVyKCdhYm9ydCcsIGFib3J0KTtcbiAgICAgIH1cblxuICAgICAgZW1pdHRlci5yZW1vdmVBbGxMaXN0ZW5lcnMoKTtcbiAgICB9O1xuXG4gICAgb25Eb25lKCh2YWx1ZSwgaXNSZWplY3RlZCkgPT4ge1xuICAgICAgaXNEb25lID0gdHJ1ZTtcbiAgICAgIGlmIChpc1JlamVjdGVkKSB7XG4gICAgICAgIHJlamVjdGVkID0gdHJ1ZTtcbiAgICAgICAgb25GaW5pc2hlZCgpO1xuICAgICAgfVxuICAgIH0pO1xuXG4gICAgZnVuY3Rpb24gYWJvcnQocmVhc29uKSB7XG4gICAgICBlbWl0dGVyLmVtaXQoJ2Fib3J0JywgIXJlYXNvbiB8fCByZWFzb24udHlwZSA/IG5ldyBDYW5jZWxlZEVycm9yKG51bGwsIGNvbmZpZywgcmVxKSA6IHJlYXNvbik7XG4gICAgfVxuXG4gICAgZW1pdHRlci5vbmNlKCdhYm9ydCcsIHJlamVjdCk7XG5cbiAgICBpZiAoY29uZmlnLmNhbmNlbFRva2VuIHx8IGNvbmZpZy5zaWduYWwpIHtcbiAgICAgIGNvbmZpZy5jYW5jZWxUb2tlbiAmJiBjb25maWcuY2FuY2VsVG9rZW4uc3Vic2NyaWJlKGFib3J0KTtcbiAgICAgIGlmIChjb25maWcuc2lnbmFsKSB7XG4gICAgICAgIGNvbmZpZy5zaWduYWwuYWJvcnRlZCA/IGFib3J0KCkgOiBjb25maWcuc2lnbmFsLmFkZEV2ZW50TGlzdGVuZXIoJ2Fib3J0JywgYWJvcnQpO1xuICAgICAgfVxuICAgIH1cblxuICAgIC8vIFBhcnNlIHVybFxuICAgIGNvbnN0IGZ1bGxQYXRoID0gYnVpbGRGdWxsUGF0aChjb25maWcuYmFzZVVSTCwgY29uZmlnLnVybCk7XG4gICAgY29uc3QgcGFyc2VkID0gbmV3IFVSTChmdWxsUGF0aCwgJ2h0dHA6Ly9sb2NhbGhvc3QnKTtcbiAgICBjb25zdCBwcm90b2NvbCA9IHBhcnNlZC5wcm90b2NvbCB8fCBzdXBwb3J0ZWRQcm90b2NvbHNbMF07XG5cbiAgICBpZiAocHJvdG9jb2wgPT09ICdkYXRhOicpIHtcbiAgICAgIGxldCBjb252ZXJ0ZWREYXRhO1xuXG4gICAgICBpZiAobWV0aG9kICE9PSAnR0VUJykge1xuICAgICAgICByZXR1cm4gc2V0dGxlKHJlc29sdmUsIHJlamVjdCwge1xuICAgICAgICAgIHN0YXR1czogNDA1LFxuICAgICAgICAgIHN0YXR1c1RleHQ6ICdtZXRob2Qgbm90IGFsbG93ZWQnLFxuICAgICAgICAgIGhlYWRlcnM6IHt9LFxuICAgICAgICAgIGNvbmZpZ1xuICAgICAgICB9KTtcbiAgICAgIH1cblxuICAgICAgdHJ5IHtcbiAgICAgICAgY29udmVydGVkRGF0YSA9IGZyb21EYXRhVVJJKGNvbmZpZy51cmwsIHJlc3BvbnNlVHlwZSA9PT0gJ2Jsb2InLCB7XG4gICAgICAgICAgQmxvYjogY29uZmlnLmVudiAmJiBjb25maWcuZW52LkJsb2JcbiAgICAgICAgfSk7XG4gICAgICB9IGNhdGNoIChlcnIpIHtcbiAgICAgICAgdGhyb3cgQXhpb3NFcnJvci5mcm9tKGVyciwgQXhpb3NFcnJvci5FUlJfQkFEX1JFUVVFU1QsIGNvbmZpZyk7XG4gICAgICB9XG5cbiAgICAgIGlmIChyZXNwb25zZVR5cGUgPT09ICd0ZXh0Jykge1xuICAgICAgICBjb252ZXJ0ZWREYXRhID0gY29udmVydGVkRGF0YS50b1N0cmluZyhyZXNwb25zZUVuY29kaW5nKTtcblxuICAgICAgICBpZiAoIXJlc3BvbnNlRW5jb2RpbmcgfHwgcmVzcG9uc2VFbmNvZGluZyA9PT0gJ3V0ZjgnKSB7XG4gICAgICAgICAgY29udmVydGVkRGF0YSA9IHV0aWxzLnN0cmlwQk9NKGNvbnZlcnRlZERhdGEpO1xuICAgICAgICB9XG4gICAgICB9IGVsc2UgaWYgKHJlc3BvbnNlVHlwZSA9PT0gJ3N0cmVhbScpIHtcbiAgICAgICAgY29udmVydGVkRGF0YSA9IHN0cmVhbV9fZGVmYXVsdFtcImRlZmF1bHRcIl0uUmVhZGFibGUuZnJvbShjb252ZXJ0ZWREYXRhKTtcbiAgICAgIH1cblxuICAgICAgcmV0dXJuIHNldHRsZShyZXNvbHZlLCByZWplY3QsIHtcbiAgICAgICAgZGF0YTogY29udmVydGVkRGF0YSxcbiAgICAgICAgc3RhdHVzOiAyMDAsXG4gICAgICAgIHN0YXR1c1RleHQ6ICdPSycsXG4gICAgICAgIGhlYWRlcnM6IG5ldyBBeGlvc0hlYWRlcnMkMSgpLFxuICAgICAgICBjb25maWdcbiAgICAgIH0pO1xuICAgIH1cblxuICAgIGlmIChzdXBwb3J0ZWRQcm90b2NvbHMuaW5kZXhPZihwcm90b2NvbCkgPT09IC0xKSB7XG4gICAgICByZXR1cm4gcmVqZWN0KG5ldyBBeGlvc0Vycm9yKFxuICAgICAgICAnVW5zdXBwb3J0ZWQgcHJvdG9jb2wgJyArIHByb3RvY29sLFxuICAgICAgICBBeGlvc0Vycm9yLkVSUl9CQURfUkVRVUVTVCxcbiAgICAgICAgY29uZmlnXG4gICAgICApKTtcbiAgICB9XG5cbiAgICBjb25zdCBoZWFkZXJzID0gQXhpb3NIZWFkZXJzJDEuZnJvbShjb25maWcuaGVhZGVycykubm9ybWFsaXplKCk7XG5cbiAgICAvLyBTZXQgVXNlci1BZ2VudCAocmVxdWlyZWQgYnkgc29tZSBzZXJ2ZXJzKVxuICAgIC8vIFNlZSBodHRwczovL2dpdGh1Yi5jb20vYXhpb3MvYXhpb3MvaXNzdWVzLzY5XG4gICAgLy8gVXNlci1BZ2VudCBpcyBzcGVjaWZpZWQ7IGhhbmRsZSBjYXNlIHdoZXJlIG5vIFVBIGhlYWRlciBpcyBkZXNpcmVkXG4gICAgLy8gT25seSBzZXQgaGVhZGVyIGlmIGl0IGhhc24ndCBiZWVuIHNldCBpbiBjb25maWdcbiAgICBoZWFkZXJzLnNldCgnVXNlci1BZ2VudCcsICdheGlvcy8nICsgVkVSU0lPTiwgZmFsc2UpO1xuXG4gICAgY29uc3Qgb25Eb3dubG9hZFByb2dyZXNzID0gY29uZmlnLm9uRG93bmxvYWRQcm9ncmVzcztcbiAgICBjb25zdCBvblVwbG9hZFByb2dyZXNzID0gY29uZmlnLm9uVXBsb2FkUHJvZ3Jlc3M7XG4gICAgY29uc3QgbWF4UmF0ZSA9IGNvbmZpZy5tYXhSYXRlO1xuICAgIGxldCBtYXhVcGxvYWRSYXRlID0gdW5kZWZpbmVkO1xuICAgIGxldCBtYXhEb3dubG9hZFJhdGUgPSB1bmRlZmluZWQ7XG5cbiAgICAvLyBzdXBwb3J0IGZvciBzcGVjIGNvbXBsaWFudCBGb3JtRGF0YSBvYmplY3RzXG4gICAgaWYgKHV0aWxzLmlzU3BlY0NvbXBsaWFudEZvcm0oZGF0YSkpIHtcbiAgICAgIGNvbnN0IHVzZXJCb3VuZGFyeSA9IGhlYWRlcnMuZ2V0Q29udGVudFR5cGUoL2JvdW5kYXJ5PShbLV9cXHdcXGRdezEwLDcwfSkvaSk7XG5cbiAgICAgIGRhdGEgPSBmb3JtRGF0YVRvU3RyZWFtJDEoZGF0YSwgKGZvcm1IZWFkZXJzKSA9PiB7XG4gICAgICAgIGhlYWRlcnMuc2V0KGZvcm1IZWFkZXJzKTtcbiAgICAgIH0sIHtcbiAgICAgICAgdGFnOiBgYXhpb3MtJHtWRVJTSU9OfS1ib3VuZGFyeWAsXG4gICAgICAgIGJvdW5kYXJ5OiB1c2VyQm91bmRhcnkgJiYgdXNlckJvdW5kYXJ5WzFdIHx8IHVuZGVmaW5lZFxuICAgICAgfSk7XG4gICAgICAvLyBzdXBwb3J0IGZvciBodHRwczovL3d3dy5ucG1qcy5jb20vcGFja2FnZS9mb3JtLWRhdGEgYXBpXG4gICAgfSBlbHNlIGlmICh1dGlscy5pc0Zvcm1EYXRhKGRhdGEpICYmIHV0aWxzLmlzRnVuY3Rpb24oZGF0YS5nZXRIZWFkZXJzKSkge1xuICAgICAgaGVhZGVycy5zZXQoZGF0YS5nZXRIZWFkZXJzKCkpO1xuXG4gICAgICBpZiAoIWhlYWRlcnMuaGFzQ29udGVudExlbmd0aCgpKSB7XG4gICAgICAgIHRyeSB7XG4gICAgICAgICAgY29uc3Qga25vd25MZW5ndGggPSBhd2FpdCB1dGlsX19kZWZhdWx0W1wiZGVmYXVsdFwiXS5wcm9taXNpZnkoZGF0YS5nZXRMZW5ndGgpLmNhbGwoZGF0YSk7XG4gICAgICAgICAgTnVtYmVyLmlzRmluaXRlKGtub3duTGVuZ3RoKSAmJiBrbm93bkxlbmd0aCA+PSAwICYmIGhlYWRlcnMuc2V0Q29udGVudExlbmd0aChrbm93bkxlbmd0aCk7XG4gICAgICAgICAgLyplc2xpbnQgbm8tZW1wdHk6MCovXG4gICAgICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgICAgfVxuICAgICAgfVxuICAgIH0gZWxzZSBpZiAodXRpbHMuaXNCbG9iKGRhdGEpKSB7XG4gICAgICBkYXRhLnNpemUgJiYgaGVhZGVycy5zZXRDb250ZW50VHlwZShkYXRhLnR5cGUgfHwgJ2FwcGxpY2F0aW9uL29jdGV0LXN0cmVhbScpO1xuICAgICAgaGVhZGVycy5zZXRDb250ZW50TGVuZ3RoKGRhdGEuc2l6ZSB8fCAwKTtcbiAgICAgIGRhdGEgPSBzdHJlYW1fX2RlZmF1bHRbXCJkZWZhdWx0XCJdLlJlYWRhYmxlLmZyb20ocmVhZEJsb2IkMShkYXRhKSk7XG4gICAgfSBlbHNlIGlmIChkYXRhICYmICF1dGlscy5pc1N0cmVhbShkYXRhKSkge1xuICAgICAgaWYgKEJ1ZmZlci5pc0J1ZmZlcihkYXRhKSkgOyBlbHNlIGlmICh1dGlscy5pc0FycmF5QnVmZmVyKGRhdGEpKSB7XG4gICAgICAgIGRhdGEgPSBCdWZmZXIuZnJvbShuZXcgVWludDhBcnJheShkYXRhKSk7XG4gICAgICB9IGVsc2UgaWYgKHV0aWxzLmlzU3RyaW5nKGRhdGEpKSB7XG4gICAgICAgIGRhdGEgPSBCdWZmZXIuZnJvbShkYXRhLCAndXRmLTgnKTtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIHJldHVybiByZWplY3QobmV3IEF4aW9zRXJyb3IoXG4gICAgICAgICAgJ0RhdGEgYWZ0ZXIgdHJhbnNmb3JtYXRpb24gbXVzdCBiZSBhIHN0cmluZywgYW4gQXJyYXlCdWZmZXIsIGEgQnVmZmVyLCBvciBhIFN0cmVhbScsXG4gICAgICAgICAgQXhpb3NFcnJvci5FUlJfQkFEX1JFUVVFU1QsXG4gICAgICAgICAgY29uZmlnXG4gICAgICAgICkpO1xuICAgICAgfVxuXG4gICAgICAvLyBBZGQgQ29udGVudC1MZW5ndGggaGVhZGVyIGlmIGRhdGEgZXhpc3RzXG4gICAgICBoZWFkZXJzLnNldENvbnRlbnRMZW5ndGgoZGF0YS5sZW5ndGgsIGZhbHNlKTtcblxuICAgICAgaWYgKGNvbmZpZy5tYXhCb2R5TGVuZ3RoID4gLTEgJiYgZGF0YS5sZW5ndGggPiBjb25maWcubWF4Qm9keUxlbmd0aCkge1xuICAgICAgICByZXR1cm4gcmVqZWN0KG5ldyBBeGlvc0Vycm9yKFxuICAgICAgICAgICdSZXF1ZXN0IGJvZHkgbGFyZ2VyIHRoYW4gbWF4Qm9keUxlbmd0aCBsaW1pdCcsXG4gICAgICAgICAgQXhpb3NFcnJvci5FUlJfQkFEX1JFUVVFU1QsXG4gICAgICAgICAgY29uZmlnXG4gICAgICAgICkpO1xuICAgICAgfVxuICAgIH1cblxuICAgIGNvbnN0IGNvbnRlbnRMZW5ndGggPSB1dGlscy50b0Zpbml0ZU51bWJlcihoZWFkZXJzLmdldENvbnRlbnRMZW5ndGgoKSk7XG5cbiAgICBpZiAodXRpbHMuaXNBcnJheShtYXhSYXRlKSkge1xuICAgICAgbWF4VXBsb2FkUmF0ZSA9IG1heFJhdGVbMF07XG4gICAgICBtYXhEb3dubG9hZFJhdGUgPSBtYXhSYXRlWzFdO1xuICAgIH0gZWxzZSB7XG4gICAgICBtYXhVcGxvYWRSYXRlID0gbWF4RG93bmxvYWRSYXRlID0gbWF4UmF0ZTtcbiAgICB9XG5cbiAgICBpZiAoZGF0YSAmJiAob25VcGxvYWRQcm9ncmVzcyB8fCBtYXhVcGxvYWRSYXRlKSkge1xuICAgICAgaWYgKCF1dGlscy5pc1N0cmVhbShkYXRhKSkge1xuICAgICAgICBkYXRhID0gc3RyZWFtX19kZWZhdWx0W1wiZGVmYXVsdFwiXS5SZWFkYWJsZS5mcm9tKGRhdGEsIHtvYmplY3RNb2RlOiBmYWxzZX0pO1xuICAgICAgfVxuXG4gICAgICBkYXRhID0gc3RyZWFtX19kZWZhdWx0W1wiZGVmYXVsdFwiXS5waXBlbGluZShbZGF0YSwgbmV3IEF4aW9zVHJhbnNmb3JtU3RyZWFtJDEoe1xuICAgICAgICBsZW5ndGg6IGNvbnRlbnRMZW5ndGgsXG4gICAgICAgIG1heFJhdGU6IHV0aWxzLnRvRmluaXRlTnVtYmVyKG1heFVwbG9hZFJhdGUpXG4gICAgICB9KV0sIHV0aWxzLm5vb3ApO1xuXG4gICAgICBvblVwbG9hZFByb2dyZXNzICYmIGRhdGEub24oJ3Byb2dyZXNzJywgcHJvZ3Jlc3MgPT4ge1xuICAgICAgICBvblVwbG9hZFByb2dyZXNzKE9iamVjdC5hc3NpZ24ocHJvZ3Jlc3MsIHtcbiAgICAgICAgICB1cGxvYWQ6IHRydWVcbiAgICAgICAgfSkpO1xuICAgICAgfSk7XG4gICAgfVxuXG4gICAgLy8gSFRUUCBiYXNpYyBhdXRoZW50aWNhdGlvblxuICAgIGxldCBhdXRoID0gdW5kZWZpbmVkO1xuICAgIGlmIChjb25maWcuYXV0aCkge1xuICAgICAgY29uc3QgdXNlcm5hbWUgPSBjb25maWcuYXV0aC51c2VybmFtZSB8fCAnJztcbiAgICAgIGNvbnN0IHBhc3N3b3JkID0gY29uZmlnLmF1dGgucGFzc3dvcmQgfHwgJyc7XG4gICAgICBhdXRoID0gdXNlcm5hbWUgKyAnOicgKyBwYXNzd29yZDtcbiAgICB9XG5cbiAgICBpZiAoIWF1dGggJiYgcGFyc2VkLnVzZXJuYW1lKSB7XG4gICAgICBjb25zdCB1cmxVc2VybmFtZSA9IHBhcnNlZC51c2VybmFtZTtcbiAgICAgIGNvbnN0IHVybFBhc3N3b3JkID0gcGFyc2VkLnBhc3N3b3JkO1xuICAgICAgYXV0aCA9IHVybFVzZXJuYW1lICsgJzonICsgdXJsUGFzc3dvcmQ7XG4gICAgfVxuXG4gICAgYXV0aCAmJiBoZWFkZXJzLmRlbGV0ZSgnYXV0aG9yaXphdGlvbicpO1xuXG4gICAgbGV0IHBhdGg7XG5cbiAgICB0cnkge1xuICAgICAgcGF0aCA9IGJ1aWxkVVJMKFxuICAgICAgICBwYXJzZWQucGF0aG5hbWUgKyBwYXJzZWQuc2VhcmNoLFxuICAgICAgICBjb25maWcucGFyYW1zLFxuICAgICAgICBjb25maWcucGFyYW1zU2VyaWFsaXplclxuICAgICAgKS5yZXBsYWNlKC9eXFw/LywgJycpO1xuICAgIH0gY2F0Y2ggKGVycikge1xuICAgICAgY29uc3QgY3VzdG9tRXJyID0gbmV3IEVycm9yKGVyci5tZXNzYWdlKTtcbiAgICAgIGN1c3RvbUVyci5jb25maWcgPSBjb25maWc7XG4gICAgICBjdXN0b21FcnIudXJsID0gY29uZmlnLnVybDtcbiAgICAgIGN1c3RvbUVyci5leGlzdHMgPSB0cnVlO1xuICAgICAgcmV0dXJuIHJlamVjdChjdXN0b21FcnIpO1xuICAgIH1cblxuICAgIGhlYWRlcnMuc2V0KFxuICAgICAgJ0FjY2VwdC1FbmNvZGluZycsXG4gICAgICAnZ3ppcCwgY29tcHJlc3MsIGRlZmxhdGUnICsgKGlzQnJvdGxpU3VwcG9ydGVkID8gJywgYnInIDogJycpLCBmYWxzZVxuICAgICAgKTtcblxuICAgIGNvbnN0IG9wdGlvbnMgPSB7XG4gICAgICBwYXRoLFxuICAgICAgbWV0aG9kOiBtZXRob2QsXG4gICAgICBoZWFkZXJzOiBoZWFkZXJzLnRvSlNPTigpLFxuICAgICAgYWdlbnRzOiB7IGh0dHA6IGNvbmZpZy5odHRwQWdlbnQsIGh0dHBzOiBjb25maWcuaHR0cHNBZ2VudCB9LFxuICAgICAgYXV0aCxcbiAgICAgIHByb3RvY29sLFxuICAgICAgZmFtaWx5LFxuICAgICAgYmVmb3JlUmVkaXJlY3Q6IGRpc3BhdGNoQmVmb3JlUmVkaXJlY3QsXG4gICAgICBiZWZvcmVSZWRpcmVjdHM6IHt9XG4gICAgfTtcblxuICAgIC8vIGNhY2hlYWJsZS1sb29rdXAgaW50ZWdyYXRpb24gaG90Zml4XG4gICAgIXV0aWxzLmlzVW5kZWZpbmVkKGxvb2t1cCkgJiYgKG9wdGlvbnMubG9va3VwID0gbG9va3VwKTtcblxuICAgIGlmIChjb25maWcuc29ja2V0UGF0aCkge1xuICAgICAgb3B0aW9ucy5zb2NrZXRQYXRoID0gY29uZmlnLnNvY2tldFBhdGg7XG4gICAgfSBlbHNlIHtcbiAgICAgIG9wdGlvbnMuaG9zdG5hbWUgPSBwYXJzZWQuaG9zdG5hbWU7XG4gICAgICBvcHRpb25zLnBvcnQgPSBwYXJzZWQucG9ydDtcbiAgICAgIHNldFByb3h5KG9wdGlvbnMsIGNvbmZpZy5wcm94eSwgcHJvdG9jb2wgKyAnLy8nICsgcGFyc2VkLmhvc3RuYW1lICsgKHBhcnNlZC5wb3J0ID8gJzonICsgcGFyc2VkLnBvcnQgOiAnJykgKyBvcHRpb25zLnBhdGgpO1xuICAgIH1cblxuICAgIGxldCB0cmFuc3BvcnQ7XG4gICAgY29uc3QgaXNIdHRwc1JlcXVlc3QgPSBpc0h0dHBzLnRlc3Qob3B0aW9ucy5wcm90b2NvbCk7XG4gICAgb3B0aW9ucy5hZ2VudCA9IGlzSHR0cHNSZXF1ZXN0ID8gY29uZmlnLmh0dHBzQWdlbnQgOiBjb25maWcuaHR0cEFnZW50O1xuICAgIGlmIChjb25maWcudHJhbnNwb3J0KSB7XG4gICAgICB0cmFuc3BvcnQgPSBjb25maWcudHJhbnNwb3J0O1xuICAgIH0gZWxzZSBpZiAoY29uZmlnLm1heFJlZGlyZWN0cyA9PT0gMCkge1xuICAgICAgdHJhbnNwb3J0ID0gaXNIdHRwc1JlcXVlc3QgPyBodHRwc19fZGVmYXVsdFtcImRlZmF1bHRcIl0gOiBodHRwX19kZWZhdWx0W1wiZGVmYXVsdFwiXTtcbiAgICB9IGVsc2Uge1xuICAgICAgaWYgKGNvbmZpZy5tYXhSZWRpcmVjdHMpIHtcbiAgICAgICAgb3B0aW9ucy5tYXhSZWRpcmVjdHMgPSBjb25maWcubWF4UmVkaXJlY3RzO1xuICAgICAgfVxuICAgICAgaWYgKGNvbmZpZy5iZWZvcmVSZWRpcmVjdCkge1xuICAgICAgICBvcHRpb25zLmJlZm9yZVJlZGlyZWN0cy5jb25maWcgPSBjb25maWcuYmVmb3JlUmVkaXJlY3Q7XG4gICAgICB9XG4gICAgICB0cmFuc3BvcnQgPSBpc0h0dHBzUmVxdWVzdCA/IGh0dHBzRm9sbG93IDogaHR0cEZvbGxvdztcbiAgICB9XG5cbiAgICBpZiAoY29uZmlnLm1heEJvZHlMZW5ndGggPiAtMSkge1xuICAgICAgb3B0aW9ucy5tYXhCb2R5TGVuZ3RoID0gY29uZmlnLm1heEJvZHlMZW5ndGg7XG4gICAgfSBlbHNlIHtcbiAgICAgIC8vIGZvbGxvdy1yZWRpcmVjdHMgZG9lcyBub3Qgc2tpcCBjb21wYXJpc29uLCBzbyBpdCBzaG91bGQgYWx3YXlzIHN1Y2NlZWQgZm9yIGF4aW9zIC0xIHVubGltaXRlZFxuICAgICAgb3B0aW9ucy5tYXhCb2R5TGVuZ3RoID0gSW5maW5pdHk7XG4gICAgfVxuXG4gICAgaWYgKGNvbmZpZy5pbnNlY3VyZUhUVFBQYXJzZXIpIHtcbiAgICAgIG9wdGlvbnMuaW5zZWN1cmVIVFRQUGFyc2VyID0gY29uZmlnLmluc2VjdXJlSFRUUFBhcnNlcjtcbiAgICB9XG5cbiAgICAvLyBDcmVhdGUgdGhlIHJlcXVlc3RcbiAgICByZXEgPSB0cmFuc3BvcnQucmVxdWVzdChvcHRpb25zLCBmdW5jdGlvbiBoYW5kbGVSZXNwb25zZShyZXMpIHtcbiAgICAgIGlmIChyZXEuZGVzdHJveWVkKSByZXR1cm47XG5cbiAgICAgIGNvbnN0IHN0cmVhbXMgPSBbcmVzXTtcblxuICAgICAgY29uc3QgcmVzcG9uc2VMZW5ndGggPSArcmVzLmhlYWRlcnNbJ2NvbnRlbnQtbGVuZ3RoJ107XG5cbiAgICAgIGlmIChvbkRvd25sb2FkUHJvZ3Jlc3MpIHtcbiAgICAgICAgY29uc3QgdHJhbnNmb3JtU3RyZWFtID0gbmV3IEF4aW9zVHJhbnNmb3JtU3RyZWFtJDEoe1xuICAgICAgICAgIGxlbmd0aDogdXRpbHMudG9GaW5pdGVOdW1iZXIocmVzcG9uc2VMZW5ndGgpLFxuICAgICAgICAgIG1heFJhdGU6IHV0aWxzLnRvRmluaXRlTnVtYmVyKG1heERvd25sb2FkUmF0ZSlcbiAgICAgICAgfSk7XG5cbiAgICAgICAgb25Eb3dubG9hZFByb2dyZXNzICYmIHRyYW5zZm9ybVN0cmVhbS5vbigncHJvZ3Jlc3MnLCBwcm9ncmVzcyA9PiB7XG4gICAgICAgICAgb25Eb3dubG9hZFByb2dyZXNzKE9iamVjdC5hc3NpZ24ocHJvZ3Jlc3MsIHtcbiAgICAgICAgICAgIGRvd25sb2FkOiB0cnVlXG4gICAgICAgICAgfSkpO1xuICAgICAgICB9KTtcblxuICAgICAgICBzdHJlYW1zLnB1c2godHJhbnNmb3JtU3RyZWFtKTtcbiAgICAgIH1cblxuICAgICAgLy8gZGVjb21wcmVzcyB0aGUgcmVzcG9uc2UgYm9keSB0cmFuc3BhcmVudGx5IGlmIHJlcXVpcmVkXG4gICAgICBsZXQgcmVzcG9uc2VTdHJlYW0gPSByZXM7XG5cbiAgICAgIC8vIHJldHVybiB0aGUgbGFzdCByZXF1ZXN0IGluIGNhc2Ugb2YgcmVkaXJlY3RzXG4gICAgICBjb25zdCBsYXN0UmVxdWVzdCA9IHJlcy5yZXEgfHwgcmVxO1xuXG4gICAgICAvLyBpZiBkZWNvbXByZXNzIGRpc2FibGVkIHdlIHNob3VsZCBub3QgZGVjb21wcmVzc1xuICAgICAgaWYgKGNvbmZpZy5kZWNvbXByZXNzICE9PSBmYWxzZSAmJiByZXMuaGVhZGVyc1snY29udGVudC1lbmNvZGluZyddKSB7XG4gICAgICAgIC8vIGlmIG5vIGNvbnRlbnQsIGJ1dCBoZWFkZXJzIHN0aWxsIHNheSB0aGF0IGl0IGlzIGVuY29kZWQsXG4gICAgICAgIC8vIHJlbW92ZSB0aGUgaGVhZGVyIG5vdCBjb25mdXNlIGRvd25zdHJlYW0gb3BlcmF0aW9uc1xuICAgICAgICBpZiAobWV0aG9kID09PSAnSEVBRCcgfHwgcmVzLnN0YXR1c0NvZGUgPT09IDIwNCkge1xuICAgICAgICAgIGRlbGV0ZSByZXMuaGVhZGVyc1snY29udGVudC1lbmNvZGluZyddO1xuICAgICAgICB9XG5cbiAgICAgICAgc3dpdGNoICgocmVzLmhlYWRlcnNbJ2NvbnRlbnQtZW5jb2RpbmcnXSB8fCAnJykudG9Mb3dlckNhc2UoKSkge1xuICAgICAgICAvKmVzbGludCBkZWZhdWx0LWNhc2U6MCovXG4gICAgICAgIGNhc2UgJ2d6aXAnOlxuICAgICAgICBjYXNlICd4LWd6aXAnOlxuICAgICAgICBjYXNlICdjb21wcmVzcyc6XG4gICAgICAgIGNhc2UgJ3gtY29tcHJlc3MnOlxuICAgICAgICAgIC8vIGFkZCB0aGUgdW56aXBwZXIgdG8gdGhlIGJvZHkgc3RyZWFtIHByb2Nlc3NpbmcgcGlwZWxpbmVcbiAgICAgICAgICBzdHJlYW1zLnB1c2goemxpYl9fZGVmYXVsdFtcImRlZmF1bHRcIl0uY3JlYXRlVW56aXAoemxpYk9wdGlvbnMpKTtcblxuICAgICAgICAgIC8vIHJlbW92ZSB0aGUgY29udGVudC1lbmNvZGluZyBpbiBvcmRlciB0byBub3QgY29uZnVzZSBkb3duc3RyZWFtIG9wZXJhdGlvbnNcbiAgICAgICAgICBkZWxldGUgcmVzLmhlYWRlcnNbJ2NvbnRlbnQtZW5jb2RpbmcnXTtcbiAgICAgICAgICBicmVhaztcbiAgICAgICAgY2FzZSAnZGVmbGF0ZSc6XG4gICAgICAgICAgc3RyZWFtcy5wdXNoKG5ldyBabGliSGVhZGVyVHJhbnNmb3JtU3RyZWFtJDEoKSk7XG5cbiAgICAgICAgICAvLyBhZGQgdGhlIHVuemlwcGVyIHRvIHRoZSBib2R5IHN0cmVhbSBwcm9jZXNzaW5nIHBpcGVsaW5lXG4gICAgICAgICAgc3RyZWFtcy5wdXNoKHpsaWJfX2RlZmF1bHRbXCJkZWZhdWx0XCJdLmNyZWF0ZVVuemlwKHpsaWJPcHRpb25zKSk7XG5cbiAgICAgICAgICAvLyByZW1vdmUgdGhlIGNvbnRlbnQtZW5jb2RpbmcgaW4gb3JkZXIgdG8gbm90IGNvbmZ1c2UgZG93bnN0cmVhbSBvcGVyYXRpb25zXG4gICAgICAgICAgZGVsZXRlIHJlcy5oZWFkZXJzWydjb250ZW50LWVuY29kaW5nJ107XG4gICAgICAgICAgYnJlYWs7XG4gICAgICAgIGNhc2UgJ2JyJzpcbiAgICAgICAgICBpZiAoaXNCcm90bGlTdXBwb3J0ZWQpIHtcbiAgICAgICAgICAgIHN0cmVhbXMucHVzaCh6bGliX19kZWZhdWx0W1wiZGVmYXVsdFwiXS5jcmVhdGVCcm90bGlEZWNvbXByZXNzKGJyb3RsaU9wdGlvbnMpKTtcbiAgICAgICAgICAgIGRlbGV0ZSByZXMuaGVhZGVyc1snY29udGVudC1lbmNvZGluZyddO1xuICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgICAgfVxuXG4gICAgICByZXNwb25zZVN0cmVhbSA9IHN0cmVhbXMubGVuZ3RoID4gMSA/IHN0cmVhbV9fZGVmYXVsdFtcImRlZmF1bHRcIl0ucGlwZWxpbmUoc3RyZWFtcywgdXRpbHMubm9vcCkgOiBzdHJlYW1zWzBdO1xuXG4gICAgICBjb25zdCBvZmZMaXN0ZW5lcnMgPSBzdHJlYW1fX2RlZmF1bHRbXCJkZWZhdWx0XCJdLmZpbmlzaGVkKHJlc3BvbnNlU3RyZWFtLCAoKSA9PiB7XG4gICAgICAgIG9mZkxpc3RlbmVycygpO1xuICAgICAgICBvbkZpbmlzaGVkKCk7XG4gICAgICB9KTtcblxuICAgICAgY29uc3QgcmVzcG9uc2UgPSB7XG4gICAgICAgIHN0YXR1czogcmVzLnN0YXR1c0NvZGUsXG4gICAgICAgIHN0YXR1c1RleHQ6IHJlcy5zdGF0dXNNZXNzYWdlLFxuICAgICAgICBoZWFkZXJzOiBuZXcgQXhpb3NIZWFkZXJzJDEocmVzLmhlYWRlcnMpLFxuICAgICAgICBjb25maWcsXG4gICAgICAgIHJlcXVlc3Q6IGxhc3RSZXF1ZXN0XG4gICAgICB9O1xuXG4gICAgICBpZiAocmVzcG9uc2VUeXBlID09PSAnc3RyZWFtJykge1xuICAgICAgICByZXNwb25zZS5kYXRhID0gcmVzcG9uc2VTdHJlYW07XG4gICAgICAgIHNldHRsZShyZXNvbHZlLCByZWplY3QsIHJlc3BvbnNlKTtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIGNvbnN0IHJlc3BvbnNlQnVmZmVyID0gW107XG4gICAgICAgIGxldCB0b3RhbFJlc3BvbnNlQnl0ZXMgPSAwO1xuXG4gICAgICAgIHJlc3BvbnNlU3RyZWFtLm9uKCdkYXRhJywgZnVuY3Rpb24gaGFuZGxlU3RyZWFtRGF0YShjaHVuaykge1xuICAgICAgICAgIHJlc3BvbnNlQnVmZmVyLnB1c2goY2h1bmspO1xuICAgICAgICAgIHRvdGFsUmVzcG9uc2VCeXRlcyArPSBjaHVuay5sZW5ndGg7XG5cbiAgICAgICAgICAvLyBtYWtlIHN1cmUgdGhlIGNvbnRlbnQgbGVuZ3RoIGlzIG5vdCBvdmVyIHRoZSBtYXhDb250ZW50TGVuZ3RoIGlmIHNwZWNpZmllZFxuICAgICAgICAgIGlmIChjb25maWcubWF4Q29udGVudExlbmd0aCA+IC0xICYmIHRvdGFsUmVzcG9uc2VCeXRlcyA+IGNvbmZpZy5tYXhDb250ZW50TGVuZ3RoKSB7XG4gICAgICAgICAgICAvLyBzdHJlYW0uZGVzdHJveSgpIGVtaXQgYWJvcnRlZCBldmVudCBiZWZvcmUgY2FsbGluZyByZWplY3QoKSBvbiBOb2RlLmpzIHYxNlxuICAgICAgICAgICAgcmVqZWN0ZWQgPSB0cnVlO1xuICAgICAgICAgICAgcmVzcG9uc2VTdHJlYW0uZGVzdHJveSgpO1xuICAgICAgICAgICAgcmVqZWN0KG5ldyBBeGlvc0Vycm9yKCdtYXhDb250ZW50TGVuZ3RoIHNpemUgb2YgJyArIGNvbmZpZy5tYXhDb250ZW50TGVuZ3RoICsgJyBleGNlZWRlZCcsXG4gICAgICAgICAgICAgIEF4aW9zRXJyb3IuRVJSX0JBRF9SRVNQT05TRSwgY29uZmlnLCBsYXN0UmVxdWVzdCkpO1xuICAgICAgICAgIH1cbiAgICAgICAgfSk7XG5cbiAgICAgICAgcmVzcG9uc2VTdHJlYW0ub24oJ2Fib3J0ZWQnLCBmdW5jdGlvbiBoYW5kbGVyU3RyZWFtQWJvcnRlZCgpIHtcbiAgICAgICAgICBpZiAocmVqZWN0ZWQpIHtcbiAgICAgICAgICAgIHJldHVybjtcbiAgICAgICAgICB9XG5cbiAgICAgICAgICBjb25zdCBlcnIgPSBuZXcgQXhpb3NFcnJvcihcbiAgICAgICAgICAgICdtYXhDb250ZW50TGVuZ3RoIHNpemUgb2YgJyArIGNvbmZpZy5tYXhDb250ZW50TGVuZ3RoICsgJyBleGNlZWRlZCcsXG4gICAgICAgICAgICBBeGlvc0Vycm9yLkVSUl9CQURfUkVTUE9OU0UsXG4gICAgICAgICAgICBjb25maWcsXG4gICAgICAgICAgICBsYXN0UmVxdWVzdFxuICAgICAgICAgICk7XG4gICAgICAgICAgcmVzcG9uc2VTdHJlYW0uZGVzdHJveShlcnIpO1xuICAgICAgICAgIHJlamVjdChlcnIpO1xuICAgICAgICB9KTtcblxuICAgICAgICByZXNwb25zZVN0cmVhbS5vbignZXJyb3InLCBmdW5jdGlvbiBoYW5kbGVTdHJlYW1FcnJvcihlcnIpIHtcbiAgICAgICAgICBpZiAocmVxLmRlc3Ryb3llZCkgcmV0dXJuO1xuICAgICAgICAgIHJlamVjdChBeGlvc0Vycm9yLmZyb20oZXJyLCBudWxsLCBjb25maWcsIGxhc3RSZXF1ZXN0KSk7XG4gICAgICAgIH0pO1xuXG4gICAgICAgIHJlc3BvbnNlU3RyZWFtLm9uKCdlbmQnLCBmdW5jdGlvbiBoYW5kbGVTdHJlYW1FbmQoKSB7XG4gICAgICAgICAgdHJ5IHtcbiAgICAgICAgICAgIGxldCByZXNwb25zZURhdGEgPSByZXNwb25zZUJ1ZmZlci5sZW5ndGggPT09IDEgPyByZXNwb25zZUJ1ZmZlclswXSA6IEJ1ZmZlci5jb25jYXQocmVzcG9uc2VCdWZmZXIpO1xuICAgICAgICAgICAgaWYgKHJlc3BvbnNlVHlwZSAhPT0gJ2FycmF5YnVmZmVyJykge1xuICAgICAgICAgICAgICByZXNwb25zZURhdGEgPSByZXNwb25zZURhdGEudG9TdHJpbmcocmVzcG9uc2VFbmNvZGluZyk7XG4gICAgICAgICAgICAgIGlmICghcmVzcG9uc2VFbmNvZGluZyB8fCByZXNwb25zZUVuY29kaW5nID09PSAndXRmOCcpIHtcbiAgICAgICAgICAgICAgICByZXNwb25zZURhdGEgPSB1dGlscy5zdHJpcEJPTShyZXNwb25zZURhdGEpO1xuICAgICAgICAgICAgICB9XG4gICAgICAgICAgICB9XG4gICAgICAgICAgICByZXNwb25zZS5kYXRhID0gcmVzcG9uc2VEYXRhO1xuICAgICAgICAgIH0gY2F0Y2ggKGVycikge1xuICAgICAgICAgICAgcmV0dXJuIHJlamVjdChBeGlvc0Vycm9yLmZyb20oZXJyLCBudWxsLCBjb25maWcsIHJlc3BvbnNlLnJlcXVlc3QsIHJlc3BvbnNlKSk7XG4gICAgICAgICAgfVxuICAgICAgICAgIHNldHRsZShyZXNvbHZlLCByZWplY3QsIHJlc3BvbnNlKTtcbiAgICAgICAgfSk7XG4gICAgICB9XG5cbiAgICAgIGVtaXR0ZXIub25jZSgnYWJvcnQnLCBlcnIgPT4ge1xuICAgICAgICBpZiAoIXJlc3BvbnNlU3RyZWFtLmRlc3Ryb3llZCkge1xuICAgICAgICAgIHJlc3BvbnNlU3RyZWFtLmVtaXQoJ2Vycm9yJywgZXJyKTtcbiAgICAgICAgICByZXNwb25zZVN0cmVhbS5kZXN0cm95KCk7XG4gICAgICAgIH1cbiAgICAgIH0pO1xuICAgIH0pO1xuXG4gICAgZW1pdHRlci5vbmNlKCdhYm9ydCcsIGVyciA9PiB7XG4gICAgICByZWplY3QoZXJyKTtcbiAgICAgIHJlcS5kZXN0cm95KGVycik7XG4gICAgfSk7XG5cbiAgICAvLyBIYW5kbGUgZXJyb3JzXG4gICAgcmVxLm9uKCdlcnJvcicsIGZ1bmN0aW9uIGhhbmRsZVJlcXVlc3RFcnJvcihlcnIpIHtcbiAgICAgIC8vIEB0b2RvIHJlbW92ZVxuICAgICAgLy8gaWYgKHJlcS5hYm9ydGVkICYmIGVyci5jb2RlICE9PSBBeGlvc0Vycm9yLkVSUl9GUl9UT09fTUFOWV9SRURJUkVDVFMpIHJldHVybjtcbiAgICAgIHJlamVjdChBeGlvc0Vycm9yLmZyb20oZXJyLCBudWxsLCBjb25maWcsIHJlcSkpO1xuICAgIH0pO1xuXG4gICAgLy8gc2V0IHRjcCBrZWVwIGFsaXZlIHRvIHByZXZlbnQgZHJvcCBjb25uZWN0aW9uIGJ5IHBlZXJcbiAgICByZXEub24oJ3NvY2tldCcsIGZ1bmN0aW9uIGhhbmRsZVJlcXVlc3RTb2NrZXQoc29ja2V0KSB7XG4gICAgICAvLyBkZWZhdWx0IGludGVydmFsIG9mIHNlbmRpbmcgYWNrIHBhY2tldCBpcyAxIG1pbnV0ZVxuICAgICAgc29ja2V0LnNldEtlZXBBbGl2ZSh0cnVlLCAxMDAwICogNjApO1xuICAgIH0pO1xuXG4gICAgLy8gSGFuZGxlIHJlcXVlc3QgdGltZW91dFxuICAgIGlmIChjb25maWcudGltZW91dCkge1xuICAgICAgLy8gVGhpcyBpcyBmb3JjaW5nIGEgaW50IHRpbWVvdXQgdG8gYXZvaWQgcHJvYmxlbXMgaWYgdGhlIGByZXFgIGludGVyZmFjZSBkb2Vzbid0IGhhbmRsZSBvdGhlciB0eXBlcy5cbiAgICAgIGNvbnN0IHRpbWVvdXQgPSBwYXJzZUludChjb25maWcudGltZW91dCwgMTApO1xuXG4gICAgICBpZiAoTnVtYmVyLmlzTmFOKHRpbWVvdXQpKSB7XG4gICAgICAgIHJlamVjdChuZXcgQXhpb3NFcnJvcihcbiAgICAgICAgICAnZXJyb3IgdHJ5aW5nIHRvIHBhcnNlIGBjb25maWcudGltZW91dGAgdG8gaW50JyxcbiAgICAgICAgICBBeGlvc0Vycm9yLkVSUl9CQURfT1BUSU9OX1ZBTFVFLFxuICAgICAgICAgIGNvbmZpZyxcbiAgICAgICAgICByZXFcbiAgICAgICAgKSk7XG5cbiAgICAgICAgcmV0dXJuO1xuICAgICAgfVxuXG4gICAgICAvLyBTb21ldGltZSwgdGhlIHJlc3BvbnNlIHdpbGwgYmUgdmVyeSBzbG93LCBhbmQgZG9lcyBub3QgcmVzcG9uZCwgdGhlIGNvbm5lY3QgZXZlbnQgd2lsbCBiZSBibG9jayBieSBldmVudCBsb29wIHN5c3RlbS5cbiAgICAgIC8vIEFuZCB0aW1lciBjYWxsYmFjayB3aWxsIGJlIGZpcmVkLCBhbmQgYWJvcnQoKSB3aWxsIGJlIGludm9rZWQgYmVmb3JlIGNvbm5lY3Rpb24sIHRoZW4gZ2V0IFwic29ja2V0IGhhbmcgdXBcIiBhbmQgY29kZSBFQ09OTlJFU0VULlxuICAgICAgLy8gQXQgdGhpcyB0aW1lLCBpZiB3ZSBoYXZlIGEgbGFyZ2UgbnVtYmVyIG9mIHJlcXVlc3QsIG5vZGVqcyB3aWxsIGhhbmcgdXAgc29tZSBzb2NrZXQgb24gYmFja2dyb3VuZC4gYW5kIHRoZSBudW1iZXIgd2lsbCB1cCBhbmQgdXAuXG4gICAgICAvLyBBbmQgdGhlbiB0aGVzZSBzb2NrZXQgd2hpY2ggYmUgaGFuZyB1cCB3aWxsIGRldm91cmluZyBDUFUgbGl0dGxlIGJ5IGxpdHRsZS5cbiAgICAgIC8vIENsaWVudFJlcXVlc3Quc2V0VGltZW91dCB3aWxsIGJlIGZpcmVkIG9uIHRoZSBzcGVjaWZ5IG1pbGxpc2Vjb25kcywgYW5kIGNhbiBtYWtlIHN1cmUgdGhhdCBhYm9ydCgpIHdpbGwgYmUgZmlyZWQgYWZ0ZXIgY29ubmVjdC5cbiAgICAgIHJlcS5zZXRUaW1lb3V0KHRpbWVvdXQsIGZ1bmN0aW9uIGhhbmRsZVJlcXVlc3RUaW1lb3V0KCkge1xuICAgICAgICBpZiAoaXNEb25lKSByZXR1cm47XG4gICAgICAgIGxldCB0aW1lb3V0RXJyb3JNZXNzYWdlID0gY29uZmlnLnRpbWVvdXQgPyAndGltZW91dCBvZiAnICsgY29uZmlnLnRpbWVvdXQgKyAnbXMgZXhjZWVkZWQnIDogJ3RpbWVvdXQgZXhjZWVkZWQnO1xuICAgICAgICBjb25zdCB0cmFuc2l0aW9uYWwgPSBjb25maWcudHJhbnNpdGlvbmFsIHx8IHRyYW5zaXRpb25hbERlZmF1bHRzO1xuICAgICAgICBpZiAoY29uZmlnLnRpbWVvdXRFcnJvck1lc3NhZ2UpIHtcbiAgICAgICAgICB0aW1lb3V0RXJyb3JNZXNzYWdlID0gY29uZmlnLnRpbWVvdXRFcnJvck1lc3NhZ2U7XG4gICAgICAgIH1cbiAgICAgICAgcmVqZWN0KG5ldyBBeGlvc0Vycm9yKFxuICAgICAgICAgIHRpbWVvdXRFcnJvck1lc3NhZ2UsXG4gICAgICAgICAgdHJhbnNpdGlvbmFsLmNsYXJpZnlUaW1lb3V0RXJyb3IgPyBBeGlvc0Vycm9yLkVUSU1FRE9VVCA6IEF4aW9zRXJyb3IuRUNPTk5BQk9SVEVELFxuICAgICAgICAgIGNvbmZpZyxcbiAgICAgICAgICByZXFcbiAgICAgICAgKSk7XG4gICAgICAgIGFib3J0KCk7XG4gICAgICB9KTtcbiAgICB9XG5cblxuICAgIC8vIFNlbmQgdGhlIHJlcXVlc3RcbiAgICBpZiAodXRpbHMuaXNTdHJlYW0oZGF0YSkpIHtcbiAgICAgIGxldCBlbmRlZCA9IGZhbHNlO1xuICAgICAgbGV0IGVycm9yZWQgPSBmYWxzZTtcblxuICAgICAgZGF0YS5vbignZW5kJywgKCkgPT4ge1xuICAgICAgICBlbmRlZCA9IHRydWU7XG4gICAgICB9KTtcblxuICAgICAgZGF0YS5vbmNlKCdlcnJvcicsIGVyciA9PiB7XG4gICAgICAgIGVycm9yZWQgPSB0cnVlO1xuICAgICAgICByZXEuZGVzdHJveShlcnIpO1xuICAgICAgfSk7XG5cbiAgICAgIGRhdGEub24oJ2Nsb3NlJywgKCkgPT4ge1xuICAgICAgICBpZiAoIWVuZGVkICYmICFlcnJvcmVkKSB7XG4gICAgICAgICAgYWJvcnQobmV3IENhbmNlbGVkRXJyb3IoJ1JlcXVlc3Qgc3RyZWFtIGhhcyBiZWVuIGFib3J0ZWQnLCBjb25maWcsIHJlcSkpO1xuICAgICAgICB9XG4gICAgICB9KTtcblxuICAgICAgZGF0YS5waXBlKHJlcSk7XG4gICAgfSBlbHNlIHtcbiAgICAgIHJlcS5lbmQoZGF0YSk7XG4gICAgfVxuICB9KTtcbn07XG5cbmNvbnN0IGNvb2tpZXMgPSBwbGF0Zm9ybS5pc1N0YW5kYXJkQnJvd3NlckVudiA/XG5cbi8vIFN0YW5kYXJkIGJyb3dzZXIgZW52cyBzdXBwb3J0IGRvY3VtZW50LmNvb2tpZVxuICAoZnVuY3Rpb24gc3RhbmRhcmRCcm93c2VyRW52KCkge1xuICAgIHJldHVybiB7XG4gICAgICB3cml0ZTogZnVuY3Rpb24gd3JpdGUobmFtZSwgdmFsdWUsIGV4cGlyZXMsIHBhdGgsIGRvbWFpbiwgc2VjdXJlKSB7XG4gICAgICAgIGNvbnN0IGNvb2tpZSA9IFtdO1xuICAgICAgICBjb29raWUucHVzaChuYW1lICsgJz0nICsgZW5jb2RlVVJJQ29tcG9uZW50KHZhbHVlKSk7XG5cbiAgICAgICAgaWYgKHV0aWxzLmlzTnVtYmVyKGV4cGlyZXMpKSB7XG4gICAgICAgICAgY29va2llLnB1c2goJ2V4cGlyZXM9JyArIG5ldyBEYXRlKGV4cGlyZXMpLnRvR01UU3RyaW5nKCkpO1xuICAgICAgICB9XG5cbiAgICAgICAgaWYgKHV0aWxzLmlzU3RyaW5nKHBhdGgpKSB7XG4gICAgICAgICAgY29va2llLnB1c2goJ3BhdGg9JyArIHBhdGgpO1xuICAgICAgICB9XG5cbiAgICAgICAgaWYgKHV0aWxzLmlzU3RyaW5nKGRvbWFpbikpIHtcbiAgICAgICAgICBjb29raWUucHVzaCgnZG9tYWluPScgKyBkb21haW4pO1xuICAgICAgICB9XG5cbiAgICAgICAgaWYgKHNlY3VyZSA9PT0gdHJ1ZSkge1xuICAgICAgICAgIGNvb2tpZS5wdXNoKCdzZWN1cmUnKTtcbiAgICAgICAgfVxuXG4gICAgICAgIGRvY3VtZW50LmNvb2tpZSA9IGNvb2tpZS5qb2luKCc7ICcpO1xuICAgICAgfSxcblxuICAgICAgcmVhZDogZnVuY3Rpb24gcmVhZChuYW1lKSB7XG4gICAgICAgIGNvbnN0IG1hdGNoID0gZG9jdW1lbnQuY29va2llLm1hdGNoKG5ldyBSZWdFeHAoJyhefDtcXFxccyopKCcgKyBuYW1lICsgJyk9KFteO10qKScpKTtcbiAgICAgICAgcmV0dXJuIChtYXRjaCA/IGRlY29kZVVSSUNvbXBvbmVudChtYXRjaFszXSkgOiBudWxsKTtcbiAgICAgIH0sXG5cbiAgICAgIHJlbW92ZTogZnVuY3Rpb24gcmVtb3ZlKG5hbWUpIHtcbiAgICAgICAgdGhpcy53cml0ZShuYW1lLCAnJywgRGF0ZS5ub3coKSAtIDg2NDAwMDAwKTtcbiAgICAgIH1cbiAgICB9O1xuICB9KSgpIDpcblxuLy8gTm9uIHN0YW5kYXJkIGJyb3dzZXIgZW52ICh3ZWIgd29ya2VycywgcmVhY3QtbmF0aXZlKSBsYWNrIG5lZWRlZCBzdXBwb3J0LlxuICAoZnVuY3Rpb24gbm9uU3RhbmRhcmRCcm93c2VyRW52KCkge1xuICAgIHJldHVybiB7XG4gICAgICB3cml0ZTogZnVuY3Rpb24gd3JpdGUoKSB7fSxcbiAgICAgIHJlYWQ6IGZ1bmN0aW9uIHJlYWQoKSB7IHJldHVybiBudWxsOyB9LFxuICAgICAgcmVtb3ZlOiBmdW5jdGlvbiByZW1vdmUoKSB7fVxuICAgIH07XG4gIH0pKCk7XG5cbmNvbnN0IGlzVVJMU2FtZU9yaWdpbiA9IHBsYXRmb3JtLmlzU3RhbmRhcmRCcm93c2VyRW52ID9cblxuLy8gU3RhbmRhcmQgYnJvd3NlciBlbnZzIGhhdmUgZnVsbCBzdXBwb3J0IG9mIHRoZSBBUElzIG5lZWRlZCB0byB0ZXN0XG4vLyB3aGV0aGVyIHRoZSByZXF1ZXN0IFVSTCBpcyBvZiB0aGUgc2FtZSBvcmlnaW4gYXMgY3VycmVudCBsb2NhdGlvbi5cbiAgKGZ1bmN0aW9uIHN0YW5kYXJkQnJvd3NlckVudigpIHtcbiAgICBjb25zdCBtc2llID0gLyhtc2llfHRyaWRlbnQpL2kudGVzdChuYXZpZ2F0b3IudXNlckFnZW50KTtcbiAgICBjb25zdCB1cmxQYXJzaW5nTm9kZSA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ2EnKTtcbiAgICBsZXQgb3JpZ2luVVJMO1xuXG4gICAgLyoqXG4gICAgKiBQYXJzZSBhIFVSTCB0byBkaXNjb3ZlciBpdCdzIGNvbXBvbmVudHNcbiAgICAqXG4gICAgKiBAcGFyYW0ge1N0cmluZ30gdXJsIFRoZSBVUkwgdG8gYmUgcGFyc2VkXG4gICAgKiBAcmV0dXJucyB7T2JqZWN0fVxuICAgICovXG4gICAgZnVuY3Rpb24gcmVzb2x2ZVVSTCh1cmwpIHtcbiAgICAgIGxldCBocmVmID0gdXJsO1xuXG4gICAgICBpZiAobXNpZSkge1xuICAgICAgICAvLyBJRSBuZWVkcyBhdHRyaWJ1dGUgc2V0IHR3aWNlIHRvIG5vcm1hbGl6ZSBwcm9wZXJ0aWVzXG4gICAgICAgIHVybFBhcnNpbmdOb2RlLnNldEF0dHJpYnV0ZSgnaHJlZicsIGhyZWYpO1xuICAgICAgICBocmVmID0gdXJsUGFyc2luZ05vZGUuaHJlZjtcbiAgICAgIH1cblxuICAgICAgdXJsUGFyc2luZ05vZGUuc2V0QXR0cmlidXRlKCdocmVmJywgaHJlZik7XG5cbiAgICAgIC8vIHVybFBhcnNpbmdOb2RlIHByb3ZpZGVzIHRoZSBVcmxVdGlscyBpbnRlcmZhY2UgLSBodHRwOi8vdXJsLnNwZWMud2hhdHdnLm9yZy8jdXJsdXRpbHNcbiAgICAgIHJldHVybiB7XG4gICAgICAgIGhyZWY6IHVybFBhcnNpbmdOb2RlLmhyZWYsXG4gICAgICAgIHByb3RvY29sOiB1cmxQYXJzaW5nTm9kZS5wcm90b2NvbCA/IHVybFBhcnNpbmdOb2RlLnByb3RvY29sLnJlcGxhY2UoLzokLywgJycpIDogJycsXG4gICAgICAgIGhvc3Q6IHVybFBhcnNpbmdOb2RlLmhvc3QsXG4gICAgICAgIHNlYXJjaDogdXJsUGFyc2luZ05vZGUuc2VhcmNoID8gdXJsUGFyc2luZ05vZGUuc2VhcmNoLnJlcGxhY2UoL15cXD8vLCAnJykgOiAnJyxcbiAgICAgICAgaGFzaDogdXJsUGFyc2luZ05vZGUuaGFzaCA/IHVybFBhcnNpbmdOb2RlLmhhc2gucmVwbGFjZSgvXiMvLCAnJykgOiAnJyxcbiAgICAgICAgaG9zdG5hbWU6IHVybFBhcnNpbmdOb2RlLmhvc3RuYW1lLFxuICAgICAgICBwb3J0OiB1cmxQYXJzaW5nTm9kZS5wb3J0LFxuICAgICAgICBwYXRobmFtZTogKHVybFBhcnNpbmdOb2RlLnBhdGhuYW1lLmNoYXJBdCgwKSA9PT0gJy8nKSA/XG4gICAgICAgICAgdXJsUGFyc2luZ05vZGUucGF0aG5hbWUgOlxuICAgICAgICAgICcvJyArIHVybFBhcnNpbmdOb2RlLnBhdGhuYW1lXG4gICAgICB9O1xuICAgIH1cblxuICAgIG9yaWdpblVSTCA9IHJlc29sdmVVUkwod2luZG93LmxvY2F0aW9uLmhyZWYpO1xuXG4gICAgLyoqXG4gICAgKiBEZXRlcm1pbmUgaWYgYSBVUkwgc2hhcmVzIHRoZSBzYW1lIG9yaWdpbiBhcyB0aGUgY3VycmVudCBsb2NhdGlvblxuICAgICpcbiAgICAqIEBwYXJhbSB7U3RyaW5nfSByZXF1ZXN0VVJMIFRoZSBVUkwgdG8gdGVzdFxuICAgICogQHJldHVybnMge2Jvb2xlYW59IFRydWUgaWYgVVJMIHNoYXJlcyB0aGUgc2FtZSBvcmlnaW4sIG90aGVyd2lzZSBmYWxzZVxuICAgICovXG4gICAgcmV0dXJuIGZ1bmN0aW9uIGlzVVJMU2FtZU9yaWdpbihyZXF1ZXN0VVJMKSB7XG4gICAgICBjb25zdCBwYXJzZWQgPSAodXRpbHMuaXNTdHJpbmcocmVxdWVzdFVSTCkpID8gcmVzb2x2ZVVSTChyZXF1ZXN0VVJMKSA6IHJlcXVlc3RVUkw7XG4gICAgICByZXR1cm4gKHBhcnNlZC5wcm90b2NvbCA9PT0gb3JpZ2luVVJMLnByb3RvY29sICYmXG4gICAgICAgICAgcGFyc2VkLmhvc3QgPT09IG9yaWdpblVSTC5ob3N0KTtcbiAgICB9O1xuICB9KSgpIDpcblxuICAvLyBOb24gc3RhbmRhcmQgYnJvd3NlciBlbnZzICh3ZWIgd29ya2VycywgcmVhY3QtbmF0aXZlKSBsYWNrIG5lZWRlZCBzdXBwb3J0LlxuICAoZnVuY3Rpb24gbm9uU3RhbmRhcmRCcm93c2VyRW52KCkge1xuICAgIHJldHVybiBmdW5jdGlvbiBpc1VSTFNhbWVPcmlnaW4oKSB7XG4gICAgICByZXR1cm4gdHJ1ZTtcbiAgICB9O1xuICB9KSgpO1xuXG5mdW5jdGlvbiBwcm9ncmVzc0V2ZW50UmVkdWNlcihsaXN0ZW5lciwgaXNEb3dubG9hZFN0cmVhbSkge1xuICBsZXQgYnl0ZXNOb3RpZmllZCA9IDA7XG4gIGNvbnN0IF9zcGVlZG9tZXRlciA9IHNwZWVkb21ldGVyKDUwLCAyNTApO1xuXG4gIHJldHVybiBlID0+IHtcbiAgICBjb25zdCBsb2FkZWQgPSBlLmxvYWRlZDtcbiAgICBjb25zdCB0b3RhbCA9IGUubGVuZ3RoQ29tcHV0YWJsZSA/IGUudG90YWwgOiB1bmRlZmluZWQ7XG4gICAgY29uc3QgcHJvZ3Jlc3NCeXRlcyA9IGxvYWRlZCAtIGJ5dGVzTm90aWZpZWQ7XG4gICAgY29uc3QgcmF0ZSA9IF9zcGVlZG9tZXRlcihwcm9ncmVzc0J5dGVzKTtcbiAgICBjb25zdCBpblJhbmdlID0gbG9hZGVkIDw9IHRvdGFsO1xuXG4gICAgYnl0ZXNOb3RpZmllZCA9IGxvYWRlZDtcblxuICAgIGNvbnN0IGRhdGEgPSB7XG4gICAgICBsb2FkZWQsXG4gICAgICB0b3RhbCxcbiAgICAgIHByb2dyZXNzOiB0b3RhbCA/IChsb2FkZWQgLyB0b3RhbCkgOiB1bmRlZmluZWQsXG4gICAgICBieXRlczogcHJvZ3Jlc3NCeXRlcyxcbiAgICAgIHJhdGU6IHJhdGUgPyByYXRlIDogdW5kZWZpbmVkLFxuICAgICAgZXN0aW1hdGVkOiByYXRlICYmIHRvdGFsICYmIGluUmFuZ2UgPyAodG90YWwgLSBsb2FkZWQpIC8gcmF0ZSA6IHVuZGVmaW5lZCxcbiAgICAgIGV2ZW50OiBlXG4gICAgfTtcblxuICAgIGRhdGFbaXNEb3dubG9hZFN0cmVhbSA/ICdkb3dubG9hZCcgOiAndXBsb2FkJ10gPSB0cnVlO1xuXG4gICAgbGlzdGVuZXIoZGF0YSk7XG4gIH07XG59XG5cbmNvbnN0IGlzWEhSQWRhcHRlclN1cHBvcnRlZCA9IHR5cGVvZiBYTUxIdHRwUmVxdWVzdCAhPT0gJ3VuZGVmaW5lZCc7XG5cbmNvbnN0IHhockFkYXB0ZXIgPSBpc1hIUkFkYXB0ZXJTdXBwb3J0ZWQgJiYgZnVuY3Rpb24gKGNvbmZpZykge1xuICByZXR1cm4gbmV3IFByb21pc2UoZnVuY3Rpb24gZGlzcGF0Y2hYaHJSZXF1ZXN0KHJlc29sdmUsIHJlamVjdCkge1xuICAgIGxldCByZXF1ZXN0RGF0YSA9IGNvbmZpZy5kYXRhO1xuICAgIGNvbnN0IHJlcXVlc3RIZWFkZXJzID0gQXhpb3NIZWFkZXJzJDEuZnJvbShjb25maWcuaGVhZGVycykubm9ybWFsaXplKCk7XG4gICAgY29uc3QgcmVzcG9uc2VUeXBlID0gY29uZmlnLnJlc3BvbnNlVHlwZTtcbiAgICBsZXQgb25DYW5jZWxlZDtcbiAgICBmdW5jdGlvbiBkb25lKCkge1xuICAgICAgaWYgKGNvbmZpZy5jYW5jZWxUb2tlbikge1xuICAgICAgICBjb25maWcuY2FuY2VsVG9rZW4udW5zdWJzY3JpYmUob25DYW5jZWxlZCk7XG4gICAgICB9XG5cbiAgICAgIGlmIChjb25maWcuc2lnbmFsKSB7XG4gICAgICAgIGNvbmZpZy5zaWduYWwucmVtb3ZlRXZlbnRMaXN0ZW5lcignYWJvcnQnLCBvbkNhbmNlbGVkKTtcbiAgICAgIH1cbiAgICB9XG5cbiAgICBsZXQgY29udGVudFR5cGU7XG5cbiAgICBpZiAodXRpbHMuaXNGb3JtRGF0YShyZXF1ZXN0RGF0YSkpIHtcbiAgICAgIGlmIChwbGF0Zm9ybS5pc1N0YW5kYXJkQnJvd3NlckVudiB8fCBwbGF0Zm9ybS5pc1N0YW5kYXJkQnJvd3NlcldlYldvcmtlckVudikge1xuICAgICAgICByZXF1ZXN0SGVhZGVycy5zZXRDb250ZW50VHlwZShmYWxzZSk7IC8vIExldCB0aGUgYnJvd3NlciBzZXQgaXRcbiAgICAgIH0gZWxzZSBpZighcmVxdWVzdEhlYWRlcnMuZ2V0Q29udGVudFR5cGUoL15cXHMqbXVsdGlwYXJ0XFwvZm9ybS1kYXRhLykpe1xuICAgICAgICByZXF1ZXN0SGVhZGVycy5zZXRDb250ZW50VHlwZSgnbXVsdGlwYXJ0L2Zvcm0tZGF0YScpOyAvLyBtb2JpbGUvZGVza3RvcCBhcHAgZnJhbWV3b3Jrc1xuICAgICAgfSBlbHNlIGlmKHV0aWxzLmlzU3RyaW5nKGNvbnRlbnRUeXBlID0gcmVxdWVzdEhlYWRlcnMuZ2V0Q29udGVudFR5cGUoKSkpe1xuICAgICAgICAvLyBmaXggc2VtaWNvbG9uIGR1cGxpY2F0aW9uIGlzc3VlIGZvciBSZWFjdE5hdGl2ZSBGb3JtRGF0YSBpbXBsZW1lbnRhdGlvblxuICAgICAgICByZXF1ZXN0SGVhZGVycy5zZXRDb250ZW50VHlwZShjb250ZW50VHlwZS5yZXBsYWNlKC9eXFxzKihtdWx0aXBhcnRcXC9mb3JtLWRhdGEpOysvLCAnJDEnKSk7XG4gICAgICB9XG4gICAgfVxuXG4gICAgbGV0IHJlcXVlc3QgPSBuZXcgWE1MSHR0cFJlcXVlc3QoKTtcblxuICAgIC8vIEhUVFAgYmFzaWMgYXV0aGVudGljYXRpb25cbiAgICBpZiAoY29uZmlnLmF1dGgpIHtcbiAgICAgIGNvbnN0IHVzZXJuYW1lID0gY29uZmlnLmF1dGgudXNlcm5hbWUgfHwgJyc7XG4gICAgICBjb25zdCBwYXNzd29yZCA9IGNvbmZpZy5hdXRoLnBhc3N3b3JkID8gdW5lc2NhcGUoZW5jb2RlVVJJQ29tcG9uZW50KGNvbmZpZy5hdXRoLnBhc3N3b3JkKSkgOiAnJztcbiAgICAgIHJlcXVlc3RIZWFkZXJzLnNldCgnQXV0aG9yaXphdGlvbicsICdCYXNpYyAnICsgYnRvYSh1c2VybmFtZSArICc6JyArIHBhc3N3b3JkKSk7XG4gICAgfVxuXG4gICAgY29uc3QgZnVsbFBhdGggPSBidWlsZEZ1bGxQYXRoKGNvbmZpZy5iYXNlVVJMLCBjb25maWcudXJsKTtcblxuICAgIHJlcXVlc3Qub3Blbihjb25maWcubWV0aG9kLnRvVXBwZXJDYXNlKCksIGJ1aWxkVVJMKGZ1bGxQYXRoLCBjb25maWcucGFyYW1zLCBjb25maWcucGFyYW1zU2VyaWFsaXplciksIHRydWUpO1xuXG4gICAgLy8gU2V0IHRoZSByZXF1ZXN0IHRpbWVvdXQgaW4gTVNcbiAgICByZXF1ZXN0LnRpbWVvdXQgPSBjb25maWcudGltZW91dDtcblxuICAgIGZ1bmN0aW9uIG9ubG9hZGVuZCgpIHtcbiAgICAgIGlmICghcmVxdWVzdCkge1xuICAgICAgICByZXR1cm47XG4gICAgICB9XG4gICAgICAvLyBQcmVwYXJlIHRoZSByZXNwb25zZVxuICAgICAgY29uc3QgcmVzcG9uc2VIZWFkZXJzID0gQXhpb3NIZWFkZXJzJDEuZnJvbShcbiAgICAgICAgJ2dldEFsbFJlc3BvbnNlSGVhZGVycycgaW4gcmVxdWVzdCAmJiByZXF1ZXN0LmdldEFsbFJlc3BvbnNlSGVhZGVycygpXG4gICAgICApO1xuICAgICAgY29uc3QgcmVzcG9uc2VEYXRhID0gIXJlc3BvbnNlVHlwZSB8fCByZXNwb25zZVR5cGUgPT09ICd0ZXh0JyB8fCByZXNwb25zZVR5cGUgPT09ICdqc29uJyA/XG4gICAgICAgIHJlcXVlc3QucmVzcG9uc2VUZXh0IDogcmVxdWVzdC5yZXNwb25zZTtcbiAgICAgIGNvbnN0IHJlc3BvbnNlID0ge1xuICAgICAgICBkYXRhOiByZXNwb25zZURhdGEsXG4gICAgICAgIHN0YXR1czogcmVxdWVzdC5zdGF0dXMsXG4gICAgICAgIHN0YXR1c1RleHQ6IHJlcXVlc3Quc3RhdHVzVGV4dCxcbiAgICAgICAgaGVhZGVyczogcmVzcG9uc2VIZWFkZXJzLFxuICAgICAgICBjb25maWcsXG4gICAgICAgIHJlcXVlc3RcbiAgICAgIH07XG5cbiAgICAgIHNldHRsZShmdW5jdGlvbiBfcmVzb2x2ZSh2YWx1ZSkge1xuICAgICAgICByZXNvbHZlKHZhbHVlKTtcbiAgICAgICAgZG9uZSgpO1xuICAgICAgfSwgZnVuY3Rpb24gX3JlamVjdChlcnIpIHtcbiAgICAgICAgcmVqZWN0KGVycik7XG4gICAgICAgIGRvbmUoKTtcbiAgICAgIH0sIHJlc3BvbnNlKTtcblxuICAgICAgLy8gQ2xlYW4gdXAgcmVxdWVzdFxuICAgICAgcmVxdWVzdCA9IG51bGw7XG4gICAgfVxuXG4gICAgaWYgKCdvbmxvYWRlbmQnIGluIHJlcXVlc3QpIHtcbiAgICAgIC8vIFVzZSBvbmxvYWRlbmQgaWYgYXZhaWxhYmxlXG4gICAgICByZXF1ZXN0Lm9ubG9hZGVuZCA9IG9ubG9hZGVuZDtcbiAgICB9IGVsc2Uge1xuICAgICAgLy8gTGlzdGVuIGZvciByZWFkeSBzdGF0ZSB0byBlbXVsYXRlIG9ubG9hZGVuZFxuICAgICAgcmVxdWVzdC5vbnJlYWR5c3RhdGVjaGFuZ2UgPSBmdW5jdGlvbiBoYW5kbGVMb2FkKCkge1xuICAgICAgICBpZiAoIXJlcXVlc3QgfHwgcmVxdWVzdC5yZWFkeVN0YXRlICE9PSA0KSB7XG4gICAgICAgICAgcmV0dXJuO1xuICAgICAgICB9XG5cbiAgICAgICAgLy8gVGhlIHJlcXVlc3QgZXJyb3JlZCBvdXQgYW5kIHdlIGRpZG4ndCBnZXQgYSByZXNwb25zZSwgdGhpcyB3aWxsIGJlXG4gICAgICAgIC8vIGhhbmRsZWQgYnkgb25lcnJvciBpbnN0ZWFkXG4gICAgICAgIC8vIFdpdGggb25lIGV4Y2VwdGlvbjogcmVxdWVzdCB0aGF0IHVzaW5nIGZpbGU6IHByb3RvY29sLCBtb3N0IGJyb3dzZXJzXG4gICAgICAgIC8vIHdpbGwgcmV0dXJuIHN0YXR1cyBhcyAwIGV2ZW4gdGhvdWdoIGl0J3MgYSBzdWNjZXNzZnVsIHJlcXVlc3RcbiAgICAgICAgaWYgKHJlcXVlc3Quc3RhdHVzID09PSAwICYmICEocmVxdWVzdC5yZXNwb25zZVVSTCAmJiByZXF1ZXN0LnJlc3BvbnNlVVJMLmluZGV4T2YoJ2ZpbGU6JykgPT09IDApKSB7XG4gICAgICAgICAgcmV0dXJuO1xuICAgICAgICB9XG4gICAgICAgIC8vIHJlYWR5c3RhdGUgaGFuZGxlciBpcyBjYWxsaW5nIGJlZm9yZSBvbmVycm9yIG9yIG9udGltZW91dCBoYW5kbGVycyxcbiAgICAgICAgLy8gc28gd2Ugc2hvdWxkIGNhbGwgb25sb2FkZW5kIG9uIHRoZSBuZXh0ICd0aWNrJ1xuICAgICAgICBzZXRUaW1lb3V0KG9ubG9hZGVuZCk7XG4gICAgICB9O1xuICAgIH1cblxuICAgIC8vIEhhbmRsZSBicm93c2VyIHJlcXVlc3QgY2FuY2VsbGF0aW9uIChhcyBvcHBvc2VkIHRvIGEgbWFudWFsIGNhbmNlbGxhdGlvbilcbiAgICByZXF1ZXN0Lm9uYWJvcnQgPSBmdW5jdGlvbiBoYW5kbGVBYm9ydCgpIHtcbiAgICAgIGlmICghcmVxdWVzdCkge1xuICAgICAgICByZXR1cm47XG4gICAgICB9XG5cbiAgICAgIHJlamVjdChuZXcgQXhpb3NFcnJvcignUmVxdWVzdCBhYm9ydGVkJywgQXhpb3NFcnJvci5FQ09OTkFCT1JURUQsIGNvbmZpZywgcmVxdWVzdCkpO1xuXG4gICAgICAvLyBDbGVhbiB1cCByZXF1ZXN0XG4gICAgICByZXF1ZXN0ID0gbnVsbDtcbiAgICB9O1xuXG4gICAgLy8gSGFuZGxlIGxvdyBsZXZlbCBuZXR3b3JrIGVycm9yc1xuICAgIHJlcXVlc3Qub25lcnJvciA9IGZ1bmN0aW9uIGhhbmRsZUVycm9yKCkge1xuICAgICAgLy8gUmVhbCBlcnJvcnMgYXJlIGhpZGRlbiBmcm9tIHVzIGJ5IHRoZSBicm93c2VyXG4gICAgICAvLyBvbmVycm9yIHNob3VsZCBvbmx5IGZpcmUgaWYgaXQncyBhIG5ldHdvcmsgZXJyb3JcbiAgICAgIHJlamVjdChuZXcgQXhpb3NFcnJvcignTmV0d29yayBFcnJvcicsIEF4aW9zRXJyb3IuRVJSX05FVFdPUkssIGNvbmZpZywgcmVxdWVzdCkpO1xuXG4gICAgICAvLyBDbGVhbiB1cCByZXF1ZXN0XG4gICAgICByZXF1ZXN0ID0gbnVsbDtcbiAgICB9O1xuXG4gICAgLy8gSGFuZGxlIHRpbWVvdXRcbiAgICByZXF1ZXN0Lm9udGltZW91dCA9IGZ1bmN0aW9uIGhhbmRsZVRpbWVvdXQoKSB7XG4gICAgICBsZXQgdGltZW91dEVycm9yTWVzc2FnZSA9IGNvbmZpZy50aW1lb3V0ID8gJ3RpbWVvdXQgb2YgJyArIGNvbmZpZy50aW1lb3V0ICsgJ21zIGV4Y2VlZGVkJyA6ICd0aW1lb3V0IGV4Y2VlZGVkJztcbiAgICAgIGNvbnN0IHRyYW5zaXRpb25hbCA9IGNvbmZpZy50cmFuc2l0aW9uYWwgfHwgdHJhbnNpdGlvbmFsRGVmYXVsdHM7XG4gICAgICBpZiAoY29uZmlnLnRpbWVvdXRFcnJvck1lc3NhZ2UpIHtcbiAgICAgICAgdGltZW91dEVycm9yTWVzc2FnZSA9IGNvbmZpZy50aW1lb3V0RXJyb3JNZXNzYWdlO1xuICAgICAgfVxuICAgICAgcmVqZWN0KG5ldyBBeGlvc0Vycm9yKFxuICAgICAgICB0aW1lb3V0RXJyb3JNZXNzYWdlLFxuICAgICAgICB0cmFuc2l0aW9uYWwuY2xhcmlmeVRpbWVvdXRFcnJvciA/IEF4aW9zRXJyb3IuRVRJTUVET1VUIDogQXhpb3NFcnJvci5FQ09OTkFCT1JURUQsXG4gICAgICAgIGNvbmZpZyxcbiAgICAgICAgcmVxdWVzdCkpO1xuXG4gICAgICAvLyBDbGVhbiB1cCByZXF1ZXN0XG4gICAgICByZXF1ZXN0ID0gbnVsbDtcbiAgICB9O1xuXG4gICAgLy8gQWRkIHhzcmYgaGVhZGVyXG4gICAgLy8gVGhpcyBpcyBvbmx5IGRvbmUgaWYgcnVubmluZyBpbiBhIHN0YW5kYXJkIGJyb3dzZXIgZW52aXJvbm1lbnQuXG4gICAgLy8gU3BlY2lmaWNhbGx5IG5vdCBpZiB3ZSdyZSBpbiBhIHdlYiB3b3JrZXIsIG9yIHJlYWN0LW5hdGl2ZS5cbiAgICBpZiAocGxhdGZvcm0uaXNTdGFuZGFyZEJyb3dzZXJFbnYpIHtcbiAgICAgIC8vIEFkZCB4c3JmIGhlYWRlclxuICAgICAgLy8gcmVnYXJkaW5nIENWRS0yMDIzLTQ1ODU3IGNvbmZpZy53aXRoQ3JlZGVudGlhbHMgY29uZGl0aW9uIHdhcyByZW1vdmVkIHRlbXBvcmFyaWx5XG4gICAgICBjb25zdCB4c3JmVmFsdWUgPSBpc1VSTFNhbWVPcmlnaW4oZnVsbFBhdGgpICYmIGNvbmZpZy54c3JmQ29va2llTmFtZSAmJiBjb29raWVzLnJlYWQoY29uZmlnLnhzcmZDb29raWVOYW1lKTtcblxuICAgICAgaWYgKHhzcmZWYWx1ZSkge1xuICAgICAgICByZXF1ZXN0SGVhZGVycy5zZXQoY29uZmlnLnhzcmZIZWFkZXJOYW1lLCB4c3JmVmFsdWUpO1xuICAgICAgfVxuICAgIH1cblxuICAgIC8vIFJlbW92ZSBDb250ZW50LVR5cGUgaWYgZGF0YSBpcyB1bmRlZmluZWRcbiAgICByZXF1ZXN0RGF0YSA9PT0gdW5kZWZpbmVkICYmIHJlcXVlc3RIZWFkZXJzLnNldENvbnRlbnRUeXBlKG51bGwpO1xuXG4gICAgLy8gQWRkIGhlYWRlcnMgdG8gdGhlIHJlcXVlc3RcbiAgICBpZiAoJ3NldFJlcXVlc3RIZWFkZXInIGluIHJlcXVlc3QpIHtcbiAgICAgIHV0aWxzLmZvckVhY2gocmVxdWVzdEhlYWRlcnMudG9KU09OKCksIGZ1bmN0aW9uIHNldFJlcXVlc3RIZWFkZXIodmFsLCBrZXkpIHtcbiAgICAgICAgcmVxdWVzdC5zZXRSZXF1ZXN0SGVhZGVyKGtleSwgdmFsKTtcbiAgICAgIH0pO1xuICAgIH1cblxuICAgIC8vIEFkZCB3aXRoQ3JlZGVudGlhbHMgdG8gcmVxdWVzdCBpZiBuZWVkZWRcbiAgICBpZiAoIXV0aWxzLmlzVW5kZWZpbmVkKGNvbmZpZy53aXRoQ3JlZGVudGlhbHMpKSB7XG4gICAgICByZXF1ZXN0LndpdGhDcmVkZW50aWFscyA9ICEhY29uZmlnLndpdGhDcmVkZW50aWFscztcbiAgICB9XG5cbiAgICAvLyBBZGQgcmVzcG9uc2VUeXBlIHRvIHJlcXVlc3QgaWYgbmVlZGVkXG4gICAgaWYgKHJlc3BvbnNlVHlwZSAmJiByZXNwb25zZVR5cGUgIT09ICdqc29uJykge1xuICAgICAgcmVxdWVzdC5yZXNwb25zZVR5cGUgPSBjb25maWcucmVzcG9uc2VUeXBlO1xuICAgIH1cblxuICAgIC8vIEhhbmRsZSBwcm9ncmVzcyBpZiBuZWVkZWRcbiAgICBpZiAodHlwZW9mIGNvbmZpZy5vbkRvd25sb2FkUHJvZ3Jlc3MgPT09ICdmdW5jdGlvbicpIHtcbiAgICAgIHJlcXVlc3QuYWRkRXZlbnRMaXN0ZW5lcigncHJvZ3Jlc3MnLCBwcm9ncmVzc0V2ZW50UmVkdWNlcihjb25maWcub25Eb3dubG9hZFByb2dyZXNzLCB0cnVlKSk7XG4gICAgfVxuXG4gICAgLy8gTm90IGFsbCBicm93c2VycyBzdXBwb3J0IHVwbG9hZCBldmVudHNcbiAgICBpZiAodHlwZW9mIGNvbmZpZy5vblVwbG9hZFByb2dyZXNzID09PSAnZnVuY3Rpb24nICYmIHJlcXVlc3QudXBsb2FkKSB7XG4gICAgICByZXF1ZXN0LnVwbG9hZC5hZGRFdmVudExpc3RlbmVyKCdwcm9ncmVzcycsIHByb2dyZXNzRXZlbnRSZWR1Y2VyKGNvbmZpZy5vblVwbG9hZFByb2dyZXNzKSk7XG4gICAgfVxuXG4gICAgaWYgKGNvbmZpZy5jYW5jZWxUb2tlbiB8fCBjb25maWcuc2lnbmFsKSB7XG4gICAgICAvLyBIYW5kbGUgY2FuY2VsbGF0aW9uXG4gICAgICAvLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgZnVuYy1uYW1lc1xuICAgICAgb25DYW5jZWxlZCA9IGNhbmNlbCA9PiB7XG4gICAgICAgIGlmICghcmVxdWVzdCkge1xuICAgICAgICAgIHJldHVybjtcbiAgICAgICAgfVxuICAgICAgICByZWplY3QoIWNhbmNlbCB8fCBjYW5jZWwudHlwZSA/IG5ldyBDYW5jZWxlZEVycm9yKG51bGwsIGNvbmZpZywgcmVxdWVzdCkgOiBjYW5jZWwpO1xuICAgICAgICByZXF1ZXN0LmFib3J0KCk7XG4gICAgICAgIHJlcXVlc3QgPSBudWxsO1xuICAgICAgfTtcblxuICAgICAgY29uZmlnLmNhbmNlbFRva2VuICYmIGNvbmZpZy5jYW5jZWxUb2tlbi5zdWJzY3JpYmUob25DYW5jZWxlZCk7XG4gICAgICBpZiAoY29uZmlnLnNpZ25hbCkge1xuICAgICAgICBjb25maWcuc2lnbmFsLmFib3J0ZWQgPyBvbkNhbmNlbGVkKCkgOiBjb25maWcuc2lnbmFsLmFkZEV2ZW50TGlzdGVuZXIoJ2Fib3J0Jywgb25DYW5jZWxlZCk7XG4gICAgICB9XG4gICAgfVxuXG4gICAgY29uc3QgcHJvdG9jb2wgPSBwYXJzZVByb3RvY29sKGZ1bGxQYXRoKTtcblxuICAgIGlmIChwcm90b2NvbCAmJiBwbGF0Zm9ybS5wcm90b2NvbHMuaW5kZXhPZihwcm90b2NvbCkgPT09IC0xKSB7XG4gICAgICByZWplY3QobmV3IEF4aW9zRXJyb3IoJ1Vuc3VwcG9ydGVkIHByb3RvY29sICcgKyBwcm90b2NvbCArICc6JywgQXhpb3NFcnJvci5FUlJfQkFEX1JFUVVFU1QsIGNvbmZpZykpO1xuICAgICAgcmV0dXJuO1xuICAgIH1cblxuXG4gICAgLy8gU2VuZCB0aGUgcmVxdWVzdFxuICAgIHJlcXVlc3Quc2VuZChyZXF1ZXN0RGF0YSB8fCBudWxsKTtcbiAgfSk7XG59O1xuXG5jb25zdCBrbm93bkFkYXB0ZXJzID0ge1xuICBodHRwOiBodHRwQWRhcHRlcixcbiAgeGhyOiB4aHJBZGFwdGVyXG59O1xuXG51dGlscy5mb3JFYWNoKGtub3duQWRhcHRlcnMsIChmbiwgdmFsdWUpID0+IHtcbiAgaWYgKGZuKSB7XG4gICAgdHJ5IHtcbiAgICAgIE9iamVjdC5kZWZpbmVQcm9wZXJ0eShmbiwgJ25hbWUnLCB7dmFsdWV9KTtcbiAgICB9IGNhdGNoIChlKSB7XG4gICAgICAvLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgbm8tZW1wdHlcbiAgICB9XG4gICAgT2JqZWN0LmRlZmluZVByb3BlcnR5KGZuLCAnYWRhcHRlck5hbWUnLCB7dmFsdWV9KTtcbiAgfVxufSk7XG5cbmNvbnN0IHJlbmRlclJlYXNvbiA9IChyZWFzb24pID0+IGAtICR7cmVhc29ufWA7XG5cbmNvbnN0IGlzUmVzb2x2ZWRIYW5kbGUgPSAoYWRhcHRlcikgPT4gdXRpbHMuaXNGdW5jdGlvbihhZGFwdGVyKSB8fCBhZGFwdGVyID09PSBudWxsIHx8IGFkYXB0ZXIgPT09IGZhbHNlO1xuXG5jb25zdCBhZGFwdGVycyA9IHtcbiAgZ2V0QWRhcHRlcjogKGFkYXB0ZXJzKSA9PiB7XG4gICAgYWRhcHRlcnMgPSB1dGlscy5pc0FycmF5KGFkYXB0ZXJzKSA/IGFkYXB0ZXJzIDogW2FkYXB0ZXJzXTtcblxuICAgIGNvbnN0IHtsZW5ndGh9ID0gYWRhcHRlcnM7XG4gICAgbGV0IG5hbWVPckFkYXB0ZXI7XG4gICAgbGV0IGFkYXB0ZXI7XG5cbiAgICBjb25zdCByZWplY3RlZFJlYXNvbnMgPSB7fTtcblxuICAgIGZvciAobGV0IGkgPSAwOyBpIDwgbGVuZ3RoOyBpKyspIHtcbiAgICAgIG5hbWVPckFkYXB0ZXIgPSBhZGFwdGVyc1tpXTtcbiAgICAgIGxldCBpZDtcblxuICAgICAgYWRhcHRlciA9IG5hbWVPckFkYXB0ZXI7XG5cbiAgICAgIGlmICghaXNSZXNvbHZlZEhhbmRsZShuYW1lT3JBZGFwdGVyKSkge1xuICAgICAgICBhZGFwdGVyID0ga25vd25BZGFwdGVyc1soaWQgPSBTdHJpbmcobmFtZU9yQWRhcHRlcikpLnRvTG93ZXJDYXNlKCldO1xuXG4gICAgICAgIGlmIChhZGFwdGVyID09PSB1bmRlZmluZWQpIHtcbiAgICAgICAgICB0aHJvdyBuZXcgQXhpb3NFcnJvcihgVW5rbm93biBhZGFwdGVyICcke2lkfSdgKTtcbiAgICAgICAgfVxuICAgICAgfVxuXG4gICAgICBpZiAoYWRhcHRlcikge1xuICAgICAgICBicmVhaztcbiAgICAgIH1cblxuICAgICAgcmVqZWN0ZWRSZWFzb25zW2lkIHx8ICcjJyArIGldID0gYWRhcHRlcjtcbiAgICB9XG5cbiAgICBpZiAoIWFkYXB0ZXIpIHtcblxuICAgICAgY29uc3QgcmVhc29ucyA9IE9iamVjdC5lbnRyaWVzKHJlamVjdGVkUmVhc29ucylcbiAgICAgICAgLm1hcCgoW2lkLCBzdGF0ZV0pID0+IGBhZGFwdGVyICR7aWR9IGAgK1xuICAgICAgICAgIChzdGF0ZSA9PT0gZmFsc2UgPyAnaXMgbm90IHN1cHBvcnRlZCBieSB0aGUgZW52aXJvbm1lbnQnIDogJ2lzIG5vdCBhdmFpbGFibGUgaW4gdGhlIGJ1aWxkJylcbiAgICAgICAgKTtcblxuICAgICAgbGV0IHMgPSBsZW5ndGggP1xuICAgICAgICAocmVhc29ucy5sZW5ndGggPiAxID8gJ3NpbmNlIDpcXG4nICsgcmVhc29ucy5tYXAocmVuZGVyUmVhc29uKS5qb2luKCdcXG4nKSA6ICcgJyArIHJlbmRlclJlYXNvbihyZWFzb25zWzBdKSkgOlxuICAgICAgICAnYXMgbm8gYWRhcHRlciBzcGVjaWZpZWQnO1xuXG4gICAgICB0aHJvdyBuZXcgQXhpb3NFcnJvcihcbiAgICAgICAgYFRoZXJlIGlzIG5vIHN1aXRhYmxlIGFkYXB0ZXIgdG8gZGlzcGF0Y2ggdGhlIHJlcXVlc3QgYCArIHMsXG4gICAgICAgICdFUlJfTk9UX1NVUFBPUlQnXG4gICAgICApO1xuICAgIH1cblxuICAgIHJldHVybiBhZGFwdGVyO1xuICB9LFxuICBhZGFwdGVyczoga25vd25BZGFwdGVyc1xufTtcblxuLyoqXG4gKiBUaHJvd3MgYSBgQ2FuY2VsZWRFcnJvcmAgaWYgY2FuY2VsbGF0aW9uIGhhcyBiZWVuIHJlcXVlc3RlZC5cbiAqXG4gKiBAcGFyYW0ge09iamVjdH0gY29uZmlnIFRoZSBjb25maWcgdGhhdCBpcyB0byBiZSB1c2VkIGZvciB0aGUgcmVxdWVzdFxuICpcbiAqIEByZXR1cm5zIHt2b2lkfVxuICovXG5mdW5jdGlvbiB0aHJvd0lmQ2FuY2VsbGF0aW9uUmVxdWVzdGVkKGNvbmZpZykge1xuICBpZiAoY29uZmlnLmNhbmNlbFRva2VuKSB7XG4gICAgY29uZmlnLmNhbmNlbFRva2VuLnRocm93SWZSZXF1ZXN0ZWQoKTtcbiAgfVxuXG4gIGlmIChjb25maWcuc2lnbmFsICYmIGNvbmZpZy5zaWduYWwuYWJvcnRlZCkge1xuICAgIHRocm93IG5ldyBDYW5jZWxlZEVycm9yKG51bGwsIGNvbmZpZyk7XG4gIH1cbn1cblxuLyoqXG4gKiBEaXNwYXRjaCBhIHJlcXVlc3QgdG8gdGhlIHNlcnZlciB1c2luZyB0aGUgY29uZmlndXJlZCBhZGFwdGVyLlxuICpcbiAqIEBwYXJhbSB7b2JqZWN0fSBjb25maWcgVGhlIGNvbmZpZyB0aGF0IGlzIHRvIGJlIHVzZWQgZm9yIHRoZSByZXF1ZXN0XG4gKlxuICogQHJldHVybnMge1Byb21pc2V9IFRoZSBQcm9taXNlIHRvIGJlIGZ1bGZpbGxlZFxuICovXG5mdW5jdGlvbiBkaXNwYXRjaFJlcXVlc3QoY29uZmlnKSB7XG4gIHRocm93SWZDYW5jZWxsYXRpb25SZXF1ZXN0ZWQoY29uZmlnKTtcblxuICBjb25maWcuaGVhZGVycyA9IEF4aW9zSGVhZGVycyQxLmZyb20oY29uZmlnLmhlYWRlcnMpO1xuXG4gIC8vIFRyYW5zZm9ybSByZXF1ZXN0IGRhdGFcbiAgY29uZmlnLmRhdGEgPSB0cmFuc2Zvcm1EYXRhLmNhbGwoXG4gICAgY29uZmlnLFxuICAgIGNvbmZpZy50cmFuc2Zvcm1SZXF1ZXN0XG4gICk7XG5cbiAgaWYgKFsncG9zdCcsICdwdXQnLCAncGF0Y2gnXS5pbmRleE9mKGNvbmZpZy5tZXRob2QpICE9PSAtMSkge1xuICAgIGNvbmZpZy5oZWFkZXJzLnNldENvbnRlbnRUeXBlKCdhcHBsaWNhdGlvbi94LXd3dy1mb3JtLXVybGVuY29kZWQnLCBmYWxzZSk7XG4gIH1cblxuICBjb25zdCBhZGFwdGVyID0gYWRhcHRlcnMuZ2V0QWRhcHRlcihjb25maWcuYWRhcHRlciB8fCBkZWZhdWx0cyQxLmFkYXB0ZXIpO1xuXG4gIHJldHVybiBhZGFwdGVyKGNvbmZpZykudGhlbihmdW5jdGlvbiBvbkFkYXB0ZXJSZXNvbHV0aW9uKHJlc3BvbnNlKSB7XG4gICAgdGhyb3dJZkNhbmNlbGxhdGlvblJlcXVlc3RlZChjb25maWcpO1xuXG4gICAgLy8gVHJhbnNmb3JtIHJlc3BvbnNlIGRhdGFcbiAgICByZXNwb25zZS5kYXRhID0gdHJhbnNmb3JtRGF0YS5jYWxsKFxuICAgICAgY29uZmlnLFxuICAgICAgY29uZmlnLnRyYW5zZm9ybVJlc3BvbnNlLFxuICAgICAgcmVzcG9uc2VcbiAgICApO1xuXG4gICAgcmVzcG9uc2UuaGVhZGVycyA9IEF4aW9zSGVhZGVycyQxLmZyb20ocmVzcG9uc2UuaGVhZGVycyk7XG5cbiAgICByZXR1cm4gcmVzcG9uc2U7XG4gIH0sIGZ1bmN0aW9uIG9uQWRhcHRlclJlamVjdGlvbihyZWFzb24pIHtcbiAgICBpZiAoIWlzQ2FuY2VsKHJlYXNvbikpIHtcbiAgICAgIHRocm93SWZDYW5jZWxsYXRpb25SZXF1ZXN0ZWQoY29uZmlnKTtcblxuICAgICAgLy8gVHJhbnNmb3JtIHJlc3BvbnNlIGRhdGFcbiAgICAgIGlmIChyZWFzb24gJiYgcmVhc29uLnJlc3BvbnNlKSB7XG4gICAgICAgIHJlYXNvbi5yZXNwb25zZS5kYXRhID0gdHJhbnNmb3JtRGF0YS5jYWxsKFxuICAgICAgICAgIGNvbmZpZyxcbiAgICAgICAgICBjb25maWcudHJhbnNmb3JtUmVzcG9uc2UsXG4gICAgICAgICAgcmVhc29uLnJlc3BvbnNlXG4gICAgICAgICk7XG4gICAgICAgIHJlYXNvbi5yZXNwb25zZS5oZWFkZXJzID0gQXhpb3NIZWFkZXJzJDEuZnJvbShyZWFzb24ucmVzcG9uc2UuaGVhZGVycyk7XG4gICAgICB9XG4gICAgfVxuXG4gICAgcmV0dXJuIFByb21pc2UucmVqZWN0KHJlYXNvbik7XG4gIH0pO1xufVxuXG5jb25zdCBoZWFkZXJzVG9PYmplY3QgPSAodGhpbmcpID0+IHRoaW5nIGluc3RhbmNlb2YgQXhpb3NIZWFkZXJzJDEgPyB0aGluZy50b0pTT04oKSA6IHRoaW5nO1xuXG4vKipcbiAqIENvbmZpZy1zcGVjaWZpYyBtZXJnZS1mdW5jdGlvbiB3aGljaCBjcmVhdGVzIGEgbmV3IGNvbmZpZy1vYmplY3RcbiAqIGJ5IG1lcmdpbmcgdHdvIGNvbmZpZ3VyYXRpb24gb2JqZWN0cyB0b2dldGhlci5cbiAqXG4gKiBAcGFyYW0ge09iamVjdH0gY29uZmlnMVxuICogQHBhcmFtIHtPYmplY3R9IGNvbmZpZzJcbiAqXG4gKiBAcmV0dXJucyB7T2JqZWN0fSBOZXcgb2JqZWN0IHJlc3VsdGluZyBmcm9tIG1lcmdpbmcgY29uZmlnMiB0byBjb25maWcxXG4gKi9cbmZ1bmN0aW9uIG1lcmdlQ29uZmlnKGNvbmZpZzEsIGNvbmZpZzIpIHtcbiAgLy8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIG5vLXBhcmFtLXJlYXNzaWduXG4gIGNvbmZpZzIgPSBjb25maWcyIHx8IHt9O1xuICBjb25zdCBjb25maWcgPSB7fTtcblxuICBmdW5jdGlvbiBnZXRNZXJnZWRWYWx1ZSh0YXJnZXQsIHNvdXJjZSwgY2FzZWxlc3MpIHtcbiAgICBpZiAodXRpbHMuaXNQbGFpbk9iamVjdCh0YXJnZXQpICYmIHV0aWxzLmlzUGxhaW5PYmplY3Qoc291cmNlKSkge1xuICAgICAgcmV0dXJuIHV0aWxzLm1lcmdlLmNhbGwoe2Nhc2VsZXNzfSwgdGFyZ2V0LCBzb3VyY2UpO1xuICAgIH0gZWxzZSBpZiAodXRpbHMuaXNQbGFpbk9iamVjdChzb3VyY2UpKSB7XG4gICAgICByZXR1cm4gdXRpbHMubWVyZ2Uoe30sIHNvdXJjZSk7XG4gICAgfSBlbHNlIGlmICh1dGlscy5pc0FycmF5KHNvdXJjZSkpIHtcbiAgICAgIHJldHVybiBzb3VyY2Uuc2xpY2UoKTtcbiAgICB9XG4gICAgcmV0dXJuIHNvdXJjZTtcbiAgfVxuXG4gIC8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBjb25zaXN0ZW50LXJldHVyblxuICBmdW5jdGlvbiBtZXJnZURlZXBQcm9wZXJ0aWVzKGEsIGIsIGNhc2VsZXNzKSB7XG4gICAgaWYgKCF1dGlscy5pc1VuZGVmaW5lZChiKSkge1xuICAgICAgcmV0dXJuIGdldE1lcmdlZFZhbHVlKGEsIGIsIGNhc2VsZXNzKTtcbiAgICB9IGVsc2UgaWYgKCF1dGlscy5pc1VuZGVmaW5lZChhKSkge1xuICAgICAgcmV0dXJuIGdldE1lcmdlZFZhbHVlKHVuZGVmaW5lZCwgYSwgY2FzZWxlc3MpO1xuICAgIH1cbiAgfVxuXG4gIC8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBjb25zaXN0ZW50LXJldHVyblxuICBmdW5jdGlvbiB2YWx1ZUZyb21Db25maWcyKGEsIGIpIHtcbiAgICBpZiAoIXV0aWxzLmlzVW5kZWZpbmVkKGIpKSB7XG4gICAgICByZXR1cm4gZ2V0TWVyZ2VkVmFsdWUodW5kZWZpbmVkLCBiKTtcbiAgICB9XG4gIH1cblxuICAvLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgY29uc2lzdGVudC1yZXR1cm5cbiAgZnVuY3Rpb24gZGVmYXVsdFRvQ29uZmlnMihhLCBiKSB7XG4gICAgaWYgKCF1dGlscy5pc1VuZGVmaW5lZChiKSkge1xuICAgICAgcmV0dXJuIGdldE1lcmdlZFZhbHVlKHVuZGVmaW5lZCwgYik7XG4gICAgfSBlbHNlIGlmICghdXRpbHMuaXNVbmRlZmluZWQoYSkpIHtcbiAgICAgIHJldHVybiBnZXRNZXJnZWRWYWx1ZSh1bmRlZmluZWQsIGEpO1xuICAgIH1cbiAgfVxuXG4gIC8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBjb25zaXN0ZW50LXJldHVyblxuICBmdW5jdGlvbiBtZXJnZURpcmVjdEtleXMoYSwgYiwgcHJvcCkge1xuICAgIGlmIChwcm9wIGluIGNvbmZpZzIpIHtcbiAgICAgIHJldHVybiBnZXRNZXJnZWRWYWx1ZShhLCBiKTtcbiAgICB9IGVsc2UgaWYgKHByb3AgaW4gY29uZmlnMSkge1xuICAgICAgcmV0dXJuIGdldE1lcmdlZFZhbHVlKHVuZGVmaW5lZCwgYSk7XG4gICAgfVxuICB9XG5cbiAgY29uc3QgbWVyZ2VNYXAgPSB7XG4gICAgdXJsOiB2YWx1ZUZyb21Db25maWcyLFxuICAgIG1ldGhvZDogdmFsdWVGcm9tQ29uZmlnMixcbiAgICBkYXRhOiB2YWx1ZUZyb21Db25maWcyLFxuICAgIGJhc2VVUkw6IGRlZmF1bHRUb0NvbmZpZzIsXG4gICAgdHJhbnNmb3JtUmVxdWVzdDogZGVmYXVsdFRvQ29uZmlnMixcbiAgICB0cmFuc2Zvcm1SZXNwb25zZTogZGVmYXVsdFRvQ29uZmlnMixcbiAgICBwYXJhbXNTZXJpYWxpemVyOiBkZWZhdWx0VG9Db25maWcyLFxuICAgIHRpbWVvdXQ6IGRlZmF1bHRUb0NvbmZpZzIsXG4gICAgdGltZW91dE1lc3NhZ2U6IGRlZmF1bHRUb0NvbmZpZzIsXG4gICAgd2l0aENyZWRlbnRpYWxzOiBkZWZhdWx0VG9Db25maWcyLFxuICAgIGFkYXB0ZXI6IGRlZmF1bHRUb0NvbmZpZzIsXG4gICAgcmVzcG9uc2VUeXBlOiBkZWZhdWx0VG9Db25maWcyLFxuICAgIHhzcmZDb29raWVOYW1lOiBkZWZhdWx0VG9Db25maWcyLFxuICAgIHhzcmZIZWFkZXJOYW1lOiBkZWZhdWx0VG9Db25maWcyLFxuICAgIG9uVXBsb2FkUHJvZ3Jlc3M6IGRlZmF1bHRUb0NvbmZpZzIsXG4gICAgb25Eb3dubG9hZFByb2dyZXNzOiBkZWZhdWx0VG9Db25maWcyLFxuICAgIGRlY29tcHJlc3M6IGRlZmF1bHRUb0NvbmZpZzIsXG4gICAgbWF4Q29udGVudExlbmd0aDogZGVmYXVsdFRvQ29uZmlnMixcbiAgICBtYXhCb2R5TGVuZ3RoOiBkZWZhdWx0VG9Db25maWcyLFxuICAgIGJlZm9yZVJlZGlyZWN0OiBkZWZhdWx0VG9Db25maWcyLFxuICAgIHRyYW5zcG9ydDogZGVmYXVsdFRvQ29uZmlnMixcbiAgICBodHRwQWdlbnQ6IGRlZmF1bHRUb0NvbmZpZzIsXG4gICAgaHR0cHNBZ2VudDogZGVmYXVsdFRvQ29uZmlnMixcbiAgICBjYW5jZWxUb2tlbjogZGVmYXVsdFRvQ29uZmlnMixcbiAgICBzb2NrZXRQYXRoOiBkZWZhdWx0VG9Db25maWcyLFxuICAgIHJlc3BvbnNlRW5jb2Rpbmc6IGRlZmF1bHRUb0NvbmZpZzIsXG4gICAgdmFsaWRhdGVTdGF0dXM6IG1lcmdlRGlyZWN0S2V5cyxcbiAgICBoZWFkZXJzOiAoYSwgYikgPT4gbWVyZ2VEZWVwUHJvcGVydGllcyhoZWFkZXJzVG9PYmplY3QoYSksIGhlYWRlcnNUb09iamVjdChiKSwgdHJ1ZSlcbiAgfTtcblxuICB1dGlscy5mb3JFYWNoKE9iamVjdC5rZXlzKE9iamVjdC5hc3NpZ24oe30sIGNvbmZpZzEsIGNvbmZpZzIpKSwgZnVuY3Rpb24gY29tcHV0ZUNvbmZpZ1ZhbHVlKHByb3ApIHtcbiAgICBjb25zdCBtZXJnZSA9IG1lcmdlTWFwW3Byb3BdIHx8IG1lcmdlRGVlcFByb3BlcnRpZXM7XG4gICAgY29uc3QgY29uZmlnVmFsdWUgPSBtZXJnZShjb25maWcxW3Byb3BdLCBjb25maWcyW3Byb3BdLCBwcm9wKTtcbiAgICAodXRpbHMuaXNVbmRlZmluZWQoY29uZmlnVmFsdWUpICYmIG1lcmdlICE9PSBtZXJnZURpcmVjdEtleXMpIHx8IChjb25maWdbcHJvcF0gPSBjb25maWdWYWx1ZSk7XG4gIH0pO1xuXG4gIHJldHVybiBjb25maWc7XG59XG5cbmNvbnN0IHZhbGlkYXRvcnMkMSA9IHt9O1xuXG4vLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgZnVuYy1uYW1lc1xuWydvYmplY3QnLCAnYm9vbGVhbicsICdudW1iZXInLCAnZnVuY3Rpb24nLCAnc3RyaW5nJywgJ3N5bWJvbCddLmZvckVhY2goKHR5cGUsIGkpID0+IHtcbiAgdmFsaWRhdG9ycyQxW3R5cGVdID0gZnVuY3Rpb24gdmFsaWRhdG9yKHRoaW5nKSB7XG4gICAgcmV0dXJuIHR5cGVvZiB0aGluZyA9PT0gdHlwZSB8fCAnYScgKyAoaSA8IDEgPyAnbiAnIDogJyAnKSArIHR5cGU7XG4gIH07XG59KTtcblxuY29uc3QgZGVwcmVjYXRlZFdhcm5pbmdzID0ge307XG5cbi8qKlxuICogVHJhbnNpdGlvbmFsIG9wdGlvbiB2YWxpZGF0b3JcbiAqXG4gKiBAcGFyYW0ge2Z1bmN0aW9ufGJvb2xlYW4/fSB2YWxpZGF0b3IgLSBzZXQgdG8gZmFsc2UgaWYgdGhlIHRyYW5zaXRpb25hbCBvcHRpb24gaGFzIGJlZW4gcmVtb3ZlZFxuICogQHBhcmFtIHtzdHJpbmc/fSB2ZXJzaW9uIC0gZGVwcmVjYXRlZCB2ZXJzaW9uIC8gcmVtb3ZlZCBzaW5jZSB2ZXJzaW9uXG4gKiBAcGFyYW0ge3N0cmluZz99IG1lc3NhZ2UgLSBzb21lIG1lc3NhZ2Ugd2l0aCBhZGRpdGlvbmFsIGluZm9cbiAqXG4gKiBAcmV0dXJucyB7ZnVuY3Rpb259XG4gKi9cbnZhbGlkYXRvcnMkMS50cmFuc2l0aW9uYWwgPSBmdW5jdGlvbiB0cmFuc2l0aW9uYWwodmFsaWRhdG9yLCB2ZXJzaW9uLCBtZXNzYWdlKSB7XG4gIGZ1bmN0aW9uIGZvcm1hdE1lc3NhZ2Uob3B0LCBkZXNjKSB7XG4gICAgcmV0dXJuICdbQXhpb3MgdicgKyBWRVJTSU9OICsgJ10gVHJhbnNpdGlvbmFsIG9wdGlvbiBcXCcnICsgb3B0ICsgJ1xcJycgKyBkZXNjICsgKG1lc3NhZ2UgPyAnLiAnICsgbWVzc2FnZSA6ICcnKTtcbiAgfVxuXG4gIC8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBmdW5jLW5hbWVzXG4gIHJldHVybiAodmFsdWUsIG9wdCwgb3B0cykgPT4ge1xuICAgIGlmICh2YWxpZGF0b3IgPT09IGZhbHNlKSB7XG4gICAgICB0aHJvdyBuZXcgQXhpb3NFcnJvcihcbiAgICAgICAgZm9ybWF0TWVzc2FnZShvcHQsICcgaGFzIGJlZW4gcmVtb3ZlZCcgKyAodmVyc2lvbiA/ICcgaW4gJyArIHZlcnNpb24gOiAnJykpLFxuICAgICAgICBBeGlvc0Vycm9yLkVSUl9ERVBSRUNBVEVEXG4gICAgICApO1xuICAgIH1cblxuICAgIGlmICh2ZXJzaW9uICYmICFkZXByZWNhdGVkV2FybmluZ3Nbb3B0XSkge1xuICAgICAgZGVwcmVjYXRlZFdhcm5pbmdzW29wdF0gPSB0cnVlO1xuICAgICAgLy8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIG5vLWNvbnNvbGVcbiAgICAgIGNvbnNvbGUud2FybihcbiAgICAgICAgZm9ybWF0TWVzc2FnZShcbiAgICAgICAgICBvcHQsXG4gICAgICAgICAgJyBoYXMgYmVlbiBkZXByZWNhdGVkIHNpbmNlIHYnICsgdmVyc2lvbiArICcgYW5kIHdpbGwgYmUgcmVtb3ZlZCBpbiB0aGUgbmVhciBmdXR1cmUnXG4gICAgICAgIClcbiAgICAgICk7XG4gICAgfVxuXG4gICAgcmV0dXJuIHZhbGlkYXRvciA/IHZhbGlkYXRvcih2YWx1ZSwgb3B0LCBvcHRzKSA6IHRydWU7XG4gIH07XG59O1xuXG4vKipcbiAqIEFzc2VydCBvYmplY3QncyBwcm9wZXJ0aWVzIHR5cGVcbiAqXG4gKiBAcGFyYW0ge29iamVjdH0gb3B0aW9uc1xuICogQHBhcmFtIHtvYmplY3R9IHNjaGVtYVxuICogQHBhcmFtIHtib29sZWFuP30gYWxsb3dVbmtub3duXG4gKlxuICogQHJldHVybnMge29iamVjdH1cbiAqL1xuXG5mdW5jdGlvbiBhc3NlcnRPcHRpb25zKG9wdGlvbnMsIHNjaGVtYSwgYWxsb3dVbmtub3duKSB7XG4gIGlmICh0eXBlb2Ygb3B0aW9ucyAhPT0gJ29iamVjdCcpIHtcbiAgICB0aHJvdyBuZXcgQXhpb3NFcnJvcignb3B0aW9ucyBtdXN0IGJlIGFuIG9iamVjdCcsIEF4aW9zRXJyb3IuRVJSX0JBRF9PUFRJT05fVkFMVUUpO1xuICB9XG4gIGNvbnN0IGtleXMgPSBPYmplY3Qua2V5cyhvcHRpb25zKTtcbiAgbGV0IGkgPSBrZXlzLmxlbmd0aDtcbiAgd2hpbGUgKGktLSA+IDApIHtcbiAgICBjb25zdCBvcHQgPSBrZXlzW2ldO1xuICAgIGNvbnN0IHZhbGlkYXRvciA9IHNjaGVtYVtvcHRdO1xuICAgIGlmICh2YWxpZGF0b3IpIHtcbiAgICAgIGNvbnN0IHZhbHVlID0gb3B0aW9uc1tvcHRdO1xuICAgICAgY29uc3QgcmVzdWx0ID0gdmFsdWUgPT09IHVuZGVmaW5lZCB8fCB2YWxpZGF0b3IodmFsdWUsIG9wdCwgb3B0aW9ucyk7XG4gICAgICBpZiAocmVzdWx0ICE9PSB0cnVlKSB7XG4gICAgICAgIHRocm93IG5ldyBBeGlvc0Vycm9yKCdvcHRpb24gJyArIG9wdCArICcgbXVzdCBiZSAnICsgcmVzdWx0LCBBeGlvc0Vycm9yLkVSUl9CQURfT1BUSU9OX1ZBTFVFKTtcbiAgICAgIH1cbiAgICAgIGNvbnRpbnVlO1xuICAgIH1cbiAgICBpZiAoYWxsb3dVbmtub3duICE9PSB0cnVlKSB7XG4gICAgICB0aHJvdyBuZXcgQXhpb3NFcnJvcignVW5rbm93biBvcHRpb24gJyArIG9wdCwgQXhpb3NFcnJvci5FUlJfQkFEX09QVElPTik7XG4gICAgfVxuICB9XG59XG5cbmNvbnN0IHZhbGlkYXRvciA9IHtcbiAgYXNzZXJ0T3B0aW9ucyxcbiAgdmFsaWRhdG9yczogdmFsaWRhdG9ycyQxXG59O1xuXG5jb25zdCB2YWxpZGF0b3JzID0gdmFsaWRhdG9yLnZhbGlkYXRvcnM7XG5cbi8qKlxuICogQ3JlYXRlIGEgbmV3IGluc3RhbmNlIG9mIEF4aW9zXG4gKlxuICogQHBhcmFtIHtPYmplY3R9IGluc3RhbmNlQ29uZmlnIFRoZSBkZWZhdWx0IGNvbmZpZyBmb3IgdGhlIGluc3RhbmNlXG4gKlxuICogQHJldHVybiB7QXhpb3N9IEEgbmV3IGluc3RhbmNlIG9mIEF4aW9zXG4gKi9cbmNsYXNzIEF4aW9zIHtcbiAgY29uc3RydWN0b3IoaW5zdGFuY2VDb25maWcpIHtcbiAgICB0aGlzLmRlZmF1bHRzID0gaW5zdGFuY2VDb25maWc7XG4gICAgdGhpcy5pbnRlcmNlcHRvcnMgPSB7XG4gICAgICByZXF1ZXN0OiBuZXcgSW50ZXJjZXB0b3JNYW5hZ2VyJDEoKSxcbiAgICAgIHJlc3BvbnNlOiBuZXcgSW50ZXJjZXB0b3JNYW5hZ2VyJDEoKVxuICAgIH07XG4gIH1cblxuICAvKipcbiAgICogRGlzcGF0Y2ggYSByZXF1ZXN0XG4gICAqXG4gICAqIEBwYXJhbSB7U3RyaW5nfE9iamVjdH0gY29uZmlnT3JVcmwgVGhlIGNvbmZpZyBzcGVjaWZpYyBmb3IgdGhpcyByZXF1ZXN0IChtZXJnZWQgd2l0aCB0aGlzLmRlZmF1bHRzKVxuICAgKiBAcGFyYW0gez9PYmplY3R9IGNvbmZpZ1xuICAgKlxuICAgKiBAcmV0dXJucyB7UHJvbWlzZX0gVGhlIFByb21pc2UgdG8gYmUgZnVsZmlsbGVkXG4gICAqL1xuICByZXF1ZXN0KGNvbmZpZ09yVXJsLCBjb25maWcpIHtcbiAgICAvKmVzbGludCBuby1wYXJhbS1yZWFzc2lnbjowKi9cbiAgICAvLyBBbGxvdyBmb3IgYXhpb3MoJ2V4YW1wbGUvdXJsJ1ssIGNvbmZpZ10pIGEgbGEgZmV0Y2ggQVBJXG4gICAgaWYgKHR5cGVvZiBjb25maWdPclVybCA9PT0gJ3N0cmluZycpIHtcbiAgICAgIGNvbmZpZyA9IGNvbmZpZyB8fCB7fTtcbiAgICAgIGNvbmZpZy51cmwgPSBjb25maWdPclVybDtcbiAgICB9IGVsc2Uge1xuICAgICAgY29uZmlnID0gY29uZmlnT3JVcmwgfHwge307XG4gICAgfVxuXG4gICAgY29uZmlnID0gbWVyZ2VDb25maWcodGhpcy5kZWZhdWx0cywgY29uZmlnKTtcblxuICAgIGNvbnN0IHt0cmFuc2l0aW9uYWwsIHBhcmFtc1NlcmlhbGl6ZXIsIGhlYWRlcnN9ID0gY29uZmlnO1xuXG4gICAgaWYgKHRyYW5zaXRpb25hbCAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICB2YWxpZGF0b3IuYXNzZXJ0T3B0aW9ucyh0cmFuc2l0aW9uYWwsIHtcbiAgICAgICAgc2lsZW50SlNPTlBhcnNpbmc6IHZhbGlkYXRvcnMudHJhbnNpdGlvbmFsKHZhbGlkYXRvcnMuYm9vbGVhbiksXG4gICAgICAgIGZvcmNlZEpTT05QYXJzaW5nOiB2YWxpZGF0b3JzLnRyYW5zaXRpb25hbCh2YWxpZGF0b3JzLmJvb2xlYW4pLFxuICAgICAgICBjbGFyaWZ5VGltZW91dEVycm9yOiB2YWxpZGF0b3JzLnRyYW5zaXRpb25hbCh2YWxpZGF0b3JzLmJvb2xlYW4pXG4gICAgICB9LCBmYWxzZSk7XG4gICAgfVxuXG4gICAgaWYgKHBhcmFtc1NlcmlhbGl6ZXIgIT0gbnVsbCkge1xuICAgICAgaWYgKHV0aWxzLmlzRnVuY3Rpb24ocGFyYW1zU2VyaWFsaXplcikpIHtcbiAgICAgICAgY29uZmlnLnBhcmFtc1NlcmlhbGl6ZXIgPSB7XG4gICAgICAgICAgc2VyaWFsaXplOiBwYXJhbXNTZXJpYWxpemVyXG4gICAgICAgIH07XG4gICAgICB9IGVsc2Uge1xuICAgICAgICB2YWxpZGF0b3IuYXNzZXJ0T3B0aW9ucyhwYXJhbXNTZXJpYWxpemVyLCB7XG4gICAgICAgICAgZW5jb2RlOiB2YWxpZGF0b3JzLmZ1bmN0aW9uLFxuICAgICAgICAgIHNlcmlhbGl6ZTogdmFsaWRhdG9ycy5mdW5jdGlvblxuICAgICAgICB9LCB0cnVlKTtcbiAgICAgIH1cbiAgICB9XG5cbiAgICAvLyBTZXQgY29uZmlnLm1ldGhvZFxuICAgIGNvbmZpZy5tZXRob2QgPSAoY29uZmlnLm1ldGhvZCB8fCB0aGlzLmRlZmF1bHRzLm1ldGhvZCB8fCAnZ2V0JykudG9Mb3dlckNhc2UoKTtcblxuICAgIC8vIEZsYXR0ZW4gaGVhZGVyc1xuICAgIGxldCBjb250ZXh0SGVhZGVycyA9IGhlYWRlcnMgJiYgdXRpbHMubWVyZ2UoXG4gICAgICBoZWFkZXJzLmNvbW1vbixcbiAgICAgIGhlYWRlcnNbY29uZmlnLm1ldGhvZF1cbiAgICApO1xuXG4gICAgaGVhZGVycyAmJiB1dGlscy5mb3JFYWNoKFxuICAgICAgWydkZWxldGUnLCAnZ2V0JywgJ2hlYWQnLCAncG9zdCcsICdwdXQnLCAncGF0Y2gnLCAnY29tbW9uJ10sXG4gICAgICAobWV0aG9kKSA9PiB7XG4gICAgICAgIGRlbGV0ZSBoZWFkZXJzW21ldGhvZF07XG4gICAgICB9XG4gICAgKTtcblxuICAgIGNvbmZpZy5oZWFkZXJzID0gQXhpb3NIZWFkZXJzJDEuY29uY2F0KGNvbnRleHRIZWFkZXJzLCBoZWFkZXJzKTtcblxuICAgIC8vIGZpbHRlciBvdXQgc2tpcHBlZCBpbnRlcmNlcHRvcnNcbiAgICBjb25zdCByZXF1ZXN0SW50ZXJjZXB0b3JDaGFpbiA9IFtdO1xuICAgIGxldCBzeW5jaHJvbm91c1JlcXVlc3RJbnRlcmNlcHRvcnMgPSB0cnVlO1xuICAgIHRoaXMuaW50ZXJjZXB0b3JzLnJlcXVlc3QuZm9yRWFjaChmdW5jdGlvbiB1bnNoaWZ0UmVxdWVzdEludGVyY2VwdG9ycyhpbnRlcmNlcHRvcikge1xuICAgICAgaWYgKHR5cGVvZiBpbnRlcmNlcHRvci5ydW5XaGVuID09PSAnZnVuY3Rpb24nICYmIGludGVyY2VwdG9yLnJ1bldoZW4oY29uZmlnKSA9PT0gZmFsc2UpIHtcbiAgICAgICAgcmV0dXJuO1xuICAgICAgfVxuXG4gICAgICBzeW5jaHJvbm91c1JlcXVlc3RJbnRlcmNlcHRvcnMgPSBzeW5jaHJvbm91c1JlcXVlc3RJbnRlcmNlcHRvcnMgJiYgaW50ZXJjZXB0b3Iuc3luY2hyb25vdXM7XG5cbiAgICAgIHJlcXVlc3RJbnRlcmNlcHRvckNoYWluLnVuc2hpZnQoaW50ZXJjZXB0b3IuZnVsZmlsbGVkLCBpbnRlcmNlcHRvci5yZWplY3RlZCk7XG4gICAgfSk7XG5cbiAgICBjb25zdCByZXNwb25zZUludGVyY2VwdG9yQ2hhaW4gPSBbXTtcbiAgICB0aGlzLmludGVyY2VwdG9ycy5yZXNwb25zZS5mb3JFYWNoKGZ1bmN0aW9uIHB1c2hSZXNwb25zZUludGVyY2VwdG9ycyhpbnRlcmNlcHRvcikge1xuICAgICAgcmVzcG9uc2VJbnRlcmNlcHRvckNoYWluLnB1c2goaW50ZXJjZXB0b3IuZnVsZmlsbGVkLCBpbnRlcmNlcHRvci5yZWplY3RlZCk7XG4gICAgfSk7XG5cbiAgICBsZXQgcHJvbWlzZTtcbiAgICBsZXQgaSA9IDA7XG4gICAgbGV0IGxlbjtcblxuICAgIGlmICghc3luY2hyb25vdXNSZXF1ZXN0SW50ZXJjZXB0b3JzKSB7XG4gICAgICBjb25zdCBjaGFpbiA9IFtkaXNwYXRjaFJlcXVlc3QuYmluZCh0aGlzKSwgdW5kZWZpbmVkXTtcbiAgICAgIGNoYWluLnVuc2hpZnQuYXBwbHkoY2hhaW4sIHJlcXVlc3RJbnRlcmNlcHRvckNoYWluKTtcbiAgICAgIGNoYWluLnB1c2guYXBwbHkoY2hhaW4sIHJlc3BvbnNlSW50ZXJjZXB0b3JDaGFpbik7XG4gICAgICBsZW4gPSBjaGFpbi5sZW5ndGg7XG5cbiAgICAgIHByb21pc2UgPSBQcm9taXNlLnJlc29sdmUoY29uZmlnKTtcblxuICAgICAgd2hpbGUgKGkgPCBsZW4pIHtcbiAgICAgICAgcHJvbWlzZSA9IHByb21pc2UudGhlbihjaGFpbltpKytdLCBjaGFpbltpKytdKTtcbiAgICAgIH1cblxuICAgICAgcmV0dXJuIHByb21pc2U7XG4gICAgfVxuXG4gICAgbGVuID0gcmVxdWVzdEludGVyY2VwdG9yQ2hhaW4ubGVuZ3RoO1xuXG4gICAgbGV0IG5ld0NvbmZpZyA9IGNvbmZpZztcblxuICAgIGkgPSAwO1xuXG4gICAgd2hpbGUgKGkgPCBsZW4pIHtcbiAgICAgIGNvbnN0IG9uRnVsZmlsbGVkID0gcmVxdWVzdEludGVyY2VwdG9yQ2hhaW5baSsrXTtcbiAgICAgIGNvbnN0IG9uUmVqZWN0ZWQgPSByZXF1ZXN0SW50ZXJjZXB0b3JDaGFpbltpKytdO1xuICAgICAgdHJ5IHtcbiAgICAgICAgbmV3Q29uZmlnID0gb25GdWxmaWxsZWQobmV3Q29uZmlnKTtcbiAgICAgIH0gY2F0Y2ggKGVycm9yKSB7XG4gICAgICAgIG9uUmVqZWN0ZWQuY2FsbCh0aGlzLCBlcnJvcik7XG4gICAgICAgIGJyZWFrO1xuICAgICAgfVxuICAgIH1cblxuICAgIHRyeSB7XG4gICAgICBwcm9taXNlID0gZGlzcGF0Y2hSZXF1ZXN0LmNhbGwodGhpcywgbmV3Q29uZmlnKTtcbiAgICB9IGNhdGNoIChlcnJvcikge1xuICAgICAgcmV0dXJuIFByb21pc2UucmVqZWN0KGVycm9yKTtcbiAgICB9XG5cbiAgICBpID0gMDtcbiAgICBsZW4gPSByZXNwb25zZUludGVyY2VwdG9yQ2hhaW4ubGVuZ3RoO1xuXG4gICAgd2hpbGUgKGkgPCBsZW4pIHtcbiAgICAgIHByb21pc2UgPSBwcm9taXNlLnRoZW4ocmVzcG9uc2VJbnRlcmNlcHRvckNoYWluW2krK10sIHJlc3BvbnNlSW50ZXJjZXB0b3JDaGFpbltpKytdKTtcbiAgICB9XG5cbiAgICByZXR1cm4gcHJvbWlzZTtcbiAgfVxuXG4gIGdldFVyaShjb25maWcpIHtcbiAgICBjb25maWcgPSBtZXJnZUNvbmZpZyh0aGlzLmRlZmF1bHRzLCBjb25maWcpO1xuICAgIGNvbnN0IGZ1bGxQYXRoID0gYnVpbGRGdWxsUGF0aChjb25maWcuYmFzZVVSTCwgY29uZmlnLnVybCk7XG4gICAgcmV0dXJuIGJ1aWxkVVJMKGZ1bGxQYXRoLCBjb25maWcucGFyYW1zLCBjb25maWcucGFyYW1zU2VyaWFsaXplcik7XG4gIH1cbn1cblxuLy8gUHJvdmlkZSBhbGlhc2VzIGZvciBzdXBwb3J0ZWQgcmVxdWVzdCBtZXRob2RzXG51dGlscy5mb3JFYWNoKFsnZGVsZXRlJywgJ2dldCcsICdoZWFkJywgJ29wdGlvbnMnXSwgZnVuY3Rpb24gZm9yRWFjaE1ldGhvZE5vRGF0YShtZXRob2QpIHtcbiAgLyplc2xpbnQgZnVuYy1uYW1lczowKi9cbiAgQXhpb3MucHJvdG90eXBlW21ldGhvZF0gPSBmdW5jdGlvbih1cmwsIGNvbmZpZykge1xuICAgIHJldHVybiB0aGlzLnJlcXVlc3QobWVyZ2VDb25maWcoY29uZmlnIHx8IHt9LCB7XG4gICAgICBtZXRob2QsXG4gICAgICB1cmwsXG4gICAgICBkYXRhOiAoY29uZmlnIHx8IHt9KS5kYXRhXG4gICAgfSkpO1xuICB9O1xufSk7XG5cbnV0aWxzLmZvckVhY2goWydwb3N0JywgJ3B1dCcsICdwYXRjaCddLCBmdW5jdGlvbiBmb3JFYWNoTWV0aG9kV2l0aERhdGEobWV0aG9kKSB7XG4gIC8qZXNsaW50IGZ1bmMtbmFtZXM6MCovXG5cbiAgZnVuY3Rpb24gZ2VuZXJhdGVIVFRQTWV0aG9kKGlzRm9ybSkge1xuICAgIHJldHVybiBmdW5jdGlvbiBodHRwTWV0aG9kKHVybCwgZGF0YSwgY29uZmlnKSB7XG4gICAgICByZXR1cm4gdGhpcy5yZXF1ZXN0KG1lcmdlQ29uZmlnKGNvbmZpZyB8fCB7fSwge1xuICAgICAgICBtZXRob2QsXG4gICAgICAgIGhlYWRlcnM6IGlzRm9ybSA/IHtcbiAgICAgICAgICAnQ29udGVudC1UeXBlJzogJ211bHRpcGFydC9mb3JtLWRhdGEnXG4gICAgICAgIH0gOiB7fSxcbiAgICAgICAgdXJsLFxuICAgICAgICBkYXRhXG4gICAgICB9KSk7XG4gICAgfTtcbiAgfVxuXG4gIEF4aW9zLnByb3RvdHlwZVttZXRob2RdID0gZ2VuZXJhdGVIVFRQTWV0aG9kKCk7XG5cbiAgQXhpb3MucHJvdG90eXBlW21ldGhvZCArICdGb3JtJ10gPSBnZW5lcmF0ZUhUVFBNZXRob2QodHJ1ZSk7XG59KTtcblxuY29uc3QgQXhpb3MkMSA9IEF4aW9zO1xuXG4vKipcbiAqIEEgYENhbmNlbFRva2VuYCBpcyBhbiBvYmplY3QgdGhhdCBjYW4gYmUgdXNlZCB0byByZXF1ZXN0IGNhbmNlbGxhdGlvbiBvZiBhbiBvcGVyYXRpb24uXG4gKlxuICogQHBhcmFtIHtGdW5jdGlvbn0gZXhlY3V0b3IgVGhlIGV4ZWN1dG9yIGZ1bmN0aW9uLlxuICpcbiAqIEByZXR1cm5zIHtDYW5jZWxUb2tlbn1cbiAqL1xuY2xhc3MgQ2FuY2VsVG9rZW4ge1xuICBjb25zdHJ1Y3RvcihleGVjdXRvcikge1xuICAgIGlmICh0eXBlb2YgZXhlY3V0b3IgIT09ICdmdW5jdGlvbicpIHtcbiAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ2V4ZWN1dG9yIG11c3QgYmUgYSBmdW5jdGlvbi4nKTtcbiAgICB9XG5cbiAgICBsZXQgcmVzb2x2ZVByb21pc2U7XG5cbiAgICB0aGlzLnByb21pc2UgPSBuZXcgUHJvbWlzZShmdW5jdGlvbiBwcm9taXNlRXhlY3V0b3IocmVzb2x2ZSkge1xuICAgICAgcmVzb2x2ZVByb21pc2UgPSByZXNvbHZlO1xuICAgIH0pO1xuXG4gICAgY29uc3QgdG9rZW4gPSB0aGlzO1xuXG4gICAgLy8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIGZ1bmMtbmFtZXNcbiAgICB0aGlzLnByb21pc2UudGhlbihjYW5jZWwgPT4ge1xuICAgICAgaWYgKCF0b2tlbi5fbGlzdGVuZXJzKSByZXR1cm47XG5cbiAgICAgIGxldCBpID0gdG9rZW4uX2xpc3RlbmVycy5sZW5ndGg7XG5cbiAgICAgIHdoaWxlIChpLS0gPiAwKSB7XG4gICAgICAgIHRva2VuLl9saXN0ZW5lcnNbaV0oY2FuY2VsKTtcbiAgICAgIH1cbiAgICAgIHRva2VuLl9saXN0ZW5lcnMgPSBudWxsO1xuICAgIH0pO1xuXG4gICAgLy8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIGZ1bmMtbmFtZXNcbiAgICB0aGlzLnByb21pc2UudGhlbiA9IG9uZnVsZmlsbGVkID0+IHtcbiAgICAgIGxldCBfcmVzb2x2ZTtcbiAgICAgIC8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBmdW5jLW5hbWVzXG4gICAgICBjb25zdCBwcm9taXNlID0gbmV3IFByb21pc2UocmVzb2x2ZSA9PiB7XG4gICAgICAgIHRva2VuLnN1YnNjcmliZShyZXNvbHZlKTtcbiAgICAgICAgX3Jlc29sdmUgPSByZXNvbHZlO1xuICAgICAgfSkudGhlbihvbmZ1bGZpbGxlZCk7XG5cbiAgICAgIHByb21pc2UuY2FuY2VsID0gZnVuY3Rpb24gcmVqZWN0KCkge1xuICAgICAgICB0b2tlbi51bnN1YnNjcmliZShfcmVzb2x2ZSk7XG4gICAgICB9O1xuXG4gICAgICByZXR1cm4gcHJvbWlzZTtcbiAgICB9O1xuXG4gICAgZXhlY3V0b3IoZnVuY3Rpb24gY2FuY2VsKG1lc3NhZ2UsIGNvbmZpZywgcmVxdWVzdCkge1xuICAgICAgaWYgKHRva2VuLnJlYXNvbikge1xuICAgICAgICAvLyBDYW5jZWxsYXRpb24gaGFzIGFscmVhZHkgYmVlbiByZXF1ZXN0ZWRcbiAgICAgICAgcmV0dXJuO1xuICAgICAgfVxuXG4gICAgICB0b2tlbi5yZWFzb24gPSBuZXcgQ2FuY2VsZWRFcnJvcihtZXNzYWdlLCBjb25maWcsIHJlcXVlc3QpO1xuICAgICAgcmVzb2x2ZVByb21pc2UodG9rZW4ucmVhc29uKTtcbiAgICB9KTtcbiAgfVxuXG4gIC8qKlxuICAgKiBUaHJvd3MgYSBgQ2FuY2VsZWRFcnJvcmAgaWYgY2FuY2VsbGF0aW9uIGhhcyBiZWVuIHJlcXVlc3RlZC5cbiAgICovXG4gIHRocm93SWZSZXF1ZXN0ZWQoKSB7XG4gICAgaWYgKHRoaXMucmVhc29uKSB7XG4gICAgICB0aHJvdyB0aGlzLnJlYXNvbjtcbiAgICB9XG4gIH1cblxuICAvKipcbiAgICogU3Vic2NyaWJlIHRvIHRoZSBjYW5jZWwgc2lnbmFsXG4gICAqL1xuXG4gIHN1YnNjcmliZShsaXN0ZW5lcikge1xuICAgIGlmICh0aGlzLnJlYXNvbikge1xuICAgICAgbGlzdGVuZXIodGhpcy5yZWFzb24pO1xuICAgICAgcmV0dXJuO1xuICAgIH1cblxuICAgIGlmICh0aGlzLl9saXN0ZW5lcnMpIHtcbiAgICAgIHRoaXMuX2xpc3RlbmVycy5wdXNoKGxpc3RlbmVyKTtcbiAgICB9IGVsc2Uge1xuICAgICAgdGhpcy5fbGlzdGVuZXJzID0gW2xpc3RlbmVyXTtcbiAgICB9XG4gIH1cblxuICAvKipcbiAgICogVW5zdWJzY3JpYmUgZnJvbSB0aGUgY2FuY2VsIHNpZ25hbFxuICAgKi9cblxuICB1bnN1YnNjcmliZShsaXN0ZW5lcikge1xuICAgIGlmICghdGhpcy5fbGlzdGVuZXJzKSB7XG4gICAgICByZXR1cm47XG4gICAgfVxuICAgIGNvbnN0IGluZGV4ID0gdGhpcy5fbGlzdGVuZXJzLmluZGV4T2YobGlzdGVuZXIpO1xuICAgIGlmIChpbmRleCAhPT0gLTEpIHtcbiAgICAgIHRoaXMuX2xpc3RlbmVycy5zcGxpY2UoaW5kZXgsIDEpO1xuICAgIH1cbiAgfVxuXG4gIC8qKlxuICAgKiBSZXR1cm5zIGFuIG9iamVjdCB0aGF0IGNvbnRhaW5zIGEgbmV3IGBDYW5jZWxUb2tlbmAgYW5kIGEgZnVuY3Rpb24gdGhhdCwgd2hlbiBjYWxsZWQsXG4gICAqIGNhbmNlbHMgdGhlIGBDYW5jZWxUb2tlbmAuXG4gICAqL1xuICBzdGF0aWMgc291cmNlKCkge1xuICAgIGxldCBjYW5jZWw7XG4gICAgY29uc3QgdG9rZW4gPSBuZXcgQ2FuY2VsVG9rZW4oZnVuY3Rpb24gZXhlY3V0b3IoYykge1xuICAgICAgY2FuY2VsID0gYztcbiAgICB9KTtcbiAgICByZXR1cm4ge1xuICAgICAgdG9rZW4sXG4gICAgICBjYW5jZWxcbiAgICB9O1xuICB9XG59XG5cbmNvbnN0IENhbmNlbFRva2VuJDEgPSBDYW5jZWxUb2tlbjtcblxuLyoqXG4gKiBTeW50YWN0aWMgc3VnYXIgZm9yIGludm9raW5nIGEgZnVuY3Rpb24gYW5kIGV4cGFuZGluZyBhbiBhcnJheSBmb3IgYXJndW1lbnRzLlxuICpcbiAqIENvbW1vbiB1c2UgY2FzZSB3b3VsZCBiZSB0byB1c2UgYEZ1bmN0aW9uLnByb3RvdHlwZS5hcHBseWAuXG4gKlxuICogIGBgYGpzXG4gKiAgZnVuY3Rpb24gZih4LCB5LCB6KSB7fVxuICogIHZhciBhcmdzID0gWzEsIDIsIDNdO1xuICogIGYuYXBwbHkobnVsbCwgYXJncyk7XG4gKiAgYGBgXG4gKlxuICogV2l0aCBgc3ByZWFkYCB0aGlzIGV4YW1wbGUgY2FuIGJlIHJlLXdyaXR0ZW4uXG4gKlxuICogIGBgYGpzXG4gKiAgc3ByZWFkKGZ1bmN0aW9uKHgsIHksIHopIHt9KShbMSwgMiwgM10pO1xuICogIGBgYFxuICpcbiAqIEBwYXJhbSB7RnVuY3Rpb259IGNhbGxiYWNrXG4gKlxuICogQHJldHVybnMge0Z1bmN0aW9ufVxuICovXG5mdW5jdGlvbiBzcHJlYWQoY2FsbGJhY2spIHtcbiAgcmV0dXJuIGZ1bmN0aW9uIHdyYXAoYXJyKSB7XG4gICAgcmV0dXJuIGNhbGxiYWNrLmFwcGx5KG51bGwsIGFycik7XG4gIH07XG59XG5cbi8qKlxuICogRGV0ZXJtaW5lcyB3aGV0aGVyIHRoZSBwYXlsb2FkIGlzIGFuIGVycm9yIHRocm93biBieSBBeGlvc1xuICpcbiAqIEBwYXJhbSB7Kn0gcGF5bG9hZCBUaGUgdmFsdWUgdG8gdGVzdFxuICpcbiAqIEByZXR1cm5zIHtib29sZWFufSBUcnVlIGlmIHRoZSBwYXlsb2FkIGlzIGFuIGVycm9yIHRocm93biBieSBBeGlvcywgb3RoZXJ3aXNlIGZhbHNlXG4gKi9cbmZ1bmN0aW9uIGlzQXhpb3NFcnJvcihwYXlsb2FkKSB7XG4gIHJldHVybiB1dGlscy5pc09iamVjdChwYXlsb2FkKSAmJiAocGF5bG9hZC5pc0F4aW9zRXJyb3IgPT09IHRydWUpO1xufVxuXG5jb25zdCBIdHRwU3RhdHVzQ29kZSA9IHtcbiAgQ29udGludWU6IDEwMCxcbiAgU3dpdGNoaW5nUHJvdG9jb2xzOiAxMDEsXG4gIFByb2Nlc3Npbmc6IDEwMixcbiAgRWFybHlIaW50czogMTAzLFxuICBPazogMjAwLFxuICBDcmVhdGVkOiAyMDEsXG4gIEFjY2VwdGVkOiAyMDIsXG4gIE5vbkF1dGhvcml0YXRpdmVJbmZvcm1hdGlvbjogMjAzLFxuICBOb0NvbnRlbnQ6IDIwNCxcbiAgUmVzZXRDb250ZW50OiAyMDUsXG4gIFBhcnRpYWxDb250ZW50OiAyMDYsXG4gIE11bHRpU3RhdHVzOiAyMDcsXG4gIEFscmVhZHlSZXBvcnRlZDogMjA4LFxuICBJbVVzZWQ6IDIyNixcbiAgTXVsdGlwbGVDaG9pY2VzOiAzMDAsXG4gIE1vdmVkUGVybWFuZW50bHk6IDMwMSxcbiAgRm91bmQ6IDMwMixcbiAgU2VlT3RoZXI6IDMwMyxcbiAgTm90TW9kaWZpZWQ6IDMwNCxcbiAgVXNlUHJveHk6IDMwNSxcbiAgVW51c2VkOiAzMDYsXG4gIFRlbXBvcmFyeVJlZGlyZWN0OiAzMDcsXG4gIFBlcm1hbmVudFJlZGlyZWN0OiAzMDgsXG4gIEJhZFJlcXVlc3Q6IDQwMCxcbiAgVW5hdXRob3JpemVkOiA0MDEsXG4gIFBheW1lbnRSZXF1aXJlZDogNDAyLFxuICBGb3JiaWRkZW46IDQwMyxcbiAgTm90Rm91bmQ6IDQwNCxcbiAgTWV0aG9kTm90QWxsb3dlZDogNDA1LFxuICBOb3RBY2NlcHRhYmxlOiA0MDYsXG4gIFByb3h5QXV0aGVudGljYXRpb25SZXF1aXJlZDogNDA3LFxuICBSZXF1ZXN0VGltZW91dDogNDA4LFxuICBDb25mbGljdDogNDA5LFxuICBHb25lOiA0MTAsXG4gIExlbmd0aFJlcXVpcmVkOiA0MTEsXG4gIFByZWNvbmRpdGlvbkZhaWxlZDogNDEyLFxuICBQYXlsb2FkVG9vTGFyZ2U6IDQxMyxcbiAgVXJpVG9vTG9uZzogNDE0LFxuICBVbnN1cHBvcnRlZE1lZGlhVHlwZTogNDE1LFxuICBSYW5nZU5vdFNhdGlzZmlhYmxlOiA0MTYsXG4gIEV4cGVjdGF0aW9uRmFpbGVkOiA0MTcsXG4gIEltQVRlYXBvdDogNDE4LFxuICBNaXNkaXJlY3RlZFJlcXVlc3Q6IDQyMSxcbiAgVW5wcm9jZXNzYWJsZUVudGl0eTogNDIyLFxuICBMb2NrZWQ6IDQyMyxcbiAgRmFpbGVkRGVwZW5kZW5jeTogNDI0LFxuICBUb29FYXJseTogNDI1LFxuICBVcGdyYWRlUmVxdWlyZWQ6IDQyNixcbiAgUHJlY29uZGl0aW9uUmVxdWlyZWQ6IDQyOCxcbiAgVG9vTWFueVJlcXVlc3RzOiA0MjksXG4gIFJlcXVlc3RIZWFkZXJGaWVsZHNUb29MYXJnZTogNDMxLFxuICBVbmF2YWlsYWJsZUZvckxlZ2FsUmVhc29uczogNDUxLFxuICBJbnRlcm5hbFNlcnZlckVycm9yOiA1MDAsXG4gIE5vdEltcGxlbWVudGVkOiA1MDEsXG4gIEJhZEdhdGV3YXk6IDUwMixcbiAgU2VydmljZVVuYXZhaWxhYmxlOiA1MDMsXG4gIEdhdGV3YXlUaW1lb3V0OiA1MDQsXG4gIEh0dHBWZXJzaW9uTm90U3VwcG9ydGVkOiA1MDUsXG4gIFZhcmlhbnRBbHNvTmVnb3RpYXRlczogNTA2LFxuICBJbnN1ZmZpY2llbnRTdG9yYWdlOiA1MDcsXG4gIExvb3BEZXRlY3RlZDogNTA4LFxuICBOb3RFeHRlbmRlZDogNTEwLFxuICBOZXR3b3JrQXV0aGVudGljYXRpb25SZXF1aXJlZDogNTExLFxufTtcblxuT2JqZWN0LmVudHJpZXMoSHR0cFN0YXR1c0NvZGUpLmZvckVhY2goKFtrZXksIHZhbHVlXSkgPT4ge1xuICBIdHRwU3RhdHVzQ29kZVt2YWx1ZV0gPSBrZXk7XG59KTtcblxuY29uc3QgSHR0cFN0YXR1c0NvZGUkMSA9IEh0dHBTdGF0dXNDb2RlO1xuXG4vKipcbiAqIENyZWF0ZSBhbiBpbnN0YW5jZSBvZiBBeGlvc1xuICpcbiAqIEBwYXJhbSB7T2JqZWN0fSBkZWZhdWx0Q29uZmlnIFRoZSBkZWZhdWx0IGNvbmZpZyBmb3IgdGhlIGluc3RhbmNlXG4gKlxuICogQHJldHVybnMge0F4aW9zfSBBIG5ldyBpbnN0YW5jZSBvZiBBeGlvc1xuICovXG5mdW5jdGlvbiBjcmVhdGVJbnN0YW5jZShkZWZhdWx0Q29uZmlnKSB7XG4gIGNvbnN0IGNvbnRleHQgPSBuZXcgQXhpb3MkMShkZWZhdWx0Q29uZmlnKTtcbiAgY29uc3QgaW5zdGFuY2UgPSBiaW5kKEF4aW9zJDEucHJvdG90eXBlLnJlcXVlc3QsIGNvbnRleHQpO1xuXG4gIC8vIENvcHkgYXhpb3MucHJvdG90eXBlIHRvIGluc3RhbmNlXG4gIHV0aWxzLmV4dGVuZChpbnN0YW5jZSwgQXhpb3MkMS5wcm90b3R5cGUsIGNvbnRleHQsIHthbGxPd25LZXlzOiB0cnVlfSk7XG5cbiAgLy8gQ29weSBjb250ZXh0IHRvIGluc3RhbmNlXG4gIHV0aWxzLmV4dGVuZChpbnN0YW5jZSwgY29udGV4dCwgbnVsbCwge2FsbE93bktleXM6IHRydWV9KTtcblxuICAvLyBGYWN0b3J5IGZvciBjcmVhdGluZyBuZXcgaW5zdGFuY2VzXG4gIGluc3RhbmNlLmNyZWF0ZSA9IGZ1bmN0aW9uIGNyZWF0ZShpbnN0YW5jZUNvbmZpZykge1xuICAgIHJldHVybiBjcmVhdGVJbnN0YW5jZShtZXJnZUNvbmZpZyhkZWZhdWx0Q29uZmlnLCBpbnN0YW5jZUNvbmZpZykpO1xuICB9O1xuXG4gIHJldHVybiBpbnN0YW5jZTtcbn1cblxuLy8gQ3JlYXRlIHRoZSBkZWZhdWx0IGluc3RhbmNlIHRvIGJlIGV4cG9ydGVkXG5jb25zdCBheGlvcyA9IGNyZWF0ZUluc3RhbmNlKGRlZmF1bHRzJDEpO1xuXG4vLyBFeHBvc2UgQXhpb3MgY2xhc3MgdG8gYWxsb3cgY2xhc3MgaW5oZXJpdGFuY2VcbmF4aW9zLkF4aW9zID0gQXhpb3MkMTtcblxuLy8gRXhwb3NlIENhbmNlbCAmIENhbmNlbFRva2VuXG5heGlvcy5DYW5jZWxlZEVycm9yID0gQ2FuY2VsZWRFcnJvcjtcbmF4aW9zLkNhbmNlbFRva2VuID0gQ2FuY2VsVG9rZW4kMTtcbmF4aW9zLmlzQ2FuY2VsID0gaXNDYW5jZWw7XG5heGlvcy5WRVJTSU9OID0gVkVSU0lPTjtcbmF4aW9zLnRvRm9ybURhdGEgPSB0b0Zvcm1EYXRhO1xuXG4vLyBFeHBvc2UgQXhpb3NFcnJvciBjbGFzc1xuYXhpb3MuQXhpb3NFcnJvciA9IEF4aW9zRXJyb3I7XG5cbi8vIGFsaWFzIGZvciBDYW5jZWxlZEVycm9yIGZvciBiYWNrd2FyZCBjb21wYXRpYmlsaXR5XG5heGlvcy5DYW5jZWwgPSBheGlvcy5DYW5jZWxlZEVycm9yO1xuXG4vLyBFeHBvc2UgYWxsL3NwcmVhZFxuYXhpb3MuYWxsID0gZnVuY3Rpb24gYWxsKHByb21pc2VzKSB7XG4gIHJldHVybiBQcm9taXNlLmFsbChwcm9taXNlcyk7XG59O1xuXG5heGlvcy5zcHJlYWQgPSBzcHJlYWQ7XG5cbi8vIEV4cG9zZSBpc0F4aW9zRXJyb3JcbmF4aW9zLmlzQXhpb3NFcnJvciA9IGlzQXhpb3NFcnJvcjtcblxuLy8gRXhwb3NlIG1lcmdlQ29uZmlnXG5heGlvcy5tZXJnZUNvbmZpZyA9IG1lcmdlQ29uZmlnO1xuXG5heGlvcy5BeGlvc0hlYWRlcnMgPSBBeGlvc0hlYWRlcnMkMTtcblxuYXhpb3MuZm9ybVRvSlNPTiA9IHRoaW5nID0+IGZvcm1EYXRhVG9KU09OKHV0aWxzLmlzSFRNTEZvcm0odGhpbmcpID8gbmV3IEZvcm1EYXRhKHRoaW5nKSA6IHRoaW5nKTtcblxuYXhpb3MuZ2V0QWRhcHRlciA9IGFkYXB0ZXJzLmdldEFkYXB0ZXI7XG5cbmF4aW9zLkh0dHBTdGF0dXNDb2RlID0gSHR0cFN0YXR1c0NvZGUkMTtcblxuYXhpb3MuZGVmYXVsdCA9IGF4aW9zO1xuXG5tb2R1bGUuZXhwb3J0cyA9IGF4aW9zO1xuLy8jIHNvdXJjZU1hcHBpbmdVUkw9YXhpb3MuY2pzLm1hcFxuIiwiLy8gVGhlIG1vZHVsZSBjYWNoZVxudmFyIF9fd2VicGFja19tb2R1bGVfY2FjaGVfXyA9IHt9O1xuXG4vLyBUaGUgcmVxdWlyZSBmdW5jdGlvblxuZnVuY3Rpb24gX193ZWJwYWNrX3JlcXVpcmVfXyhtb2R1bGVJZCkge1xuXHQvLyBDaGVjayBpZiBtb2R1bGUgaXMgaW4gY2FjaGVcblx0dmFyIGNhY2hlZE1vZHVsZSA9IF9fd2VicGFja19tb2R1bGVfY2FjaGVfX1ttb2R1bGVJZF07XG5cdGlmIChjYWNoZWRNb2R1bGUgIT09IHVuZGVmaW5lZCkge1xuXHRcdHJldHVybiBjYWNoZWRNb2R1bGUuZXhwb3J0cztcblx0fVxuXHQvLyBDcmVhdGUgYSBuZXcgbW9kdWxlIChhbmQgcHV0IGl0IGludG8gdGhlIGNhY2hlKVxuXHR2YXIgbW9kdWxlID0gX193ZWJwYWNrX21vZHVsZV9jYWNoZV9fW21vZHVsZUlkXSA9IHtcblx0XHRpZDogbW9kdWxlSWQsXG5cdFx0bG9hZGVkOiBmYWxzZSxcblx0XHRleHBvcnRzOiB7fVxuXHR9O1xuXG5cdC8vIEV4ZWN1dGUgdGhlIG1vZHVsZSBmdW5jdGlvblxuXHRfX3dlYnBhY2tfbW9kdWxlc19fW21vZHVsZUlkXS5jYWxsKG1vZHVsZS5leHBvcnRzLCBtb2R1bGUsIG1vZHVsZS5leHBvcnRzLCBfX3dlYnBhY2tfcmVxdWlyZV9fKTtcblxuXHQvLyBGbGFnIHRoZSBtb2R1bGUgYXMgbG9hZGVkXG5cdG1vZHVsZS5sb2FkZWQgPSB0cnVlO1xuXG5cdC8vIFJldHVybiB0aGUgZXhwb3J0cyBvZiB0aGUgbW9kdWxlXG5cdHJldHVybiBtb2R1bGUuZXhwb3J0cztcbn1cblxuIiwiX193ZWJwYWNrX3JlcXVpcmVfXy5ubWQgPSAobW9kdWxlKSA9PiB7XG5cdG1vZHVsZS5wYXRocyA9IFtdO1xuXHRpZiAoIW1vZHVsZS5jaGlsZHJlbikgbW9kdWxlLmNoaWxkcmVuID0gW107XG5cdHJldHVybiBtb2R1bGU7XG59OyIsIi8vIHN0YXJ0dXBcbi8vIExvYWQgZW50cnkgbW9kdWxlIGFuZCByZXR1cm4gZXhwb3J0c1xuLy8gVGhpcyBlbnRyeSBtb2R1bGUgaXMgcmVmZXJlbmNlZCBieSBvdGhlciBtb2R1bGVzIHNvIGl0IGNhbid0IGJlIGlubGluZWRcbnZhciBfX3dlYnBhY2tfZXhwb3J0c19fID0gX193ZWJwYWNrX3JlcXVpcmVfXyg3NTMwKTtcbiJdLCJuYW1lcyI6WyJyb290IiwiZmFjdG9yeSIsImV4cG9ydHMiLCJtb2R1bGUiLCJkZWZpbmUiLCJhbWQiLCJ0aGlzIiwicGFyYWxsZWwiLCJzZXJpYWwiLCJzZXJpYWxPcmRlcmVkIiwiY2xlYW4iLCJrZXkiLCJqb2JzIiwic3RhdGUiLCJPYmplY3QiLCJrZXlzIiwiZm9yRWFjaCIsImJpbmQiLCJkZWZlciIsImNhbGxiYWNrIiwiaXNBc3luYyIsImVyciIsInJlc3VsdCIsImZuIiwibmV4dFRpY2siLCJzZXRJbW1lZGlhdGUiLCJwcm9jZXNzIiwic2V0VGltZW91dCIsImFzeW5jIiwiYWJvcnQiLCJsaXN0IiwiaXRlcmF0b3IiLCJpbmRleCIsIml0ZW0iLCJhYm9ydGVyIiwibGVuZ3RoIiwicnVuSm9iIiwiZXJyb3IiLCJvdXRwdXQiLCJyZXN1bHRzIiwic29ydE1ldGhvZCIsImlzTmFtZWRMaXN0IiwiQXJyYXkiLCJpc0FycmF5IiwiaW5pdFN0YXRlIiwia2V5ZWRMaXN0Iiwic2l6ZSIsInNvcnQiLCJhIiwiYiIsIml0ZXJhdGUiLCJ0ZXJtaW5hdG9yIiwiYXNjZW5kaW5nIiwiaXRlcmF0b3JIYW5kbGVyIiwiZGVzY2VuZGluZyIsIkNvbWJpbmVkU3RyZWFtIiwidXRpbCIsInBhdGgiLCJodHRwIiwiaHR0cHMiLCJwYXJzZVVybCIsImZzIiwiU3RyZWFtIiwibWltZSIsImFzeW5ja2l0IiwicG9wdWxhdGUiLCJGb3JtRGF0YSIsIm9wdGlvbnMiLCJvcHRpb24iLCJfb3ZlcmhlYWRMZW5ndGgiLCJfdmFsdWVMZW5ndGgiLCJfdmFsdWVzVG9NZWFzdXJlIiwiY2FsbCIsImluaGVyaXRzIiwiTElORV9CUkVBSyIsIkRFRkFVTFRfQ09OVEVOVF9UWVBFIiwicHJvdG90eXBlIiwiYXBwZW5kIiwiZmllbGQiLCJ2YWx1ZSIsImZpbGVuYW1lIiwiX2Vycm9yIiwiRXJyb3IiLCJoZWFkZXIiLCJfbXVsdGlQYXJ0SGVhZGVyIiwiZm9vdGVyIiwiX211bHRpUGFydEZvb3RlciIsIl90cmFja0xlbmd0aCIsInZhbHVlTGVuZ3RoIiwia25vd25MZW5ndGgiLCJCdWZmZXIiLCJpc0J1ZmZlciIsImJ5dGVMZW5ndGgiLCJyZWFkYWJsZSIsImhhc093blByb3BlcnR5IiwicHVzaCIsIl9sZW5ndGhSZXRyaWV2ZXIiLCJ1bmRlZmluZWQiLCJlbmQiLCJJbmZpbml0eSIsInN0YXJ0Iiwic3RhdCIsImZpbGVTaXplIiwiaGVhZGVycyIsIm9uIiwicmVzcG9uc2UiLCJwYXVzZSIsInJlc3VtZSIsImNvbnRlbnREaXNwb3NpdGlvbiIsIl9nZXRDb250ZW50RGlzcG9zaXRpb24iLCJjb250ZW50VHlwZSIsIl9nZXRDb250ZW50VHlwZSIsImNvbnRlbnRzIiwiY29uY2F0IiwicHJvcCIsImpvaW4iLCJnZXRCb3VuZGFyeSIsImZpbGVwYXRoIiwibm9ybWFsaXplIiwicmVwbGFjZSIsIm5hbWUiLCJiYXNlbmFtZSIsImNsaWVudCIsIl9odHRwTWVzc2FnZSIsImxvb2t1cCIsIm5leHQiLCJfc3RyZWFtcyIsIl9sYXN0Qm91bmRhcnkiLCJnZXRIZWFkZXJzIiwidXNlckhlYWRlcnMiLCJmb3JtSGVhZGVycyIsInRvTG93ZXJDYXNlIiwic2V0Qm91bmRhcnkiLCJib3VuZGFyeSIsIl9ib3VuZGFyeSIsIl9nZW5lcmF0ZUJvdW5kYXJ5IiwiZ2V0QnVmZmVyIiwiZGF0YUJ1ZmZlciIsImFsbG9jIiwiaSIsImxlbiIsImZyb20iLCJzdWJzdHJpbmciLCJNYXRoIiwiZmxvb3IiLCJyYW5kb20iLCJ0b1N0cmluZyIsImdldExlbmd0aFN5bmMiLCJoYXNLbm93bkxlbmd0aCIsImdldExlbmd0aCIsImNiIiwidmFsdWVzIiwic3VibWl0IiwicGFyYW1zIiwicmVxdWVzdCIsImRlZmF1bHRzIiwibWV0aG9kIiwicG9ydCIsInBhdGhuYW1lIiwiaG9zdCIsImhvc3RuYW1lIiwicHJvdG9jb2wiLCJzZXRIZWFkZXIiLCJwaXBlIiwib25SZXNwb25zZSIsInJlc3BvbmNlIiwicmVtb3ZlTGlzdGVuZXIiLCJlbWl0IiwiZHN0Iiwic3JjIiwiRG9tYWluIiwiZGF0YSIsInJlY2VpdmluZyIsInNlbmRpbmciLCJyZXF1aXJlX3RscyIsInNraXBfdmVyaWZpY2F0aW9uIiwid2lsZGNhcmQiLCJzcGFtX2FjdGlvbiIsImNyZWF0ZWRfYXQiLCJzbXRwX3Bhc3N3b3JkIiwic210cF9sb2dpbiIsInR5cGUiLCJyZWNlaXZpbmdfZG5zX3JlY29yZHMiLCJzZW5kaW5nX2Ruc19yZWNvcmRzIiwiZHluYW1pY1Byb3BlcnRpZXMiLCJyZWR1Y2UiLCJhY2MiLCJwcm9wZXJ0eU5hbWUiLCJhc3NpZ24iLCJ1cmxfam9pbl8xIiwiX19pbXBvcnREZWZhdWx0IiwicmVxdWlyZSIsIkVycm9yXzEiLCJkb21haW5fMSIsIkRvbWFpbnNDbGllbnQiLCJkb21haW5DcmVkZW50aWFsc0NsaWVudCIsImRvbWFpblRlbXBsYXRlc0NsaWVudCIsImRvbWFpblRhZ3NDbGllbnQiLCJkb21haW5DcmVkZW50aWFscyIsImRvbWFpblRlbXBsYXRlcyIsImRvbWFpblRhZ3MiLCJfaGFuZGxlQm9vbFZhbHVlcyIsInByb3BzRm9yUmVwbGFjZW1lbnQiLCJyZXBsYWNlZFByb3BzIiwiX19hc3NpZ24iLCJfcGFyc2VNZXNzYWdlIiwiYm9keSIsInBhcnNlRG9tYWluTGlzdCIsIml0ZW1zIiwibWFwIiwiZGVmYXVsdCIsIl9wYXJzZURvbWFpbiIsImRvbWFpbiIsIl9wYXJzZVRyYWNraW5nU2V0dGluZ3MiLCJ0cmFja2luZyIsIl9wYXJzZVRyYWNraW5nVXBkYXRlIiwicXVlcnkiLCJfdGhpcyIsImdldCIsInRoZW4iLCJyZXMiLCJjcmVhdGUiLCJwb3N0T2JqIiwicG9zdFdpdGhGRCIsInVwZGF0ZSIsInB1dERhdGEiLCJwdXRXaXRoRkQiLCJ2ZXJpZnkiLCJwdXQiLCJkZXN0cm95IiwiZGVsZXRlIiwiZ2V0Q29ubmVjdGlvbiIsImNvbm5lY3Rpb24iLCJ1cGRhdGVDb25uZWN0aW9uIiwiZ2V0VHJhY2tpbmciLCJ1cGRhdGVUcmFja2luZyIsImFjdGl2ZSIsInN0YXR1cyIsInN0YXR1c1RleHQiLCJtZXNzYWdlIiwiZ2V0SXBzIiwiX2EiLCJhc3NpZ25JcCIsImlwIiwiZGVsZXRlSXAiLCJsaW5rSXBQb29sIiwicG9vbElkIiwicG9vbF9pZCIsInVubGlua0lwUG9sbCIsInJlcGxhY2VtZW50Iiwic2VhcmNoUGFyYW1zIiwidXBkYXRlREtJTUF1dGhvcml0eSIsInNlbGYiLCJ1cGRhdGVES0lNU2VsZWN0b3IiLCJka2ltU2VsZWN0b3IiLCJ1cGRhdGVXZWJQcmVmaXgiLCJ3ZWJQcmVmaXgiLCJEb21haW5DcmVkZW50aWFsc0NsaWVudCIsImJhc2VSb3V0ZSIsIl9wYXJzZURvbWFpbkNyZWRlbnRpYWxzTGlzdCIsInRvdGFsQ291bnQiLCJ0b3RhbF9jb3VudCIsIl9wYXJzZU1lc3NhZ2VSZXNwb25zZSIsIl9wYXJzZURlbGV0ZWRSZXNwb25zZSIsInNwZWMiLCJjcmVkZW50aWFsc0xvZ2luIiwiTmF2aWdhdGlvblRocnVQYWdlc18xIiwiRG9tYWluVGFnIiwidGFnSW5mbyIsInRhZyIsImRlc2NyaXB0aW9uIiwiRGF0ZSIsIkRvbWFpblRhZ1N0YXRpc3RpYyIsInRhZ1N0YXRpc3RpY0luZm8iLCJyZXNvbHV0aW9uIiwic3RhdHMiLCJ0aW1lIiwiRG9tYWluVGFnc0NsaWVudCIsIl9zdXBlciIsIl9fZXh0ZW5kcyIsInBhcnNlTGlzdCIsInBhZ2VzIiwicGFyc2VQYWdlTGlua3MiLCJfcGFyc2VUYWdTdGF0aXN0aWMiLCJyZXF1ZXN0TGlzdFdpdGhQYWdlcyIsInN0YXRpc3RpYyIsImNvdW50cmllcyIsInByb3ZpZGVycyIsImRldmljZXMiLCJEb21haW5UZW1wbGF0ZUl0ZW0iLCJkb21haW5UZW1wbGF0ZUZyb21BUEkiLCJjcmVhdGVkQXQiLCJjcmVhdGVkQnkiLCJpZCIsInZlcnNpb24iLCJ2ZXJzaW9ucyIsIkRvbWFpblRlbXBsYXRlc0NsaWVudCIsInBhcnNlQ3JlYXRpb25SZXNwb25zZSIsInRlbXBsYXRlIiwicGFyc2VDcmVhdGlvblZlcnNpb25SZXNwb25zZSIsInBhcnNlTXV0YXRpb25SZXNwb25zZSIsInRlbXBsYXRlTmFtZSIsInBhcnNlTm90aWZpY2F0aW9uUmVzcG9uc2UiLCJwYXJzZU11dGF0ZVRlbXBsYXRlVmVyc2lvblJlc3BvbnNlIiwidGVtcGxhdGVWZXJzaW9uIiwiZCIsInBhcnNlTGlzdFRlbXBsYXRlVmVyc2lvbnMiLCJkZXN0cm95QWxsIiwiY3JlYXRlVmVyc2lvbiIsImdldFZlcnNpb24iLCJ1cGRhdGVWZXJzaW9uIiwiZGVzdHJveVZlcnNpb24iLCJsaXN0VmVyc2lvbnMiLCJFdmVudENsaWVudCIsIklwUG9vbHNDbGllbnQiLCJwYXJzZUlwUG9vbHNSZXNwb25zZSIsInNlbnQiLCJwYXRjaFdpdGhGRCIsIklwc0NsaWVudCIsInBhcnNlSXBzUmVzcG9uc2UiLCJSZXF1ZXN0XzEiLCJkb21haW5zQ2xpZW50XzEiLCJFdmVudHNfMSIsIlN0YXRzQ2xpZW50XzEiLCJTdXBwcmVzc2lvbnNDbGllbnRfMSIsIldlYmhvb2tzXzEiLCJNZXNzYWdlc18xIiwiUm91dGVzXzEiLCJ2YWxpZGF0ZV8xIiwiSVBzXzEiLCJJUFBvb2xzXzEiLCJtYWlsaW5nTGlzdHNfMSIsIm1haWxMaXN0TWVtYmVyc18xIiwiZG9tYWluc0NyZWRlbnRpYWxzXzEiLCJtdWx0aXBsZVZhbGlkYXRpb25fMSIsImRvbWFpbnNUZW1wbGF0ZXNfMSIsImRvbWFpbnNUYWdzXzEiLCJTdWJhY2NvdW50c18xIiwiTWFpbGd1bkNsaWVudCIsImZvcm1EYXRhIiwiY29uZmlnIiwidXJsIiwidXNlcm5hbWUiLCJtYWlsTGlzdHNNZW1iZXJzIiwibXVsdGlwbGVWYWxpZGF0aW9uQ2xpZW50IiwiZG9tYWlucyIsIndlYmhvb2tzIiwiZXZlbnRzIiwic3VwcHJlc3Npb25zIiwibWVzc2FnZXMiLCJyb3V0ZXMiLCJpcHMiLCJpcF9wb29scyIsImxpc3RzIiwidmFsaWRhdGUiLCJzdWJhY2NvdW50cyIsInNldFN1YmFjY291bnQiLCJzdWJhY2NvdW50SWQiLCJzZXRTdWJhY2NvdW50SGVhZGVyIiwicmVzZXRTdWJhY2NvdW50IiwicmVzZXRTdWJhY2NvdW50SGVhZGVyIiwiTWFpbExpc3RzTWVtYmVycyIsImNoZWNrQW5kVXBkYXRlRGF0YSIsIm5ld0RhdGEiLCJ2YXJzIiwiSlNPTiIsInN0cmluZ2lmeSIsInN1YnNjcmliZWQiLCJsaXN0TWVtYmVycyIsIm1haWxMaXN0QWRkcmVzcyIsImdldE1lbWJlciIsIm1haWxMaXN0TWVtYmVyQWRkcmVzcyIsIm1lbWJlciIsImNyZWF0ZU1lbWJlciIsInJlcURhdGEiLCJjcmVhdGVNZW1iZXJzIiwibWVtYmVycyIsInVwc2VydCIsInVwZGF0ZU1lbWJlciIsImRlc3Ryb3lNZW1iZXIiLCJNYWlsaW5nTGlzdHNDbGllbnQiLCJwYXJzZVZhbGlkYXRpb25SZXN1bHQiLCJ2YWxpZGF0aW9uUmVzdWx0IiwicG9zdCIsImNhbmNlbFZhbGlkYXRpb24iLCJNZXNzYWdlc0NsaWVudCIsInByZXBhcmVCb29sZWFuVmFsdWVzIiwieWVzTm9Qcm9wZXJ0aWVzIiwiU2V0IiwiaGFzIiwiX3BhcnNlUmVzcG9uc2UiLCJtb2RpZmllZERhdGEiLCJSb3V0ZXNDbGllbnQiLCJyb3V0ZSIsIlN0YXRzQ29udGFpbmVyXzEiLCJTdGF0c0NsaWVudCIsImxvZ2dlciIsImNvbnNvbGUiLCJjb252ZXJ0RGF0ZVRvVVRDIiwiaW5wdXREYXRlIiwid2FybiIsInRvVVRDU3RyaW5nIiwicHJlcGFyZVNlYXJjaFBhcmFtcyIsImVudHJpZXMiLCJhcnJheVdpdGhQYWlycyIsImN1cnJlbnRQYWlyIiwicmVwZWF0ZWRQcm9wZXJ0eSIsIl9fc3ByZWFkQXJyYXkiLCJwYXJzZVN0YXRzIiwiZ2V0RG9tYWluIiwiZ2V0QWNjb3VudCIsIlN0YXRzQ29udGFpbmVyIiwiU3ViYWNjb3VudHNDbGllbnQiLCJlbmFibGUiLCJkaXNhYmxlIiwiU1VCQUNDT1VOVF9IRUFERVIiLCJFbnVtc18xIiwiQm91bmNlIiwiU3VwcHJlc3Npb25Nb2RlbHMiLCJCT1VOQ0VTIiwiYWRkcmVzcyIsImNvZGUiLCJDb21wbGFpbnQiLCJDT01QTEFJTlRTIiwiU3VwcHJlc3Npb24iLCJCb3VuY2VfMSIsIkNvbXBsYWludF8xIiwiVW5zdWJzY3JpYmVfMSIsIldoaXRlTGlzdF8xIiwiY3JlYXRlT3B0aW9ucyIsIlN1cHByZXNzaW9uQ2xpZW50IiwibW9kZWxzIiwiYm91bmNlcyIsImNvbXBsYWludHMiLCJ1bnN1YnNjcmliZXMiLCJ3aGl0ZWxpc3RzIiwiTW9kZWwiLCJfcGFyc2VJdGVtIiwiY3JlYXRlV2hpdGVMaXN0IiwiaXNEYXRhQXJyYXkiLCJwcmVwYXJlUmVzcG9uc2UiLCJjcmVhdGVVbnN1YnNjcmliZSIsInNvbWUiLCJ1bnN1YnNjcmliZSIsInRhZ3MiLCJnZXRNb2RlbCIsIm1vZGVsIiwiZW5jb2RlVVJJQ29tcG9uZW50IiwicG9zdERhdGEiLCJVbnN1YnNjcmliZSIsIlVOU1VCU0NSSUJFUyIsIldoaXRlTGlzdCIsIldISVRFTElTVFMiLCJyZWFzb24iLCJNdWx0aXBsZVZhbGlkYXRpb25Kb2IiLCJyZXNwb25zZVN0YXR1c0NvZGUiLCJxdWFudGl0eSIsInJlY29yZHNQcm9jZXNzZWQiLCJyZWNvcmRzX3Byb2Nlc3NlZCIsImRvd25sb2FkX3VybCIsImRvd25sb2FkVXJsIiwiY3N2IiwianNvbiIsIl9iIiwic3VtbWFyeSIsImNhdGNoQWxsIiwiY2F0Y2hfYWxsIiwiZGVsaXZlcmFibGUiLCJkb05vdFNlbmQiLCJkb19ub3Rfc2VuZCIsInVuZGVsaXZlcmFibGUiLCJ1bmtub3duIiwicmlzayIsImhpZ2giLCJsb3ciLCJtZWRpdW0iLCJNdWx0aXBsZVZhbGlkYXRpb25DbGllbnQiLCJoYW5kbGVSZXNwb25zZSIsImpvYiIsInRvdGFsIiwibGlzdElkIiwibXVsdGlwbGVWYWxpZGF0aW9uRGF0YSIsIm11bHRpcGxlVmFsaWRhdGlvbkZpbGUiLCJmaWxlIiwiVmFsaWRhdGVDbGllbnQiLCJtdWx0aXBsZVZhbGlkYXRpb24iLCJXZWJob29rIiwidXJscyIsIldlYmhvb2tzQ2xpZW50IiwiX3BhcnNlV2ViaG9va0xpc3QiLCJfcGFyc2VXZWJob29rV2l0aElEIiwid2ViaG9va1Jlc3BvbnNlIiwid2ViaG9vayIsIl9wYXJzZVdlYmhvb2tUZXN0IiwidGVzdCIsInVybFZhbHVlcyIsIkFQSUVycm9yIiwiYm9keU1lc3NhZ2UiLCJzdGFjayIsImRldGFpbHMiLCJGb3JtRGF0YUJ1aWxkZXIiLCJGb3JtRGF0YUNvbnN0cnVjdG9yIiwiY3JlYXRlRm9ybURhdGEiLCJmaWx0ZXIiLCJmb3JtRGF0YUFjYyIsImluY2x1ZGVzIiwiYWRkRmlsZXNUb0ZEIiwiYWRkTWltZURhdGFUb0ZEIiwiYWRkQ29tbW9uUHJvcGVydHlUb0ZEIiwiaXNGb3JtRGF0YVBhY2thZ2UiLCJmb3JtRGF0YUluc3RhbmNlIiwiZ2V0QXR0YWNobWVudE9wdGlvbnMiLCJpc1N0cmVhbSIsIkJsb2IiLCJicm93c2VyRm9ybURhdGEiLCJibG9iSW5zdGFuY2UiLCJhcHBlbmRGaWxlVG9GRCIsIm9yaWdpbmFsS2V5Iiwib2JqIiwib2JqRGF0YSIsImZkIiwiTmF2aWdhdGlvblRocnVQYWdlcyIsInBhcnNlUGFnZSIsInBhZ2VVcmwiLCJ1cmxTZXBhcmF0b3IiLCJpdGVyYXRvck5hbWUiLCJVUkwiLCJwYWdlVmFsdWUiLCJzcGxpdCIsInBvcCIsIml0ZXJhdG9yUG9zaXRpb24iLCJwYWdlIiwicGFnaW5nIiwidXBkYXRlVXJsQW5kUXVlcnkiLCJjbGllbnRVcmwiLCJxdWVyeUNvcHkiLCJ1cGRhdGVkUXVlcnkiLCJiYXNlNjQiLCJfX2ltcG9ydFN0YXIiLCJheGlvc18xIiwiRm9ybURhdGFCdWlsZGVyXzEiLCJSZXF1ZXN0IiwidGltZW91dCIsIm1ha2VIZWFkZXJzRnJvbU9iamVjdCIsImZvcm1EYXRhQnVpbGRlciIsIm1heEJvZHlMZW5ndGgiLCJwcm94eSIsIm9uQ2FsbE9wdGlvbnMiLCJyZXF1ZXN0SGVhZGVycyIsImpvaW5BbmRUcmFuc2Zvcm1IZWFkZXJzIiwiZ2V0T3duUHJvcGVydHlOYW1lcyIsIlVSTFNlYXJjaFBhcmFtcyIsInVybFZhbHVlIiwidG9Mb2NhbGVVcHBlckNhc2UiLCJfZCIsImVycm9yUmVzcG9uc2UiLCJlcnJfMSIsIl9jIiwiZ2V0UmVzcG9uc2VCb2R5IiwiQXhpb3NIZWFkZXJzIiwiYmFzaWMiLCJlbmNvZGUiLCJzZXRBdXRob3JpemF0aW9uIiwic2V0IiwicmVjZWl2ZWRPbkNhbGxIZWFkZXJzIiwib25DYWxsSGVhZGVycyIsImhlYWRlcnNPYmplY3QiLCJoZWFkZXJzQWNjdW11bGF0b3IiLCJjb21tYW5kIiwiYWRkRGVmYXVsdEhlYWRlcnMiLCJyZXF1ZXN0T3B0aW9ucyIsIlJlc29sdXRpb24iLCJXZWJob29rc0lkcyIsIlllc05vIiwiX19leHBvcnRTdGFyIiwiTWFpbGd1bkNsaWVudF8xIiwiTWFpbGd1biIsImRlZmluZVByb3BlcnR5IiwiZnJlZUV4cG9ydHMiLCJmcmVlR2xvYmFsIiwiZ2xvYmFsIiwid2luZG93IiwiSW52YWxpZENoYXJhY3RlckVycm9yIiwiVEFCTEUiLCJSRUdFWF9TUEFDRV9DSEFSQUNURVJTIiwiaW5wdXQiLCJTdHJpbmciLCJjIiwiYnVmZmVyIiwicGFkZGluZyIsInBvc2l0aW9uIiwiY2hhckNvZGVBdCIsImNoYXJBdCIsImJpdFN0b3JhZ2UiLCJiaXRDb3VudGVyIiwiaW5kZXhPZiIsImZyb21DaGFyQ29kZSIsIkRlbGF5ZWRTdHJlYW0iLCJ3cml0YWJsZSIsImRhdGFTaXplIiwibWF4RGF0YVNpemUiLCJwYXVzZVN0cmVhbXMiLCJfcmVsZWFzZWQiLCJfY3VycmVudFN0cmVhbSIsIl9pbnNpZGVMb29wIiwiX3BlbmRpbmdOZXh0IiwiY29tYmluZWRTdHJlYW0iLCJpc1N0cmVhbUxpa2UiLCJzdHJlYW0iLCJuZXdTdHJlYW0iLCJwYXVzZVN0cmVhbSIsIl9jaGVja0RhdGFTaXplIiwiX2hhbmRsZUVycm9ycyIsImRlc3QiLCJfZ2V0TmV4dCIsIl9yZWFsR2V0TmV4dCIsInNoaWZ0IiwiX3BpcGVOZXh0Iiwid3JpdGUiLCJfZW1pdEVycm9yIiwiX3Jlc2V0IiwiX3VwZGF0ZURhdGFTaXplIiwiZm9ybWF0QXJncyIsImFyZ3MiLCJ1c2VDb2xvcnMiLCJuYW1lc3BhY2UiLCJodW1hbml6ZSIsImRpZmYiLCJjb2xvciIsInNwbGljZSIsImxhc3RDIiwibWF0Y2giLCJzYXZlIiwibmFtZXNwYWNlcyIsInN0b3JhZ2UiLCJzZXRJdGVtIiwicmVtb3ZlSXRlbSIsImxvYWQiLCJyIiwiZ2V0SXRlbSIsImVudiIsIkRFQlVHIiwiX19ud2pzIiwibmF2aWdhdG9yIiwidXNlckFnZW50IiwiZG9jdW1lbnQiLCJkb2N1bWVudEVsZW1lbnQiLCJzdHlsZSIsIldlYmtpdEFwcGVhcmFuY2UiLCJmaXJlYnVnIiwiZXhjZXB0aW9uIiwidGFibGUiLCJwYXJzZUludCIsIlJlZ0V4cCIsIiQxIiwibG9jYWxTdG9yYWdlIiwibG9jYWxzdG9yYWdlIiwid2FybmVkIiwiY29sb3JzIiwibG9nIiwiZGVidWciLCJmb3JtYXR0ZXJzIiwiaiIsInYiLCJjcmVhdGVEZWJ1ZyIsInByZXZUaW1lIiwibmFtZXNwYWNlc0NhY2hlIiwiZW5hYmxlZENhY2hlIiwiZW5hYmxlT3ZlcnJpZGUiLCJlbmFibGVkIiwiY3VyciIsIk51bWJlciIsIm1zIiwicHJldiIsImNvZXJjZSIsInVuc2hpZnQiLCJmb3JtYXQiLCJmb3JtYXR0ZXIiLCJ2YWwiLCJhcHBseSIsInNlbGVjdENvbG9yIiwiZXh0ZW5kIiwiZW51bWVyYWJsZSIsImNvbmZpZ3VyYWJsZSIsImluaXQiLCJkZWxpbWl0ZXIiLCJuZXdEZWJ1ZyIsInRvTmFtZXNwYWNlIiwicmVnZXhwIiwibmFtZXMiLCJza2lwcyIsInNsaWNlIiwiaGFzaCIsImFicyIsImJyb3dzZXIiLCJ0dHkiLCJpbnNwZWN0T3B0cyIsInN0ZGVyciIsImNvbG9yQ29kZSIsInByZWZpeCIsImhpZGVEYXRlIiwidG9JU09TdHJpbmciLCJnZXREYXRlIiwiQm9vbGVhbiIsImlzYXR0eSIsImRlcHJlY2F0ZSIsInN1cHBvcnRzQ29sb3IiLCJsZXZlbCIsIl8iLCJrIiwidG9VcHBlckNhc2UiLCJvIiwiaW5zcGVjdCIsInN0ciIsInRyaW0iLCJPIiwic291cmNlIiwiX21heERhdGFTaXplRXhjZWVkZWQiLCJfYnVmZmVyZWRFdmVudHMiLCJkZWxheWVkU3RyZWFtIiwicmVhbEVtaXQiLCJfaGFuZGxlRW1pdCIsImFyZ3VtZW50cyIsInNldEVuY29kaW5nIiwicmVsZWFzZSIsIl9jaGVja0lmTWF4RGF0YVNpemVFeGNlZWRlZCIsIldyaXRhYmxlIiwiYXNzZXJ0IiwidXNlTmF0aXZlVVJMIiwicHJlc2VydmVkVXJsRmllbGRzIiwiZXZlbnRIYW5kbGVycyIsImV2ZW50IiwiYXJnMSIsImFyZzIiLCJhcmczIiwiX3JlZGlyZWN0YWJsZSIsIkludmFsaWRVcmxFcnJvciIsImNyZWF0ZUVycm9yVHlwZSIsIlR5cGVFcnJvciIsIlJlZGlyZWN0aW9uRXJyb3IiLCJUb29NYW55UmVkaXJlY3RzRXJyb3IiLCJNYXhCb2R5TGVuZ3RoRXhjZWVkZWRFcnJvciIsIldyaXRlQWZ0ZXJFbmRFcnJvciIsIm5vb3AiLCJSZWRpcmVjdGFibGVSZXF1ZXN0IiwicmVzcG9uc2VDYWxsYmFjayIsIl9zYW5pdGl6ZU9wdGlvbnMiLCJfb3B0aW9ucyIsIl9lbmRlZCIsIl9lbmRpbmciLCJfcmVkaXJlY3RDb3VudCIsIl9yZWRpcmVjdHMiLCJfcmVxdWVzdEJvZHlMZW5ndGgiLCJfcmVxdWVzdEJvZHlCdWZmZXJzIiwiX29uTmF0aXZlUmVzcG9uc2UiLCJfcHJvY2Vzc1Jlc3BvbnNlIiwiY2F1c2UiLCJfcGVyZm9ybVJlcXVlc3QiLCJ3cmFwIiwicHJvdG9jb2xzIiwibWF4UmVkaXJlY3RzIiwibmF0aXZlUHJvdG9jb2xzIiwic2NoZW1lIiwibmF0aXZlUHJvdG9jb2wiLCJ3cmFwcGVkUHJvdG9jb2wiLCJkZWZpbmVQcm9wZXJ0aWVzIiwic3ByZWFkVXJsT2JqZWN0IiwiaXNTdHJpbmciLCJ2YWxpZGF0ZVVybCIsImlzRnVuY3Rpb24iLCJlcXVhbCIsIndyYXBwZWRSZXF1ZXN0IiwicGFyc2VkIiwicGFyc2UiLCJocmVmIiwidXJsT2JqZWN0IiwidGFyZ2V0Iiwic3ByZWFkIiwic3RhcnRzV2l0aCIsInNlYXJjaCIsInJlbW92ZU1hdGNoaW5nSGVhZGVycyIsInJlZ2V4IiwibGFzdFZhbHVlIiwiYmFzZUNsYXNzIiwiQ3VzdG9tRXJyb3IiLCJwcm9wZXJ0aWVzIiwiY2FwdHVyZVN0YWNrVHJhY2UiLCJjb25zdHJ1Y3RvciIsImRlc3Ryb3lSZXF1ZXN0IiwiX2N1cnJlbnRSZXF1ZXN0IiwiZW5jb2RpbmciLCJjdXJyZW50UmVxdWVzdCIsInJlbW92ZUhlYWRlciIsIm1zZWNzIiwiZGVzdHJveU9uVGltZW91dCIsInNvY2tldCIsImFkZExpc3RlbmVyIiwic3RhcnRUaW1lciIsIl90aW1lb3V0IiwiY2xlYXJUaW1lb3V0IiwiY2xlYXJUaW1lciIsIm9uY2UiLCJwcm9wZXJ0eSIsInNlYXJjaFBvcyIsImFnZW50cyIsImFnZW50IiwiX2N1cnJlbnRVcmwiLCJfaXNSZWRpcmVjdCIsImJ1ZmZlcnMiLCJ3cml0ZU5leHQiLCJmaW5pc2hlZCIsInN0YXR1c0NvZGUiLCJ0cmFja1JlZGlyZWN0cyIsImxvY2F0aW9uIiwiZm9sbG93UmVkaXJlY3RzIiwicmVzcG9uc2VVcmwiLCJyZWRpcmVjdHMiLCJiZWZvcmVSZWRpcmVjdCIsIkhvc3QiLCJyZXEiLCJnZXRIZWFkZXIiLCJyZWxhdGl2ZSIsImJhc2UiLCJjdXJyZW50SG9zdEhlYWRlciIsImN1cnJlbnRVcmxQYXJ0cyIsImN1cnJlbnRIb3N0IiwiY3VycmVudFVybCIsInJlZGlyZWN0VXJsIiwicmVzb2x2ZSIsInN1YmRvbWFpbiIsImRvdCIsImVuZHNXaXRoIiwiaXNTdWJkb21haW4iLCJyZXNwb25zZURldGFpbHMiLCJyZXF1ZXN0RGV0YWlscyIsImZsYWciLCJhcmd2IiwicG9zIiwidGVybWluYXRvclBvcyIsImV4dGVuc2lvbnMiLCJ0eXBlcyIsInByZWZlcmVuY2UiLCJkYiIsImV4dG5hbWUiLCJFWFRSQUNUX1RZUEVfUkVHRVhQIiwiVEVYVF9UWVBFX1JFR0VYUCIsImNoYXJzZXQiLCJleGVjIiwiY2hhcnNldHMiLCJleHRlbnNpb24iLCJleHRzIiwic3Vic3RyIiwidG8iLCJzIiwibSIsImgiLCJ3IiwieSIsInBsdXJhbCIsIm1zQWJzIiwibiIsImlzUGx1cmFsIiwicm91bmQiLCJwYXJzZUZsb2F0IiwiaXNGaW5pdGUiLCJsb25nIiwiZm10TG9uZyIsImZtdFNob3J0IiwiREVGQVVMVF9QT1JUUyIsImZ0cCIsImdvcGhlciIsIndzIiwid3NzIiwic3RyaW5nRW5kc1dpdGgiLCJnZXRFbnYiLCJnZXRQcm94eUZvclVybCIsInBhcnNlZFVybCIsInByb3RvIiwiTk9fUFJPWFkiLCJldmVyeSIsInBhcnNlZFByb3h5IiwicGFyc2VkUHJveHlIb3N0bmFtZSIsInBhcnNlZFByb3h5UG9ydCIsInNob3VsZFByb3h5Iiwib3MiLCJoYXNGbGFnIiwiZm9yY2VDb2xvciIsImdldFN1cHBvcnRMZXZlbCIsImlzVFRZIiwibWluIiwicGxhdGZvcm0iLCJvc1JlbGVhc2UiLCJub2RlIiwic2lnbiIsIkNJX05BTUUiLCJURUFNQ0lUWV9WRVJTSU9OIiwiQ09MT1JURVJNIiwiVEVSTV9QUk9HUkFNX1ZFUlNJT04iLCJURVJNX1BST0dSQU0iLCJURVJNIiwiaGFzQmFzaWMiLCJoYXMyNTYiLCJoYXMxNm0iLCJ0cmFuc2xhdGVMZXZlbCIsIkZPUkNFX0NPTE9SIiwic3Rkb3V0Iiwic3RyQXJyYXkiLCJyZXN1bHRBcnJheSIsImZpcnN0IiwiY29tcG9uZW50IiwicGFydHMiLCJkZWZpbml0aW9uIiwiRm9ybURhdGEkMSIsInByb3h5RnJvbUVudiIsInpsaWIiLCJFdmVudEVtaXR0ZXIiLCJfaW50ZXJvcERlZmF1bHRMZWdhY3kiLCJlIiwiRm9ybURhdGFfX2RlZmF1bHQiLCJ1cmxfX2RlZmF1bHQiLCJodHRwX19kZWZhdWx0IiwiaHR0cHNfX2RlZmF1bHQiLCJ1dGlsX19kZWZhdWx0IiwiZm9sbG93UmVkaXJlY3RzX19kZWZhdWx0IiwiemxpYl9fZGVmYXVsdCIsInN0cmVhbV9fZGVmYXVsdCIsIkV2ZW50RW1pdHRlcl9fZGVmYXVsdCIsInRoaXNBcmciLCJnZXRQcm90b3R5cGVPZiIsImtpbmRPZiIsImNhY2hlIiwidGhpbmciLCJraW5kT2ZUZXN0IiwidHlwZU9mVGVzdCIsImlzVW5kZWZpbmVkIiwiaXNBcnJheUJ1ZmZlciIsImlzTnVtYmVyIiwiaXNPYmplY3QiLCJpc1BsYWluT2JqZWN0IiwiU3ltYm9sIiwidG9TdHJpbmdUYWciLCJpc0RhdGUiLCJpc0ZpbGUiLCJpc0Jsb2IiLCJpc0ZpbGVMaXN0IiwiaXNVUkxTZWFyY2hQYXJhbXMiLCJhbGxPd25LZXlzIiwibCIsImZpbmRLZXkiLCJfa2V5IiwiX2dsb2JhbCIsImdsb2JhbFRoaXMiLCJpc0NvbnRleHREZWZpbmVkIiwiY29udGV4dCIsImlzVHlwZWRBcnJheSIsIlR5cGVkQXJyYXkiLCJVaW50OEFycmF5IiwiaXNIVE1MRm9ybSIsImlzUmVnRXhwIiwicmVkdWNlRGVzY3JpcHRvcnMiLCJyZWR1Y2VyIiwiZGVzY3JpcHRvcnMiLCJnZXRPd25Qcm9wZXJ0eURlc2NyaXB0b3JzIiwicmVkdWNlZERlc2NyaXB0b3JzIiwiZGVzY3JpcHRvciIsInJldCIsIkFMUEhBIiwiRElHSVQiLCJBTFBIQUJFVCIsIkFMUEhBX0RJR0lUIiwiaXNBc3luY0ZuIiwidXRpbHMiLCJpc0Zvcm1EYXRhIiwia2luZCIsImlzQXJyYXlCdWZmZXJWaWV3IiwiQXJyYXlCdWZmZXIiLCJpc1ZpZXciLCJpc0Jvb2xlYW4iLCJtZXJnZSIsImNhc2VsZXNzIiwiYXNzaWduVmFsdWUiLCJ0YXJnZXRLZXkiLCJzdHJpcEJPTSIsImNvbnRlbnQiLCJzdXBlckNvbnN0cnVjdG9yIiwicHJvcHMiLCJ0b0ZsYXRPYmplY3QiLCJzb3VyY2VPYmoiLCJkZXN0T2JqIiwicHJvcEZpbHRlciIsIm1lcmdlZCIsInNlYXJjaFN0cmluZyIsImxhc3RJbmRleCIsInRvQXJyYXkiLCJhcnIiLCJmb3JFYWNoRW50cnkiLCJkb25lIiwicGFpciIsIm1hdGNoQWxsIiwicmVnRXhwIiwibWF0Y2hlcyIsImhhc093blByb3AiLCJmcmVlemVNZXRob2RzIiwidG9PYmplY3RTZXQiLCJhcnJheU9yU3RyaW5nIiwidG9DYW1lbENhc2UiLCJwMSIsInAyIiwidG9GaW5pdGVOdW1iZXIiLCJkZWZhdWx0VmFsdWUiLCJnZW5lcmF0ZVN0cmluZyIsImFscGhhYmV0IiwiaXNTcGVjQ29tcGxpYW50Rm9ybSIsInRvSlNPTk9iamVjdCIsInZpc2l0IiwicmVkdWNlZFZhbHVlIiwiaXNUaGVuYWJsZSIsImNhdGNoIiwiQXhpb3NFcnJvciIsInRvSlNPTiIsIm51bWJlciIsImZpbGVOYW1lIiwibGluZU51bWJlciIsImNvbHVtbk51bWJlciIsInByb3RvdHlwZSQxIiwiaXNWaXNpdGFibGUiLCJyZW1vdmVCcmFja2V0cyIsInJlbmRlcktleSIsImRvdHMiLCJ0b2tlbiIsImN1c3RvbVByb3BzIiwiYXhpb3NFcnJvciIsInByZWRpY2F0ZXMiLCJ0b0Zvcm1EYXRhIiwibWV0YVRva2VucyIsImluZGV4ZXMiLCJ2aXNpdG9yIiwiZGVmYXVsdFZpc2l0b3IiLCJ1c2VCbG9iIiwiY29udmVydFZhbHVlIiwiaXNGbGF0QXJyYXkiLCJlbCIsImV4cG9zZWRIZWxwZXJzIiwiYnVpbGQiLCJlbmNvZGUkMSIsImNoYXJNYXAiLCJBeGlvc1VSTFNlYXJjaFBhcmFtcyIsIl9wYWlycyIsImJ1aWxkVVJMIiwiX2VuY29kZSIsInNlcmlhbGl6ZUZuIiwic2VyaWFsaXplIiwic2VyaWFsaXplZFBhcmFtcyIsImhhc2htYXJrSW5kZXgiLCJlbmNvZGVyIiwiSW50ZXJjZXB0b3JNYW5hZ2VyJDEiLCJJbnRlcmNlcHRvck1hbmFnZXIiLCJoYW5kbGVycyIsInVzZSIsImZ1bGZpbGxlZCIsInJlamVjdGVkIiwic3luY2hyb25vdXMiLCJydW5XaGVuIiwiZWplY3QiLCJjbGVhciIsInRyYW5zaXRpb25hbERlZmF1bHRzIiwic2lsZW50SlNPTlBhcnNpbmciLCJmb3JjZWRKU09OUGFyc2luZyIsImNsYXJpZnlUaW1lb3V0RXJyb3IiLCJpc05vZGUiLCJjbGFzc2VzIiwiZm9ybURhdGFUb0pTT04iLCJidWlsZFBhdGgiLCJpc051bWVyaWNLZXkiLCJpc0xhc3QiLCJhcnJheVRvT2JqZWN0IiwicGFyc2VQcm9wUGF0aCIsInRyYW5zaXRpb25hbCIsImFkYXB0ZXIiLCJ0cmFuc2Zvcm1SZXF1ZXN0IiwiZ2V0Q29udGVudFR5cGUiLCJoYXNKU09OQ29udGVudFR5cGUiLCJpc09iamVjdFBheWxvYWQiLCJzZXRDb250ZW50VHlwZSIsImhlbHBlcnMiLCJ0b1VSTEVuY29kZWRGb3JtIiwiZm9ybVNlcmlhbGl6ZXIiLCJfRm9ybURhdGEiLCJyYXdWYWx1ZSIsInBhcnNlciIsInN0cmluZ2lmeVNhZmVseSIsInRyYW5zZm9ybVJlc3BvbnNlIiwiSlNPTlJlcXVlc3RlZCIsInJlc3BvbnNlVHlwZSIsInN0cmljdEpTT05QYXJzaW5nIiwiRVJSX0JBRF9SRVNQT05TRSIsInhzcmZDb29raWVOYW1lIiwieHNyZkhlYWRlck5hbWUiLCJtYXhDb250ZW50TGVuZ3RoIiwidmFsaWRhdGVTdGF0dXMiLCJjb21tb24iLCJkZWZhdWx0cyQxIiwiaWdub3JlRHVwbGljYXRlT2YiLCIkaW50ZXJuYWxzIiwibm9ybWFsaXplSGVhZGVyIiwibm9ybWFsaXplVmFsdWUiLCJtYXRjaEhlYWRlclZhbHVlIiwiaXNIZWFkZXJOYW1lRmlsdGVyIiwidmFsdWVPclJld3JpdGUiLCJyZXdyaXRlIiwiX3ZhbHVlIiwiX2hlYWRlciIsIl9yZXdyaXRlIiwibEhlYWRlciIsInNldEhlYWRlcnMiLCJyYXdIZWFkZXJzIiwibGluZSIsInBhcnNlSGVhZGVycyIsInRva2VucyIsInRva2Vuc1JFIiwicGFyc2VUb2tlbnMiLCJtYXRjaGVyIiwiZGVsZXRlZCIsImRlbGV0ZUhlYWRlciIsIm5vcm1hbGl6ZWQiLCJjaGFyIiwiZm9ybWF0SGVhZGVyIiwidGFyZ2V0cyIsImFzU3RyaW5ncyIsInN0YXRpYyIsImNvbXB1dGVkIiwiYWNjZXNzb3JzIiwiZGVmaW5lQWNjZXNzb3IiLCJhY2Nlc3Nvck5hbWUiLCJtZXRob2ROYW1lIiwiYnVpbGRBY2Nlc3NvcnMiLCJhY2Nlc3NvciIsIm1hcHBlZCIsImhlYWRlclZhbHVlIiwiQXhpb3NIZWFkZXJzJDEiLCJ0cmFuc2Zvcm1EYXRhIiwiZm5zIiwiaXNDYW5jZWwiLCJfX0NBTkNFTF9fIiwiQ2FuY2VsZWRFcnJvciIsIkVSUl9DQU5DRUxFRCIsInNldHRsZSIsInJlamVjdCIsIkVSUl9CQURfUkVRVUVTVCIsImJ1aWxkRnVsbFBhdGgiLCJiYXNlVVJMIiwicmVxdWVzdGVkVVJMIiwiaXNBYnNvbHV0ZVVSTCIsInJlbGF0aXZlVVJMIiwiY29tYmluZVVSTHMiLCJWRVJTSU9OIiwicGFyc2VQcm90b2NvbCIsIkRBVEFfVVJMX1BBVFRFUk4iLCJzcGVlZG9tZXRlciIsInNhbXBsZXNDb3VudCIsImJ5dGVzIiwidGltZXN0YW1wcyIsImZpcnN0U2FtcGxlVFMiLCJoZWFkIiwidGFpbCIsImNodW5rTGVuZ3RoIiwibm93Iiwic3RhcnRlZEF0IiwiYnl0ZXNDb3VudCIsInBhc3NlZCIsImtJbnRlcm5hbHMiLCJBeGlvc1RyYW5zZm9ybVN0cmVhbSIsIlRyYW5zZm9ybSIsInN1cGVyIiwicmVhZGFibGVIaWdoV2F0ZXJNYXJrIiwibWF4UmF0ZSIsImNodW5rU2l6ZSIsIm1pbkNodW5rU2l6ZSIsInRpbWVXaW5kb3ciLCJ0aWNrc1JhdGUiLCJpbnRlcm5hbHMiLCJieXRlc1NlZW4iLCJpc0NhcHR1cmVkIiwibm90aWZpZWRCeXRlc0xvYWRlZCIsInRzIiwib25SZWFkQ2FsbGJhY2siLCJfc3BlZWRvbWV0ZXIiLCJieXRlc05vdGlmaWVkIiwidXBkYXRlUHJvZ3Jlc3MiLCJmcmVxIiwidGltZXN0YW1wIiwidGhyZXNob2xkIiwidGltZXIiLCJmb3JjZSIsInRocm90dGxlIiwidG90YWxCeXRlcyIsImJ5dGVzVHJhbnNmZXJyZWQiLCJwcm9ncmVzc0J5dGVzIiwiZGVzdHJveWVkIiwicmF0ZSIsIm9uRmluaXNoIiwiX3JlYWQiLCJfdHJhbnNmb3JtIiwiY2h1bmsiLCJieXRlc1RocmVzaG9sZCIsIm1heCIsInRyYW5zZm9ybUNodW5rIiwiX2NodW5rIiwiX2NhbGxiYWNrIiwiYnl0ZXNMZWZ0IiwiY2h1bmtSZW1haW5kZXIiLCJtYXhDaHVua1NpemUiLCJzdWJhcnJheSIsInB1c2hDaHVuayIsInRyYW5zZm9ybU5leHRDaHVuayIsInNldExlbmd0aCIsIkF4aW9zVHJhbnNmb3JtU3RyZWFtJDEiLCJhc3luY0l0ZXJhdG9yIiwicmVhZEJsb2IkMSIsImJsb2IiLCJhcnJheUJ1ZmZlciIsIkJPVU5EQVJZX0FMUEhBQkVUIiwidGV4dEVuY29kZXIiLCJUZXh0RW5jb2RlciIsIkNSTEYiLCJDUkxGX0JZVEVTIiwiRm9ybURhdGFQYXJ0IiwiZXNjYXBlTmFtZSIsImlzU3RyaW5nVmFsdWUiLCJjb250ZW50TGVuZ3RoIiwiZm9ybURhdGFUb1N0cmVhbSQxIiwiZm9ybSIsImhlYWRlcnNIYW5kbGVyIiwiYm91bmRhcnlCeXRlcyIsImZvb3RlckJ5dGVzIiwicGFydCIsImNvbXB1dGVkSGVhZGVycyIsIlJlYWRhYmxlIiwiWmxpYkhlYWRlclRyYW5zZm9ybVN0cmVhbSIsIl9fdHJhbnNmb3JtIiwiWmxpYkhlYWRlclRyYW5zZm9ybVN0cmVhbSQxIiwiY2FsbGJhY2tpZnkkMSIsInpsaWJPcHRpb25zIiwiZmx1c2giLCJjb25zdGFudHMiLCJaX1NZTkNfRkxVU0giLCJmaW5pc2hGbHVzaCIsImJyb3RsaU9wdGlvbnMiLCJCUk9UTElfT1BFUkFUSU9OX0ZMVVNIIiwiaXNCcm90bGlTdXBwb3J0ZWQiLCJjcmVhdGVCcm90bGlEZWNvbXByZXNzIiwiaHR0cEZvbGxvdyIsImh0dHBzRm9sbG93IiwiaXNIdHRwcyIsInN1cHBvcnRlZFByb3RvY29scyIsImRpc3BhdGNoQmVmb3JlUmVkaXJlY3QiLCJiZWZvcmVSZWRpcmVjdHMiLCJzZXRQcm94eSIsImNvbmZpZ1Byb3h5IiwicHJveHlVcmwiLCJhdXRoIiwicGFzc3dvcmQiLCJwcm94eUhvc3QiLCJyZWRpcmVjdE9wdGlvbnMiLCJpc0h0dHBBZGFwdGVyU3VwcG9ydGVkIiwiYnVpbGRBZGRyZXNzRW50cnkiLCJmYW1pbHkiLCJyZXNvbHZlRmFtaWx5IiwiaHR0cEFkYXB0ZXIiLCJhc3luY0V4ZWN1dG9yIiwib25Eb25lIiwicmVzcG9uc2VFbmNvZGluZyIsImlzRG9uZSIsIl9sb29rdXAiLCJvcHQiLCJhcmcwIiwiYWRkcmVzc2VzIiwiYWRkciIsImFsbCIsImVtaXR0ZXIiLCJvbkZpbmlzaGVkIiwiY2FuY2VsVG9rZW4iLCJzaWduYWwiLCJyZW1vdmVFdmVudExpc3RlbmVyIiwicmVtb3ZlQWxsTGlzdGVuZXJzIiwiaXNSZWplY3RlZCIsInN1YnNjcmliZSIsImFib3J0ZWQiLCJhZGRFdmVudExpc3RlbmVyIiwiZnVsbFBhdGgiLCJjb252ZXJ0ZWREYXRhIiwidXJpIiwiYXNCbG9iIiwiX0Jsb2IiLCJFUlJfSU5WQUxJRF9VUkwiLCJpc0Jhc2U2NCIsImRlY29kZVVSSUNvbXBvbmVudCIsIkVSUl9OT1RfU1VQUE9SVCIsImZyb21EYXRhVVJJIiwib25Eb3dubG9hZFByb2dyZXNzIiwib25VcGxvYWRQcm9ncmVzcyIsIm1heFVwbG9hZFJhdGUiLCJtYXhEb3dubG9hZFJhdGUiLCJ1c2VyQm91bmRhcnkiLCJoYXNDb250ZW50TGVuZ3RoIiwicHJvbWlzaWZ5Iiwic2V0Q29udGVudExlbmd0aCIsImdldENvbnRlbnRMZW5ndGgiLCJvYmplY3RNb2RlIiwicGlwZWxpbmUiLCJwcm9ncmVzcyIsInVwbG9hZCIsInBhcmFtc1NlcmlhbGl6ZXIiLCJjdXN0b21FcnIiLCJleGlzdHMiLCJodHRwQWdlbnQiLCJodHRwc0FnZW50IiwidHJhbnNwb3J0Iiwic29ja2V0UGF0aCIsImlzSHR0cHNSZXF1ZXN0IiwiaW5zZWN1cmVIVFRQUGFyc2VyIiwic3RyZWFtcyIsInJlc3BvbnNlTGVuZ3RoIiwidHJhbnNmb3JtU3RyZWFtIiwiZG93bmxvYWQiLCJyZXNwb25zZVN0cmVhbSIsImxhc3RSZXF1ZXN0IiwiZGVjb21wcmVzcyIsImNyZWF0ZVVuemlwIiwib2ZmTGlzdGVuZXJzIiwic3RhdHVzTWVzc2FnZSIsInJlc3BvbnNlQnVmZmVyIiwidG90YWxSZXNwb25zZUJ5dGVzIiwicmVzcG9uc2VEYXRhIiwic2V0S2VlcEFsaXZlIiwiaXNOYU4iLCJFUlJfQkFEX09QVElPTl9WQUxVRSIsInRpbWVvdXRFcnJvck1lc3NhZ2UiLCJFVElNRURPVVQiLCJFQ09OTkFCT1JURUQiLCJlbmRlZCIsImVycm9yZWQiLCJQcm9taXNlIiwiX3JlamVjdCIsIm9uRG9uZUhhbmRsZXIiLCJjb29raWVzIiwiaXNTdGFuZGFyZEJyb3dzZXJFbnYiLCJleHBpcmVzIiwic2VjdXJlIiwiY29va2llIiwidG9HTVRTdHJpbmciLCJyZWFkIiwicmVtb3ZlIiwiaXNVUkxTYW1lT3JpZ2luIiwibXNpZSIsInVybFBhcnNpbmdOb2RlIiwiY3JlYXRlRWxlbWVudCIsIm9yaWdpblVSTCIsInJlc29sdmVVUkwiLCJzZXRBdHRyaWJ1dGUiLCJyZXF1ZXN0VVJMIiwicHJvZ3Jlc3NFdmVudFJlZHVjZXIiLCJsaXN0ZW5lciIsImlzRG93bmxvYWRTdHJlYW0iLCJsb2FkZWQiLCJsZW5ndGhDb21wdXRhYmxlIiwiZXN0aW1hdGVkIiwia25vd25BZGFwdGVycyIsInhociIsIlhNTEh0dHBSZXF1ZXN0IiwicmVxdWVzdERhdGEiLCJvbkNhbmNlbGVkIiwiaXNTdGFuZGFyZEJyb3dzZXJXZWJXb3JrZXJFbnYiLCJ1bmVzY2FwZSIsImJ0b2EiLCJvbmxvYWRlbmQiLCJyZXNwb25zZUhlYWRlcnMiLCJnZXRBbGxSZXNwb25zZUhlYWRlcnMiLCJyZXNwb25zZVRleHQiLCJvcGVuIiwib25yZWFkeXN0YXRlY2hhbmdlIiwicmVhZHlTdGF0ZSIsInJlc3BvbnNlVVJMIiwib25hYm9ydCIsIm9uZXJyb3IiLCJFUlJfTkVUV09SSyIsIm9udGltZW91dCIsInhzcmZWYWx1ZSIsInNldFJlcXVlc3RIZWFkZXIiLCJ3aXRoQ3JlZGVudGlhbHMiLCJjYW5jZWwiLCJzZW5kIiwicmVuZGVyUmVhc29uIiwiaXNSZXNvbHZlZEhhbmRsZSIsImFkYXB0ZXJzIiwibmFtZU9yQWRhcHRlciIsInJlamVjdGVkUmVhc29ucyIsInJlYXNvbnMiLCJ0aHJvd0lmQ2FuY2VsbGF0aW9uUmVxdWVzdGVkIiwidGhyb3dJZlJlcXVlc3RlZCIsImRpc3BhdGNoUmVxdWVzdCIsImhlYWRlcnNUb09iamVjdCIsIm1lcmdlQ29uZmlnIiwiY29uZmlnMSIsImNvbmZpZzIiLCJnZXRNZXJnZWRWYWx1ZSIsIm1lcmdlRGVlcFByb3BlcnRpZXMiLCJ2YWx1ZUZyb21Db25maWcyIiwiZGVmYXVsdFRvQ29uZmlnMiIsIm1lcmdlRGlyZWN0S2V5cyIsIm1lcmdlTWFwIiwidGltZW91dE1lc3NhZ2UiLCJjb25maWdWYWx1ZSIsInZhbGlkYXRvcnMkMSIsImRlcHJlY2F0ZWRXYXJuaW5ncyIsInZhbGlkYXRvciIsImZvcm1hdE1lc3NhZ2UiLCJkZXNjIiwib3B0cyIsIkVSUl9ERVBSRUNBVEVEIiwiYXNzZXJ0T3B0aW9ucyIsInNjaGVtYSIsImFsbG93VW5rbm93biIsIkVSUl9CQURfT1BUSU9OIiwidmFsaWRhdG9ycyIsIkF4aW9zIiwiaW5zdGFuY2VDb25maWciLCJpbnRlcmNlcHRvcnMiLCJjb25maWdPclVybCIsImJvb2xlYW4iLCJmdW5jdGlvbiIsImNvbnRleHRIZWFkZXJzIiwicmVxdWVzdEludGVyY2VwdG9yQ2hhaW4iLCJzeW5jaHJvbm91c1JlcXVlc3RJbnRlcmNlcHRvcnMiLCJpbnRlcmNlcHRvciIsInJlc3BvbnNlSW50ZXJjZXB0b3JDaGFpbiIsInByb21pc2UiLCJjaGFpbiIsIm5ld0NvbmZpZyIsIm9uRnVsZmlsbGVkIiwib25SZWplY3RlZCIsImdldFVyaSIsImdlbmVyYXRlSFRUUE1ldGhvZCIsImlzRm9ybSIsIkF4aW9zJDEiLCJDYW5jZWxUb2tlbiIsImV4ZWN1dG9yIiwicmVzb2x2ZVByb21pc2UiLCJfbGlzdGVuZXJzIiwib25mdWxmaWxsZWQiLCJfcmVzb2x2ZSIsIkNhbmNlbFRva2VuJDEiLCJIdHRwU3RhdHVzQ29kZSIsIkNvbnRpbnVlIiwiU3dpdGNoaW5nUHJvdG9jb2xzIiwiUHJvY2Vzc2luZyIsIkVhcmx5SGludHMiLCJPayIsIkNyZWF0ZWQiLCJBY2NlcHRlZCIsIk5vbkF1dGhvcml0YXRpdmVJbmZvcm1hdGlvbiIsIk5vQ29udGVudCIsIlJlc2V0Q29udGVudCIsIlBhcnRpYWxDb250ZW50IiwiTXVsdGlTdGF0dXMiLCJBbHJlYWR5UmVwb3J0ZWQiLCJJbVVzZWQiLCJNdWx0aXBsZUNob2ljZXMiLCJNb3ZlZFBlcm1hbmVudGx5IiwiRm91bmQiLCJTZWVPdGhlciIsIk5vdE1vZGlmaWVkIiwiVXNlUHJveHkiLCJVbnVzZWQiLCJUZW1wb3JhcnlSZWRpcmVjdCIsIlBlcm1hbmVudFJlZGlyZWN0IiwiQmFkUmVxdWVzdCIsIlVuYXV0aG9yaXplZCIsIlBheW1lbnRSZXF1aXJlZCIsIkZvcmJpZGRlbiIsIk5vdEZvdW5kIiwiTWV0aG9kTm90QWxsb3dlZCIsIk5vdEFjY2VwdGFibGUiLCJQcm94eUF1dGhlbnRpY2F0aW9uUmVxdWlyZWQiLCJSZXF1ZXN0VGltZW91dCIsIkNvbmZsaWN0IiwiR29uZSIsIkxlbmd0aFJlcXVpcmVkIiwiUHJlY29uZGl0aW9uRmFpbGVkIiwiUGF5bG9hZFRvb0xhcmdlIiwiVXJpVG9vTG9uZyIsIlVuc3VwcG9ydGVkTWVkaWFUeXBlIiwiUmFuZ2VOb3RTYXRpc2ZpYWJsZSIsIkV4cGVjdGF0aW9uRmFpbGVkIiwiSW1BVGVhcG90IiwiTWlzZGlyZWN0ZWRSZXF1ZXN0IiwiVW5wcm9jZXNzYWJsZUVudGl0eSIsIkxvY2tlZCIsIkZhaWxlZERlcGVuZGVuY3kiLCJUb29FYXJseSIsIlVwZ3JhZGVSZXF1aXJlZCIsIlByZWNvbmRpdGlvblJlcXVpcmVkIiwiVG9vTWFueVJlcXVlc3RzIiwiUmVxdWVzdEhlYWRlckZpZWxkc1Rvb0xhcmdlIiwiVW5hdmFpbGFibGVGb3JMZWdhbFJlYXNvbnMiLCJJbnRlcm5hbFNlcnZlckVycm9yIiwiTm90SW1wbGVtZW50ZWQiLCJCYWRHYXRld2F5IiwiU2VydmljZVVuYXZhaWxhYmxlIiwiR2F0ZXdheVRpbWVvdXQiLCJIdHRwVmVyc2lvbk5vdFN1cHBvcnRlZCIsIlZhcmlhbnRBbHNvTmVnb3RpYXRlcyIsIkluc3VmZmljaWVudFN0b3JhZ2UiLCJMb29wRGV0ZWN0ZWQiLCJOb3RFeHRlbmRlZCIsIk5ldHdvcmtBdXRoZW50aWNhdGlvblJlcXVpcmVkIiwiSHR0cFN0YXR1c0NvZGUkMSIsImF4aW9zIiwiY3JlYXRlSW5zdGFuY2UiLCJkZWZhdWx0Q29uZmlnIiwiaW5zdGFuY2UiLCJDYW5jZWwiLCJwcm9taXNlcyIsImlzQXhpb3NFcnJvciIsInBheWxvYWQiLCJmb3JtVG9KU09OIiwiZ2V0QWRhcHRlciIsIl9fd2VicGFja19tb2R1bGVfY2FjaGVfXyIsIl9fd2VicGFja19yZXF1aXJlX18iLCJtb2R1bGVJZCIsImNhY2hlZE1vZHVsZSIsIl9fd2VicGFja19tb2R1bGVzX18iLCJubWQiLCJwYXRocyIsImNoaWxkcmVuIiwiX193ZWJwYWNrX2V4cG9ydHNfXyJdLCJzb3VyY2VSb290IjoiIn0= + +/***/ }), + +/***/ 7426: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +/*! + * mime-db + * Copyright(c) 2014 Jonathan Ong + * Copyright(c) 2015-2022 Douglas Christopher Wilson + * MIT Licensed + */ + +/** + * Module exports. + */ + +module.exports = __nccwpck_require__(3765) + + +/***/ }), + +/***/ 3583: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; +/*! + * mime-types + * Copyright(c) 2014 Jonathan Ong + * Copyright(c) 2015 Douglas Christopher Wilson + * MIT Licensed + */ + + + +/** + * Module dependencies. + * @private + */ + +var db = __nccwpck_require__(7426) +var extname = (__nccwpck_require__(1017).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 + } + }) +} + + +/***/ }), + +/***/ 4294: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +module.exports = __nccwpck_require__(4219); + + +/***/ }), + +/***/ 4219: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + + +var net = __nccwpck_require__(1808); +var tls = __nccwpck_require__(4404); +var http = __nccwpck_require__(3685); +var https = __nccwpck_require__(5687); +var events = __nccwpck_require__(2361); +var assert = __nccwpck_require__(9491); +var util = __nccwpck_require__(3837); + + +exports.httpOverHttp = httpOverHttp; +exports.httpsOverHttp = httpsOverHttp; +exports.httpOverHttps = httpOverHttps; +exports.httpsOverHttps = httpsOverHttps; + + +function httpOverHttp(options) { + var agent = new TunnelingAgent(options); + agent.request = http.request; + return agent; +} + +function httpsOverHttp(options) { + var agent = new TunnelingAgent(options); + agent.request = http.request; + agent.createSocket = createSecureSocket; + agent.defaultPort = 443; + return agent; +} + +function httpOverHttps(options) { + var agent = new TunnelingAgent(options); + agent.request = https.request; + return agent; +} + +function httpsOverHttps(options) { + var agent = new TunnelingAgent(options); + agent.request = https.request; + agent.createSocket = createSecureSocket; + agent.defaultPort = 443; + return agent; +} + + +function TunnelingAgent(options) { + var self = this; + self.options = options || {}; + self.proxyOptions = self.options.proxy || {}; + self.maxSockets = self.options.maxSockets || http.Agent.defaultMaxSockets; + self.requests = []; + self.sockets = []; + + self.on('free', function onFree(socket, host, port, localAddress) { + var options = toOptions(host, port, localAddress); + for (var i = 0, len = self.requests.length; i < len; ++i) { + var pending = self.requests[i]; + if (pending.host === options.host && pending.port === options.port) { + // Detect the request to connect same origin server, + // reuse the connection. + self.requests.splice(i, 1); + pending.request.onSocket(socket); + return; + } + } + socket.destroy(); + self.removeSocket(socket); + }); +} +util.inherits(TunnelingAgent, events.EventEmitter); + +TunnelingAgent.prototype.addRequest = function addRequest(req, host, port, localAddress) { + var self = this; + var options = mergeOptions({request: req}, self.options, toOptions(host, port, localAddress)); + + if (self.sockets.length >= this.maxSockets) { + // We are over limit so we'll add it to the queue. + self.requests.push(options); + return; + } + + // If we are under maxSockets create a new one. + self.createSocket(options, function(socket) { + socket.on('free', onFree); + socket.on('close', onCloseOrRemove); + socket.on('agentRemove', onCloseOrRemove); + req.onSocket(socket); + + function onFree() { + self.emit('free', socket, options); + } + + function onCloseOrRemove(err) { + self.removeSocket(socket); + socket.removeListener('free', onFree); + socket.removeListener('close', onCloseOrRemove); + socket.removeListener('agentRemove', onCloseOrRemove); + } + }); +}; + +TunnelingAgent.prototype.createSocket = function createSocket(options, cb) { + var self = this; + var placeholder = {}; + self.sockets.push(placeholder); + + var connectOptions = mergeOptions({}, self.proxyOptions, { + method: 'CONNECT', + path: options.host + ':' + options.port, + agent: false, + headers: { + host: options.host + ':' + options.port + } + }); + if (options.localAddress) { + connectOptions.localAddress = options.localAddress; + } + if (connectOptions.proxyAuth) { + connectOptions.headers = connectOptions.headers || {}; + connectOptions.headers['Proxy-Authorization'] = 'Basic ' + + new Buffer(connectOptions.proxyAuth).toString('base64'); + } + + debug('making CONNECT request'); + var connectReq = self.request(connectOptions); + connectReq.useChunkedEncodingByDefault = false; // for v0.6 + connectReq.once('response', onResponse); // for v0.6 + connectReq.once('upgrade', onUpgrade); // for v0.6 + connectReq.once('connect', onConnect); // for v0.7 or later + connectReq.once('error', onError); + connectReq.end(); + + function onResponse(res) { + // Very hacky. This is necessary to avoid http-parser leaks. + res.upgrade = true; + } + + function onUpgrade(res, socket, head) { + // Hacky. + process.nextTick(function() { + onConnect(res, socket, head); + }); + } + + function onConnect(res, socket, head) { + connectReq.removeAllListeners(); + socket.removeAllListeners(); + + if (res.statusCode !== 200) { + debug('tunneling socket could not be established, statusCode=%d', + res.statusCode); + socket.destroy(); + var error = new Error('tunneling socket could not be established, ' + + 'statusCode=' + res.statusCode); + error.code = 'ECONNRESET'; + options.request.emit('error', error); + self.removeSocket(placeholder); + return; + } + if (head.length > 0) { + debug('got illegal response body from proxy'); + socket.destroy(); + var error = new Error('got illegal response body from proxy'); + error.code = 'ECONNRESET'; + options.request.emit('error', error); + self.removeSocket(placeholder); + return; + } + debug('tunneling connection has established'); + self.sockets[self.sockets.indexOf(placeholder)] = socket; + return cb(socket); + } + + function onError(cause) { + connectReq.removeAllListeners(); + + debug('tunneling socket could not be established, cause=%s\n', + cause.message, cause.stack); + var error = new Error('tunneling socket could not be established, ' + + 'cause=' + cause.message); + error.code = 'ECONNRESET'; + options.request.emit('error', error); + self.removeSocket(placeholder); + } +}; + +TunnelingAgent.prototype.removeSocket = function removeSocket(socket) { + var pos = this.sockets.indexOf(socket) + if (pos === -1) { + return; + } + this.sockets.splice(pos, 1); + + var pending = this.requests.shift(); + if (pending) { + // If we have pending requests and a socket gets closed a new one + // needs to be created to take over in the pool for the one that closed. + this.createSocket(pending, function(socket) { + pending.request.onSocket(socket); + }); + } +}; + +function createSecureSocket(options, cb) { + var self = this; + TunnelingAgent.prototype.createSocket.call(self, options, function(socket) { + var hostHeader = options.request.getHeader('host'); + var tlsOptions = mergeOptions({}, self.options, { + socket: socket, + servername: hostHeader ? hostHeader.replace(/:.*$/, '') : options.host + }); + + // 0 is dummy port for v0.6 + var secureSocket = tls.connect(0, tlsOptions); + self.sockets[self.sockets.indexOf(socket)] = secureSocket; + cb(secureSocket); + }); +} + + +function toOptions(host, port, localAddress) { + if (typeof host === 'string') { // since v0.10 + return { + host: host, + port: port, + localAddress: localAddress + }; + } + return host; // for v0.11 or later +} + +function mergeOptions(target) { + for (var i = 1, len = arguments.length; i < len; ++i) { + var overrides = arguments[i]; + if (typeof overrides === 'object') { + var keys = Object.keys(overrides); + for (var j = 0, keyLen = keys.length; j < keyLen; ++j) { + var k = keys[j]; + if (overrides[k] !== undefined) { + target[k] = overrides[k]; + } + } + } + } + return target; +} + + +var debug; +if (process.env.NODE_DEBUG && /\btunnel\b/.test(process.env.NODE_DEBUG)) { + debug = function() { + var args = Array.prototype.slice.call(arguments); + if (typeof args[0] === 'string') { + args[0] = 'TUNNEL: ' + args[0]; + } else { + args.unshift('TUNNEL:'); + } + console.error.apply(console, args); + } +} else { + debug = function() {}; +} +exports.debug = debug; // for test + + +/***/ }), + +/***/ 1773: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const Client = __nccwpck_require__(3598) +const Dispatcher = __nccwpck_require__(412) +const errors = __nccwpck_require__(8045) +const Pool = __nccwpck_require__(4634) +const BalancedPool = __nccwpck_require__(7931) +const Agent = __nccwpck_require__(7890) +const util = __nccwpck_require__(3983) +const { InvalidArgumentError } = errors +const api = __nccwpck_require__(4059) +const buildConnector = __nccwpck_require__(2067) +const MockClient = __nccwpck_require__(8687) +const MockAgent = __nccwpck_require__(6771) +const MockPool = __nccwpck_require__(6193) +const mockErrors = __nccwpck_require__(888) +const ProxyAgent = __nccwpck_require__(7858) +const RetryHandler = __nccwpck_require__(2286) +const { getGlobalDispatcher, setGlobalDispatcher } = __nccwpck_require__(1892) +const DecoratorHandler = __nccwpck_require__(6930) +const RedirectHandler = __nccwpck_require__(2860) +const createRedirectInterceptor = __nccwpck_require__(8861) + +let hasCrypto +try { + __nccwpck_require__(6113) + hasCrypto = true +} catch { + hasCrypto = false +} + +Object.assign(Dispatcher.prototype, api) + +module.exports.Dispatcher = Dispatcher +module.exports.Client = Client +module.exports.Pool = Pool +module.exports.BalancedPool = BalancedPool +module.exports.Agent = Agent +module.exports.ProxyAgent = ProxyAgent +module.exports.RetryHandler = RetryHandler + +module.exports.DecoratorHandler = DecoratorHandler +module.exports.RedirectHandler = RedirectHandler +module.exports.createRedirectInterceptor = createRedirectInterceptor + +module.exports.buildConnector = buildConnector +module.exports.errors = errors + +function makeDispatcher (fn) { + return (url, opts, handler) => { + if (typeof opts === 'function') { + handler = opts + opts = null + } + + if (!url || (typeof url !== 'string' && typeof url !== 'object' && !(url instanceof URL))) { + throw new InvalidArgumentError('invalid url') + } + + if (opts != null && typeof opts !== 'object') { + throw new InvalidArgumentError('invalid opts') + } + + if (opts && opts.path != null) { + if (typeof opts.path !== 'string') { + throw new InvalidArgumentError('invalid opts.path') + } + + let path = opts.path + if (!opts.path.startsWith('/')) { + path = `/${path}` + } + + url = new URL(util.parseOrigin(url).origin + path) + } else { + if (!opts) { + opts = typeof url === 'object' ? url : {} + } + + url = util.parseURL(url) + } + + const { agent, dispatcher = getGlobalDispatcher() } = opts + + if (agent) { + throw new InvalidArgumentError('unsupported opts.agent. Did you mean opts.client?') + } + + return fn.call(dispatcher, { + ...opts, + origin: url.origin, + path: url.search ? `${url.pathname}${url.search}` : url.pathname, + method: opts.method || (opts.body ? 'PUT' : 'GET') + }, handler) + } +} + +module.exports.setGlobalDispatcher = setGlobalDispatcher +module.exports.getGlobalDispatcher = getGlobalDispatcher + +if (util.nodeMajor > 16 || (util.nodeMajor === 16 && util.nodeMinor >= 8)) { + let fetchImpl = null + module.exports.fetch = async function fetch (resource) { + if (!fetchImpl) { + fetchImpl = (__nccwpck_require__(4881).fetch) + } + + try { + return await fetchImpl(...arguments) + } catch (err) { + if (typeof err === 'object') { + Error.captureStackTrace(err, this) + } + + throw err + } + } + module.exports.Headers = __nccwpck_require__(554).Headers + module.exports.Response = __nccwpck_require__(7823).Response + module.exports.Request = __nccwpck_require__(8359).Request + module.exports.FormData = __nccwpck_require__(2015).FormData + module.exports.File = __nccwpck_require__(8511).File + module.exports.FileReader = __nccwpck_require__(1446).FileReader + + const { setGlobalOrigin, getGlobalOrigin } = __nccwpck_require__(1246) + + module.exports.setGlobalOrigin = setGlobalOrigin + module.exports.getGlobalOrigin = getGlobalOrigin + + const { CacheStorage } = __nccwpck_require__(7907) + const { kConstruct } = __nccwpck_require__(9174) + + // Cache & CacheStorage are tightly coupled with fetch. Even if it may run + // in an older version of Node, it doesn't have any use without fetch. + module.exports.caches = new CacheStorage(kConstruct) +} + +if (util.nodeMajor >= 16) { + const { deleteCookie, getCookies, getSetCookies, setCookie } = __nccwpck_require__(1724) + + module.exports.deleteCookie = deleteCookie + module.exports.getCookies = getCookies + module.exports.getSetCookies = getSetCookies + module.exports.setCookie = setCookie + + const { parseMIMEType, serializeAMimeType } = __nccwpck_require__(685) + + module.exports.parseMIMEType = parseMIMEType + module.exports.serializeAMimeType = serializeAMimeType +} + +if (util.nodeMajor >= 18 && hasCrypto) { + const { WebSocket } = __nccwpck_require__(4284) + + module.exports.WebSocket = WebSocket +} + +module.exports.request = makeDispatcher(api.request) +module.exports.stream = makeDispatcher(api.stream) +module.exports.pipeline = makeDispatcher(api.pipeline) +module.exports.connect = makeDispatcher(api.connect) +module.exports.upgrade = makeDispatcher(api.upgrade) + +module.exports.MockClient = MockClient +module.exports.MockPool = MockPool +module.exports.MockAgent = MockAgent +module.exports.mockErrors = mockErrors + + +/***/ }), + +/***/ 7890: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { InvalidArgumentError } = __nccwpck_require__(8045) +const { kClients, kRunning, kClose, kDestroy, kDispatch, kInterceptors } = __nccwpck_require__(2785) +const DispatcherBase = __nccwpck_require__(4839) +const Pool = __nccwpck_require__(4634) +const Client = __nccwpck_require__(3598) +const util = __nccwpck_require__(3983) +const createRedirectInterceptor = __nccwpck_require__(8861) +const { WeakRef, FinalizationRegistry } = __nccwpck_require__(6436)() + +const kOnConnect = Symbol('onConnect') +const kOnDisconnect = Symbol('onDisconnect') +const kOnConnectionError = Symbol('onConnectionError') +const kMaxRedirections = Symbol('maxRedirections') +const kOnDrain = Symbol('onDrain') +const kFactory = Symbol('factory') +const kFinalizer = Symbol('finalizer') +const kOptions = Symbol('options') + +function defaultFactory (origin, opts) { + return opts && opts.connections === 1 + ? new Client(origin, opts) + : new Pool(origin, opts) +} + +class Agent extends DispatcherBase { + constructor ({ factory = defaultFactory, maxRedirections = 0, connect, ...options } = {}) { + super() + + if (typeof factory !== 'function') { + throw new InvalidArgumentError('factory must be a function.') + } + + if (connect != null && typeof connect !== 'function' && typeof connect !== 'object') { + throw new InvalidArgumentError('connect must be a function or an object') + } + + if (!Number.isInteger(maxRedirections) || maxRedirections < 0) { + throw new InvalidArgumentError('maxRedirections must be a positive number') + } + + if (connect && typeof connect !== 'function') { + connect = { ...connect } + } + + this[kInterceptors] = options.interceptors && options.interceptors.Agent && Array.isArray(options.interceptors.Agent) + ? options.interceptors.Agent + : [createRedirectInterceptor({ maxRedirections })] + + this[kOptions] = { ...util.deepClone(options), connect } + this[kOptions].interceptors = options.interceptors + ? { ...options.interceptors } + : undefined + this[kMaxRedirections] = maxRedirections + this[kFactory] = factory + this[kClients] = new Map() + this[kFinalizer] = new FinalizationRegistry(/* istanbul ignore next: gc is undeterministic */ key => { + const ref = this[kClients].get(key) + if (ref !== undefined && ref.deref() === undefined) { + this[kClients].delete(key) + } + }) + + const agent = this + + this[kOnDrain] = (origin, targets) => { + agent.emit('drain', origin, [agent, ...targets]) + } + + this[kOnConnect] = (origin, targets) => { + agent.emit('connect', origin, [agent, ...targets]) + } + + this[kOnDisconnect] = (origin, targets, err) => { + agent.emit('disconnect', origin, [agent, ...targets], err) + } + + this[kOnConnectionError] = (origin, targets, err) => { + agent.emit('connectionError', origin, [agent, ...targets], err) + } + } + + get [kRunning] () { + let ret = 0 + for (const ref of this[kClients].values()) { + const client = ref.deref() + /* istanbul ignore next: gc is undeterministic */ + if (client) { + ret += client[kRunning] + } + } + return ret + } + + [kDispatch] (opts, handler) { + let key + if (opts.origin && (typeof opts.origin === 'string' || opts.origin instanceof URL)) { + key = String(opts.origin) + } else { + throw new InvalidArgumentError('opts.origin must be a non-empty string or URL.') + } + + const ref = this[kClients].get(key) + + let dispatcher = ref ? ref.deref() : null + if (!dispatcher) { + dispatcher = this[kFactory](opts.origin, this[kOptions]) + .on('drain', this[kOnDrain]) + .on('connect', this[kOnConnect]) + .on('disconnect', this[kOnDisconnect]) + .on('connectionError', this[kOnConnectionError]) + + this[kClients].set(key, new WeakRef(dispatcher)) + this[kFinalizer].register(dispatcher, key) + } + + return dispatcher.dispatch(opts, handler) + } + + async [kClose] () { + const closePromises = [] + for (const ref of this[kClients].values()) { + const client = ref.deref() + /* istanbul ignore else: gc is undeterministic */ + if (client) { + closePromises.push(client.close()) + } + } + + await Promise.all(closePromises) + } + + async [kDestroy] (err) { + const destroyPromises = [] + for (const ref of this[kClients].values()) { + const client = ref.deref() + /* istanbul ignore else: gc is undeterministic */ + if (client) { + destroyPromises.push(client.destroy(err)) + } + } + + await Promise.all(destroyPromises) + } +} + +module.exports = Agent + + +/***/ }), + +/***/ 7032: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const { addAbortListener } = __nccwpck_require__(3983) +const { RequestAbortedError } = __nccwpck_require__(8045) + +const kListener = Symbol('kListener') +const kSignal = Symbol('kSignal') + +function abort (self) { + if (self.abort) { + self.abort() + } else { + self.onError(new RequestAbortedError()) + } +} + +function addSignal (self, signal) { + self[kSignal] = null + self[kListener] = null + + if (!signal) { + return + } + + if (signal.aborted) { + abort(self) + return + } + + self[kSignal] = signal + self[kListener] = () => { + abort(self) + } + + addAbortListener(self[kSignal], self[kListener]) +} + +function removeSignal (self) { + if (!self[kSignal]) { + return + } + + if ('removeEventListener' in self[kSignal]) { + self[kSignal].removeEventListener('abort', self[kListener]) + } else { + self[kSignal].removeListener('abort', self[kListener]) + } + + self[kSignal] = null + self[kListener] = null +} + +module.exports = { + addSignal, + removeSignal +} + + +/***/ }), + +/***/ 9744: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { AsyncResource } = __nccwpck_require__(852) +const { InvalidArgumentError, RequestAbortedError, SocketError } = __nccwpck_require__(8045) +const util = __nccwpck_require__(3983) +const { addSignal, removeSignal } = __nccwpck_require__(7032) + +class ConnectHandler extends AsyncResource { + constructor (opts, callback) { + if (!opts || typeof opts !== 'object') { + throw new InvalidArgumentError('invalid opts') + } + + if (typeof callback !== 'function') { + throw new InvalidArgumentError('invalid callback') + } + + const { signal, opaque, responseHeaders } = opts + + if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') { + throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget') + } + + super('UNDICI_CONNECT') + + this.opaque = opaque || null + this.responseHeaders = responseHeaders || null + this.callback = callback + this.abort = null + + addSignal(this, signal) + } + + onConnect (abort, context) { + if (!this.callback) { + throw new RequestAbortedError() + } + + this.abort = abort + this.context = context + } + + onHeaders () { + throw new SocketError('bad connect', null) + } + + onUpgrade (statusCode, rawHeaders, socket) { + const { callback, opaque, context } = this + + removeSignal(this) + + this.callback = null + + let headers = rawHeaders + // Indicates is an HTTP2Session + if (headers != null) { + headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + } + + this.runInAsyncScope(callback, null, null, { + statusCode, + headers, + socket, + opaque, + context + }) + } + + onError (err) { + const { callback, opaque } = this + + removeSignal(this) + + if (callback) { + this.callback = null + queueMicrotask(() => { + this.runInAsyncScope(callback, null, err, { opaque }) + }) + } + } +} + +function connect (opts, callback) { + if (callback === undefined) { + return new Promise((resolve, reject) => { + connect.call(this, opts, (err, data) => { + return err ? reject(err) : resolve(data) + }) + }) + } + + try { + const connectHandler = new ConnectHandler(opts, callback) + this.dispatch({ ...opts, method: 'CONNECT' }, connectHandler) + } catch (err) { + if (typeof callback !== 'function') { + throw err + } + const opaque = opts && opts.opaque + queueMicrotask(() => callback(err, { opaque })) + } +} + +module.exports = connect + + +/***/ }), + +/***/ 8752: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { + Readable, + Duplex, + PassThrough +} = __nccwpck_require__(2781) +const { + InvalidArgumentError, + InvalidReturnValueError, + RequestAbortedError +} = __nccwpck_require__(8045) +const util = __nccwpck_require__(3983) +const { AsyncResource } = __nccwpck_require__(852) +const { addSignal, removeSignal } = __nccwpck_require__(7032) +const assert = __nccwpck_require__(9491) + +const kResume = Symbol('resume') + +class PipelineRequest extends Readable { + constructor () { + super({ autoDestroy: true }) + + this[kResume] = null + } + + _read () { + const { [kResume]: resume } = this + + if (resume) { + this[kResume] = null + resume() + } + } + + _destroy (err, callback) { + this._read() + + callback(err) + } +} + +class PipelineResponse extends Readable { + constructor (resume) { + super({ autoDestroy: true }) + this[kResume] = resume + } + + _read () { + this[kResume]() + } + + _destroy (err, callback) { + if (!err && !this._readableState.endEmitted) { + err = new RequestAbortedError() + } + + callback(err) + } +} + +class PipelineHandler extends AsyncResource { + constructor (opts, handler) { + if (!opts || typeof opts !== 'object') { + throw new InvalidArgumentError('invalid opts') + } + + if (typeof handler !== 'function') { + throw new InvalidArgumentError('invalid handler') + } + + const { signal, method, opaque, onInfo, responseHeaders } = opts + + if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') { + throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget') + } + + if (method === 'CONNECT') { + throw new InvalidArgumentError('invalid method') + } + + if (onInfo && typeof onInfo !== 'function') { + throw new InvalidArgumentError('invalid onInfo callback') + } + + super('UNDICI_PIPELINE') + + this.opaque = opaque || null + this.responseHeaders = responseHeaders || null + this.handler = handler + this.abort = null + this.context = null + this.onInfo = onInfo || null + + this.req = new PipelineRequest().on('error', util.nop) + + this.ret = new Duplex({ + readableObjectMode: opts.objectMode, + autoDestroy: true, + read: () => { + const { body } = this + + if (body && body.resume) { + body.resume() + } + }, + write: (chunk, encoding, callback) => { + const { req } = this + + if (req.push(chunk, encoding) || req._readableState.destroyed) { + callback() + } else { + req[kResume] = callback + } + }, + destroy: (err, callback) => { + const { body, req, res, ret, abort } = this + + if (!err && !ret._readableState.endEmitted) { + err = new RequestAbortedError() + } + + if (abort && err) { + abort() + } + + util.destroy(body, err) + util.destroy(req, err) + util.destroy(res, err) + + removeSignal(this) + + callback(err) + } + }).on('prefinish', () => { + const { req } = this + + // Node < 15 does not call _final in same tick. + req.push(null) + }) + + this.res = null + + addSignal(this, signal) + } + + onConnect (abort, context) { + const { ret, res } = this + + assert(!res, 'pipeline cannot be retried') + + if (ret.destroyed) { + throw new RequestAbortedError() + } + + this.abort = abort + this.context = context + } + + onHeaders (statusCode, rawHeaders, resume) { + const { opaque, handler, context } = this + + if (statusCode < 200) { + if (this.onInfo) { + const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + this.onInfo({ statusCode, headers }) + } + return + } + + this.res = new PipelineResponse(resume) + + let body + try { + this.handler = null + const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + body = this.runInAsyncScope(handler, null, { + statusCode, + headers, + opaque, + body: this.res, + context + }) + } catch (err) { + this.res.on('error', util.nop) + throw err + } + + if (!body || typeof body.on !== 'function') { + throw new InvalidReturnValueError('expected Readable') + } + + body + .on('data', (chunk) => { + const { ret, body } = this + + if (!ret.push(chunk) && body.pause) { + body.pause() + } + }) + .on('error', (err) => { + const { ret } = this + + util.destroy(ret, err) + }) + .on('end', () => { + const { ret } = this + + ret.push(null) + }) + .on('close', () => { + const { ret } = this + + if (!ret._readableState.ended) { + util.destroy(ret, new RequestAbortedError()) + } + }) + + this.body = body + } + + onData (chunk) { + const { res } = this + return res.push(chunk) + } + + onComplete (trailers) { + const { res } = this + res.push(null) + } + + onError (err) { + const { ret } = this + this.handler = null + util.destroy(ret, err) + } +} + +function pipeline (opts, handler) { + try { + const pipelineHandler = new PipelineHandler(opts, handler) + this.dispatch({ ...opts, body: pipelineHandler.req }, pipelineHandler) + return pipelineHandler.ret + } catch (err) { + return new PassThrough().destroy(err) + } +} + +module.exports = pipeline + + +/***/ }), + +/***/ 5448: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const Readable = __nccwpck_require__(3858) +const { + InvalidArgumentError, + RequestAbortedError +} = __nccwpck_require__(8045) +const util = __nccwpck_require__(3983) +const { getResolveErrorBodyCallback } = __nccwpck_require__(7474) +const { AsyncResource } = __nccwpck_require__(852) +const { addSignal, removeSignal } = __nccwpck_require__(7032) + +class RequestHandler extends AsyncResource { + constructor (opts, callback) { + if (!opts || typeof opts !== 'object') { + throw new InvalidArgumentError('invalid opts') + } + + const { signal, method, opaque, body, onInfo, responseHeaders, throwOnError, highWaterMark } = opts + + try { + if (typeof callback !== 'function') { + throw new InvalidArgumentError('invalid callback') + } + + if (highWaterMark && (typeof highWaterMark !== 'number' || highWaterMark < 0)) { + throw new InvalidArgumentError('invalid highWaterMark') + } + + if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') { + throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget') + } + + if (method === 'CONNECT') { + throw new InvalidArgumentError('invalid method') + } + + if (onInfo && typeof onInfo !== 'function') { + throw new InvalidArgumentError('invalid onInfo callback') + } + + super('UNDICI_REQUEST') + } catch (err) { + if (util.isStream(body)) { + util.destroy(body.on('error', util.nop), err) + } + throw err + } + + this.responseHeaders = responseHeaders || null + this.opaque = opaque || null + this.callback = callback + this.res = null + this.abort = null + this.body = body + this.trailers = {} + this.context = null + this.onInfo = onInfo || null + this.throwOnError = throwOnError + this.highWaterMark = highWaterMark + + if (util.isStream(body)) { + body.on('error', (err) => { + this.onError(err) + }) + } + + addSignal(this, signal) + } + + onConnect (abort, context) { + if (!this.callback) { + throw new RequestAbortedError() + } + + this.abort = abort + this.context = context + } + + onHeaders (statusCode, rawHeaders, resume, statusMessage) { + const { callback, opaque, abort, context, responseHeaders, highWaterMark } = this + + const headers = responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + + if (statusCode < 200) { + if (this.onInfo) { + this.onInfo({ statusCode, headers }) + } + return + } + + const parsedHeaders = responseHeaders === 'raw' ? util.parseHeaders(rawHeaders) : headers + const contentType = parsedHeaders['content-type'] + const body = new Readable({ resume, abort, contentType, highWaterMark }) + + this.callback = null + this.res = body + if (callback !== null) { + if (this.throwOnError && statusCode >= 400) { + this.runInAsyncScope(getResolveErrorBodyCallback, null, + { callback, body, contentType, statusCode, statusMessage, headers } + ) + } else { + this.runInAsyncScope(callback, null, null, { + statusCode, + headers, + trailers: this.trailers, + opaque, + body, + context + }) + } + } + } + + onData (chunk) { + const { res } = this + return res.push(chunk) + } + + onComplete (trailers) { + const { res } = this + + removeSignal(this) + + util.parseHeaders(trailers, this.trailers) + + res.push(null) + } + + onError (err) { + const { res, callback, body, opaque } = this + + removeSignal(this) + + if (callback) { + // TODO: Does this need queueMicrotask? + this.callback = null + queueMicrotask(() => { + this.runInAsyncScope(callback, null, err, { opaque }) + }) + } + + if (res) { + this.res = null + // Ensure all queued handlers are invoked before destroying res. + queueMicrotask(() => { + util.destroy(res, err) + }) + } + + if (body) { + this.body = null + util.destroy(body, err) + } + } +} + +function request (opts, callback) { + if (callback === undefined) { + return new Promise((resolve, reject) => { + request.call(this, opts, (err, data) => { + return err ? reject(err) : resolve(data) + }) + }) + } + + try { + this.dispatch(opts, new RequestHandler(opts, callback)) + } catch (err) { + if (typeof callback !== 'function') { + throw err + } + const opaque = opts && opts.opaque + queueMicrotask(() => callback(err, { opaque })) + } +} + +module.exports = request +module.exports.RequestHandler = RequestHandler + + +/***/ }), + +/***/ 5395: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { finished, PassThrough } = __nccwpck_require__(2781) +const { + InvalidArgumentError, + InvalidReturnValueError, + RequestAbortedError +} = __nccwpck_require__(8045) +const util = __nccwpck_require__(3983) +const { getResolveErrorBodyCallback } = __nccwpck_require__(7474) +const { AsyncResource } = __nccwpck_require__(852) +const { addSignal, removeSignal } = __nccwpck_require__(7032) + +class StreamHandler extends AsyncResource { + constructor (opts, factory, callback) { + if (!opts || typeof opts !== 'object') { + throw new InvalidArgumentError('invalid opts') + } + + const { signal, method, opaque, body, onInfo, responseHeaders, throwOnError } = opts + + try { + if (typeof callback !== 'function') { + throw new InvalidArgumentError('invalid callback') + } + + if (typeof factory !== 'function') { + throw new InvalidArgumentError('invalid factory') + } + + if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') { + throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget') + } + + if (method === 'CONNECT') { + throw new InvalidArgumentError('invalid method') + } + + if (onInfo && typeof onInfo !== 'function') { + throw new InvalidArgumentError('invalid onInfo callback') + } + + super('UNDICI_STREAM') + } catch (err) { + if (util.isStream(body)) { + util.destroy(body.on('error', util.nop), err) + } + throw err + } + + this.responseHeaders = responseHeaders || null + this.opaque = opaque || null + this.factory = factory + this.callback = callback + this.res = null + this.abort = null + this.context = null + this.trailers = null + this.body = body + this.onInfo = onInfo || null + this.throwOnError = throwOnError || false + + if (util.isStream(body)) { + body.on('error', (err) => { + this.onError(err) + }) + } + + addSignal(this, signal) + } + + onConnect (abort, context) { + if (!this.callback) { + throw new RequestAbortedError() + } + + this.abort = abort + this.context = context + } + + onHeaders (statusCode, rawHeaders, resume, statusMessage) { + const { factory, opaque, context, callback, responseHeaders } = this + + const headers = responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + + if (statusCode < 200) { + if (this.onInfo) { + this.onInfo({ statusCode, headers }) + } + return + } + + this.factory = null + + let res + + if (this.throwOnError && statusCode >= 400) { + const parsedHeaders = responseHeaders === 'raw' ? util.parseHeaders(rawHeaders) : headers + const contentType = parsedHeaders['content-type'] + res = new PassThrough() + + this.callback = null + this.runInAsyncScope(getResolveErrorBodyCallback, null, + { callback, body: res, contentType, statusCode, statusMessage, headers } + ) + } else { + if (factory === null) { + return + } + + res = this.runInAsyncScope(factory, null, { + statusCode, + headers, + opaque, + context + }) + + if ( + !res || + typeof res.write !== 'function' || + typeof res.end !== 'function' || + typeof res.on !== 'function' + ) { + throw new InvalidReturnValueError('expected Writable') + } + + // TODO: Avoid finished. It registers an unnecessary amount of listeners. + finished(res, { readable: false }, (err) => { + const { callback, res, opaque, trailers, abort } = this + + this.res = null + if (err || !res.readable) { + util.destroy(res, err) + } + + this.callback = null + this.runInAsyncScope(callback, null, err || null, { opaque, trailers }) + + if (err) { + abort() + } + }) + } + + res.on('drain', resume) + + this.res = res + + const needDrain = res.writableNeedDrain !== undefined + ? res.writableNeedDrain + : res._writableState && res._writableState.needDrain + + return needDrain !== true + } + + onData (chunk) { + const { res } = this + + return res ? res.write(chunk) : true + } + + onComplete (trailers) { + const { res } = this + + removeSignal(this) + + if (!res) { + return + } + + this.trailers = util.parseHeaders(trailers) + + res.end() + } + + onError (err) { + const { res, callback, opaque, body } = this + + removeSignal(this) + + this.factory = null + + if (res) { + this.res = null + util.destroy(res, err) + } else if (callback) { + this.callback = null + queueMicrotask(() => { + this.runInAsyncScope(callback, null, err, { opaque }) + }) + } + + if (body) { + this.body = null + util.destroy(body, err) + } + } +} + +function stream (opts, factory, callback) { + if (callback === undefined) { + return new Promise((resolve, reject) => { + stream.call(this, opts, factory, (err, data) => { + return err ? reject(err) : resolve(data) + }) + }) + } + + try { + this.dispatch(opts, new StreamHandler(opts, factory, callback)) + } catch (err) { + if (typeof callback !== 'function') { + throw err + } + const opaque = opts && opts.opaque + queueMicrotask(() => callback(err, { opaque })) + } +} + +module.exports = stream + + +/***/ }), + +/***/ 6923: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { InvalidArgumentError, RequestAbortedError, SocketError } = __nccwpck_require__(8045) +const { AsyncResource } = __nccwpck_require__(852) +const util = __nccwpck_require__(3983) +const { addSignal, removeSignal } = __nccwpck_require__(7032) +const assert = __nccwpck_require__(9491) + +class UpgradeHandler extends AsyncResource { + constructor (opts, callback) { + if (!opts || typeof opts !== 'object') { + throw new InvalidArgumentError('invalid opts') + } + + if (typeof callback !== 'function') { + throw new InvalidArgumentError('invalid callback') + } + + const { signal, opaque, responseHeaders } = opts + + if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') { + throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget') + } + + super('UNDICI_UPGRADE') + + this.responseHeaders = responseHeaders || null + this.opaque = opaque || null + this.callback = callback + this.abort = null + this.context = null + + addSignal(this, signal) + } + + onConnect (abort, context) { + if (!this.callback) { + throw new RequestAbortedError() + } + + this.abort = abort + this.context = null + } + + onHeaders () { + throw new SocketError('bad upgrade', null) + } + + onUpgrade (statusCode, rawHeaders, socket) { + const { callback, opaque, context } = this + + assert.strictEqual(statusCode, 101) + + removeSignal(this) + + this.callback = null + const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) + this.runInAsyncScope(callback, null, null, { + headers, + socket, + opaque, + context + }) + } + + onError (err) { + const { callback, opaque } = this + + removeSignal(this) + + if (callback) { + this.callback = null + queueMicrotask(() => { + this.runInAsyncScope(callback, null, err, { opaque }) + }) + } + } +} + +function upgrade (opts, callback) { + if (callback === undefined) { + return new Promise((resolve, reject) => { + upgrade.call(this, opts, (err, data) => { + return err ? reject(err) : resolve(data) + }) + }) + } + + try { + const upgradeHandler = new UpgradeHandler(opts, callback) + this.dispatch({ + ...opts, + method: opts.method || 'GET', + upgrade: opts.protocol || 'Websocket' + }, upgradeHandler) + } catch (err) { + if (typeof callback !== 'function') { + throw err + } + const opaque = opts && opts.opaque + queueMicrotask(() => callback(err, { opaque })) + } +} + +module.exports = upgrade + + +/***/ }), + +/***/ 4059: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +module.exports.request = __nccwpck_require__(5448) +module.exports.stream = __nccwpck_require__(5395) +module.exports.pipeline = __nccwpck_require__(8752) +module.exports.upgrade = __nccwpck_require__(6923) +module.exports.connect = __nccwpck_require__(9744) + + +/***/ }), + +/***/ 3858: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; +// Ported from https://github.com/nodejs/undici/pull/907 + + + +const assert = __nccwpck_require__(9491) +const { Readable } = __nccwpck_require__(2781) +const { RequestAbortedError, NotSupportedError, InvalidArgumentError } = __nccwpck_require__(8045) +const util = __nccwpck_require__(3983) +const { ReadableStreamFrom, toUSVString } = __nccwpck_require__(3983) + +let Blob + +const kConsume = Symbol('kConsume') +const kReading = Symbol('kReading') +const kBody = Symbol('kBody') +const kAbort = Symbol('abort') +const kContentType = Symbol('kContentType') + +const noop = () => {} + +module.exports = class BodyReadable extends Readable { + constructor ({ + resume, + abort, + contentType = '', + highWaterMark = 64 * 1024 // Same as nodejs fs streams. + }) { + super({ + autoDestroy: true, + read: resume, + highWaterMark + }) + + this._readableState.dataEmitted = false + + this[kAbort] = abort + this[kConsume] = null + this[kBody] = null + this[kContentType] = contentType + + // Is stream being consumed through Readable API? + // This is an optimization so that we avoid checking + // for 'data' and 'readable' listeners in the hot path + // inside push(). + this[kReading] = false + } + + destroy (err) { + if (this.destroyed) { + // Node < 16 + return this + } + + if (!err && !this._readableState.endEmitted) { + err = new RequestAbortedError() + } + + if (err) { + this[kAbort]() + } + + return super.destroy(err) + } + + emit (ev, ...args) { + if (ev === 'data') { + // Node < 16.7 + this._readableState.dataEmitted = true + } else if (ev === 'error') { + // Node < 16 + this._readableState.errorEmitted = true + } + return super.emit(ev, ...args) + } + + on (ev, ...args) { + if (ev === 'data' || ev === 'readable') { + this[kReading] = true + } + return super.on(ev, ...args) + } + + addListener (ev, ...args) { + return this.on(ev, ...args) + } + + off (ev, ...args) { + const ret = super.off(ev, ...args) + if (ev === 'data' || ev === 'readable') { + this[kReading] = ( + this.listenerCount('data') > 0 || + this.listenerCount('readable') > 0 + ) + } + return ret + } + + removeListener (ev, ...args) { + return this.off(ev, ...args) + } + + push (chunk) { + if (this[kConsume] && chunk !== null && this.readableLength === 0) { + consumePush(this[kConsume], chunk) + return this[kReading] ? super.push(chunk) : true + } + return super.push(chunk) + } + + // https://fetch.spec.whatwg.org/#dom-body-text + async text () { + return consume(this, 'text') + } + + // https://fetch.spec.whatwg.org/#dom-body-json + async json () { + return consume(this, 'json') + } + + // https://fetch.spec.whatwg.org/#dom-body-blob + async blob () { + return consume(this, 'blob') + } + + // https://fetch.spec.whatwg.org/#dom-body-arraybuffer + async arrayBuffer () { + return consume(this, 'arrayBuffer') + } + + // https://fetch.spec.whatwg.org/#dom-body-formdata + async formData () { + // TODO: Implement. + throw new NotSupportedError() + } + + // https://fetch.spec.whatwg.org/#dom-body-bodyused + get bodyUsed () { + return util.isDisturbed(this) + } + + // https://fetch.spec.whatwg.org/#dom-body-body + get body () { + if (!this[kBody]) { + this[kBody] = ReadableStreamFrom(this) + if (this[kConsume]) { + // TODO: Is this the best way to force a lock? + this[kBody].getReader() // Ensure stream is locked. + assert(this[kBody].locked) + } + } + return this[kBody] + } + + dump (opts) { + let limit = opts && Number.isFinite(opts.limit) ? opts.limit : 262144 + const signal = opts && opts.signal + + if (signal) { + try { + if (typeof signal !== 'object' || !('aborted' in signal)) { + throw new InvalidArgumentError('signal must be an AbortSignal') + } + util.throwIfAborted(signal) + } catch (err) { + return Promise.reject(err) + } + } + + if (this.closed) { + return Promise.resolve(null) + } + + return new Promise((resolve, reject) => { + const signalListenerCleanup = signal + ? util.addAbortListener(signal, () => { + this.destroy() + }) + : noop + + this + .on('close', function () { + signalListenerCleanup() + if (signal && signal.aborted) { + reject(signal.reason || Object.assign(new Error('The operation was aborted'), { name: 'AbortError' })) + } else { + resolve(null) + } + }) + .on('error', noop) + .on('data', function (chunk) { + limit -= chunk.length + if (limit <= 0) { + this.destroy() + } + }) + .resume() + }) + } +} + +// https://streams.spec.whatwg.org/#readablestream-locked +function isLocked (self) { + // Consume is an implicit lock. + return (self[kBody] && self[kBody].locked === true) || self[kConsume] +} + +// https://fetch.spec.whatwg.org/#body-unusable +function isUnusable (self) { + return util.isDisturbed(self) || isLocked(self) +} + +async function consume (stream, type) { + if (isUnusable(stream)) { + throw new TypeError('unusable') + } + + assert(!stream[kConsume]) + + return new Promise((resolve, reject) => { + stream[kConsume] = { + type, + stream, + resolve, + reject, + length: 0, + body: [] + } + + stream + .on('error', function (err) { + consumeFinish(this[kConsume], err) + }) + .on('close', function () { + if (this[kConsume].body !== null) { + consumeFinish(this[kConsume], new RequestAbortedError()) + } + }) + + process.nextTick(consumeStart, stream[kConsume]) + }) +} + +function consumeStart (consume) { + if (consume.body === null) { + return + } + + const { _readableState: state } = consume.stream + + for (const chunk of state.buffer) { + consumePush(consume, chunk) + } + + if (state.endEmitted) { + consumeEnd(this[kConsume]) + } else { + consume.stream.on('end', function () { + consumeEnd(this[kConsume]) + }) + } + + consume.stream.resume() + + while (consume.stream.read() != null) { + // Loop + } +} + +function consumeEnd (consume) { + const { type, body, resolve, stream, length } = consume + + try { + if (type === 'text') { + resolve(toUSVString(Buffer.concat(body))) + } else if (type === 'json') { + resolve(JSON.parse(Buffer.concat(body))) + } else if (type === 'arrayBuffer') { + const dst = new Uint8Array(length) + + let pos = 0 + for (const buf of body) { + dst.set(buf, pos) + pos += buf.byteLength + } + + resolve(dst.buffer) + } else if (type === 'blob') { + if (!Blob) { + Blob = (__nccwpck_require__(4300).Blob) + } + resolve(new Blob(body, { type: stream[kContentType] })) + } + + consumeFinish(consume) + } catch (err) { + stream.destroy(err) + } +} + +function consumePush (consume, chunk) { + consume.length += chunk.length + consume.body.push(chunk) +} + +function consumeFinish (consume, err) { + if (consume.body === null) { + return + } + + if (err) { + consume.reject(err) + } else { + consume.resolve() + } + + consume.type = null + consume.stream = null + consume.resolve = null + consume.reject = null + consume.length = 0 + consume.body = null +} + + +/***/ }), + +/***/ 7474: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const assert = __nccwpck_require__(9491) +const { + ResponseStatusCodeError +} = __nccwpck_require__(8045) +const { toUSVString } = __nccwpck_require__(3983) + +async function getResolveErrorBodyCallback ({ callback, body, contentType, statusCode, statusMessage, headers }) { + assert(body) + + let chunks = [] + let limit = 0 + + for await (const chunk of body) { + chunks.push(chunk) + limit += chunk.length + if (limit > 128 * 1024) { + chunks = null + break + } + } + + if (statusCode === 204 || !contentType || !chunks) { + process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers)) + return + } + + try { + if (contentType.startsWith('application/json')) { + const payload = JSON.parse(toUSVString(Buffer.concat(chunks))) + process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers, payload)) + return + } + + if (contentType.startsWith('text/')) { + const payload = toUSVString(Buffer.concat(chunks)) + process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers, payload)) + return + } + } catch (err) { + // Process in a fallback if error + } + + process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers)) +} + +module.exports = { getResolveErrorBodyCallback } + + +/***/ }), + +/***/ 7931: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { + BalancedPoolMissingUpstreamError, + InvalidArgumentError +} = __nccwpck_require__(8045) +const { + PoolBase, + kClients, + kNeedDrain, + kAddClient, + kRemoveClient, + kGetDispatcher +} = __nccwpck_require__(3198) +const Pool = __nccwpck_require__(4634) +const { kUrl, kInterceptors } = __nccwpck_require__(2785) +const { parseOrigin } = __nccwpck_require__(3983) +const kFactory = Symbol('factory') + +const kOptions = Symbol('options') +const kGreatestCommonDivisor = Symbol('kGreatestCommonDivisor') +const kCurrentWeight = Symbol('kCurrentWeight') +const kIndex = Symbol('kIndex') +const kWeight = Symbol('kWeight') +const kMaxWeightPerServer = Symbol('kMaxWeightPerServer') +const kErrorPenalty = Symbol('kErrorPenalty') + +function getGreatestCommonDivisor (a, b) { + if (b === 0) return a + return getGreatestCommonDivisor(b, a % b) +} + +function defaultFactory (origin, opts) { + return new Pool(origin, opts) +} + +class BalancedPool extends PoolBase { + constructor (upstreams = [], { factory = defaultFactory, ...opts } = {}) { + super() + + this[kOptions] = opts + this[kIndex] = -1 + this[kCurrentWeight] = 0 + + this[kMaxWeightPerServer] = this[kOptions].maxWeightPerServer || 100 + this[kErrorPenalty] = this[kOptions].errorPenalty || 15 + + if (!Array.isArray(upstreams)) { + upstreams = [upstreams] + } + + if (typeof factory !== 'function') { + throw new InvalidArgumentError('factory must be a function.') + } + + this[kInterceptors] = opts.interceptors && opts.interceptors.BalancedPool && Array.isArray(opts.interceptors.BalancedPool) + ? opts.interceptors.BalancedPool + : [] + this[kFactory] = factory + + for (const upstream of upstreams) { + this.addUpstream(upstream) + } + this._updateBalancedPoolStats() + } + + addUpstream (upstream) { + const upstreamOrigin = parseOrigin(upstream).origin + + if (this[kClients].find((pool) => ( + pool[kUrl].origin === upstreamOrigin && + pool.closed !== true && + pool.destroyed !== true + ))) { + return this + } + const pool = this[kFactory](upstreamOrigin, Object.assign({}, this[kOptions])) + + this[kAddClient](pool) + pool.on('connect', () => { + pool[kWeight] = Math.min(this[kMaxWeightPerServer], pool[kWeight] + this[kErrorPenalty]) + }) + + pool.on('connectionError', () => { + pool[kWeight] = Math.max(1, pool[kWeight] - this[kErrorPenalty]) + this._updateBalancedPoolStats() + }) + + pool.on('disconnect', (...args) => { + const err = args[2] + if (err && err.code === 'UND_ERR_SOCKET') { + // decrease the weight of the pool. + pool[kWeight] = Math.max(1, pool[kWeight] - this[kErrorPenalty]) + this._updateBalancedPoolStats() + } + }) + + for (const client of this[kClients]) { + client[kWeight] = this[kMaxWeightPerServer] + } + + this._updateBalancedPoolStats() + + return this + } + + _updateBalancedPoolStats () { + this[kGreatestCommonDivisor] = this[kClients].map(p => p[kWeight]).reduce(getGreatestCommonDivisor, 0) + } + + removeUpstream (upstream) { + const upstreamOrigin = parseOrigin(upstream).origin + + const pool = this[kClients].find((pool) => ( + pool[kUrl].origin === upstreamOrigin && + pool.closed !== true && + pool.destroyed !== true + )) + + if (pool) { + this[kRemoveClient](pool) + } + + return this + } + + get upstreams () { + return this[kClients] + .filter(dispatcher => dispatcher.closed !== true && dispatcher.destroyed !== true) + .map((p) => p[kUrl].origin) + } + + [kGetDispatcher] () { + // We validate that pools is greater than 0, + // otherwise we would have to wait until an upstream + // is added, which might never happen. + if (this[kClients].length === 0) { + throw new BalancedPoolMissingUpstreamError() + } + + const dispatcher = this[kClients].find(dispatcher => ( + !dispatcher[kNeedDrain] && + dispatcher.closed !== true && + dispatcher.destroyed !== true + )) + + if (!dispatcher) { + return + } + + const allClientsBusy = this[kClients].map(pool => pool[kNeedDrain]).reduce((a, b) => a && b, true) + + if (allClientsBusy) { + return + } + + let counter = 0 + + let maxWeightIndex = this[kClients].findIndex(pool => !pool[kNeedDrain]) + + while (counter++ < this[kClients].length) { + this[kIndex] = (this[kIndex] + 1) % this[kClients].length + const pool = this[kClients][this[kIndex]] + + // find pool index with the largest weight + if (pool[kWeight] > this[kClients][maxWeightIndex][kWeight] && !pool[kNeedDrain]) { + maxWeightIndex = this[kIndex] + } + + // decrease the current weight every `this[kClients].length`. + if (this[kIndex] === 0) { + // Set the current weight to the next lower weight. + this[kCurrentWeight] = this[kCurrentWeight] - this[kGreatestCommonDivisor] + + if (this[kCurrentWeight] <= 0) { + this[kCurrentWeight] = this[kMaxWeightPerServer] + } + } + if (pool[kWeight] >= this[kCurrentWeight] && (!pool[kNeedDrain])) { + return pool + } + } + + this[kCurrentWeight] = this[kClients][maxWeightIndex][kWeight] + this[kIndex] = maxWeightIndex + return this[kClients][maxWeightIndex] + } +} + +module.exports = BalancedPool + + +/***/ }), + +/***/ 6101: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { kConstruct } = __nccwpck_require__(9174) +const { urlEquals, fieldValues: getFieldValues } = __nccwpck_require__(2396) +const { kEnumerableProperty, isDisturbed } = __nccwpck_require__(3983) +const { kHeadersList } = __nccwpck_require__(2785) +const { webidl } = __nccwpck_require__(1744) +const { Response, cloneResponse } = __nccwpck_require__(7823) +const { Request } = __nccwpck_require__(8359) +const { kState, kHeaders, kGuard, kRealm } = __nccwpck_require__(5861) +const { fetching } = __nccwpck_require__(4881) +const { urlIsHttpHttpsScheme, createDeferredPromise, readAllBytes } = __nccwpck_require__(2538) +const assert = __nccwpck_require__(9491) +const { getGlobalDispatcher } = __nccwpck_require__(1892) + +/** + * @see https://w3c.github.io/ServiceWorker/#dfn-cache-batch-operation + * @typedef {Object} CacheBatchOperation + * @property {'delete' | 'put'} type + * @property {any} request + * @property {any} response + * @property {import('../../types/cache').CacheQueryOptions} options + */ + +/** + * @see https://w3c.github.io/ServiceWorker/#dfn-request-response-list + * @typedef {[any, any][]} requestResponseList + */ + +class Cache { + /** + * @see https://w3c.github.io/ServiceWorker/#dfn-relevant-request-response-list + * @type {requestResponseList} + */ + #relevantRequestResponseList + + constructor () { + if (arguments[0] !== kConstruct) { + webidl.illegalConstructor() + } + + this.#relevantRequestResponseList = arguments[1] + } + + async match (request, options = {}) { + webidl.brandCheck(this, Cache) + webidl.argumentLengthCheck(arguments, 1, { header: 'Cache.match' }) + + request = webidl.converters.RequestInfo(request) + options = webidl.converters.CacheQueryOptions(options) + + const p = await this.matchAll(request, options) + + if (p.length === 0) { + return + } + + return p[0] + } + + async matchAll (request = undefined, options = {}) { + webidl.brandCheck(this, Cache) + + if (request !== undefined) request = webidl.converters.RequestInfo(request) + options = webidl.converters.CacheQueryOptions(options) + + // 1. + let r = null + + // 2. + if (request !== undefined) { + if (request instanceof Request) { + // 2.1.1 + r = request[kState] + + // 2.1.2 + if (r.method !== 'GET' && !options.ignoreMethod) { + return [] + } + } else if (typeof request === 'string') { + // 2.2.1 + r = new Request(request)[kState] + } + } + + // 5. + // 5.1 + const responses = [] + + // 5.2 + if (request === undefined) { + // 5.2.1 + for (const requestResponse of this.#relevantRequestResponseList) { + responses.push(requestResponse[1]) + } + } else { // 5.3 + // 5.3.1 + const requestResponses = this.#queryCache(r, options) + + // 5.3.2 + for (const requestResponse of requestResponses) { + responses.push(requestResponse[1]) + } + } + + // 5.4 + // We don't implement CORs so we don't need to loop over the responses, yay! + + // 5.5.1 + const responseList = [] + + // 5.5.2 + for (const response of responses) { + // 5.5.2.1 + const responseObject = new Response(response.body?.source ?? null) + const body = responseObject[kState].body + responseObject[kState] = response + responseObject[kState].body = body + responseObject[kHeaders][kHeadersList] = response.headersList + responseObject[kHeaders][kGuard] = 'immutable' + + responseList.push(responseObject) + } + + // 6. + return Object.freeze(responseList) + } + + async add (request) { + webidl.brandCheck(this, Cache) + webidl.argumentLengthCheck(arguments, 1, { header: 'Cache.add' }) + + request = webidl.converters.RequestInfo(request) + + // 1. + const requests = [request] + + // 2. + const responseArrayPromise = this.addAll(requests) + + // 3. + return await responseArrayPromise + } + + async addAll (requests) { + webidl.brandCheck(this, Cache) + webidl.argumentLengthCheck(arguments, 1, { header: 'Cache.addAll' }) + + requests = webidl.converters['sequence'](requests) + + // 1. + const responsePromises = [] + + // 2. + const requestList = [] + + // 3. + for (const request of requests) { + if (typeof request === 'string') { + continue + } + + // 3.1 + const r = request[kState] + + // 3.2 + if (!urlIsHttpHttpsScheme(r.url) || r.method !== 'GET') { + throw webidl.errors.exception({ + header: 'Cache.addAll', + message: 'Expected http/s scheme when method is not GET.' + }) + } + } + + // 4. + /** @type {ReturnType[]} */ + const fetchControllers = [] + + // 5. + for (const request of requests) { + // 5.1 + const r = new Request(request)[kState] + + // 5.2 + if (!urlIsHttpHttpsScheme(r.url)) { + throw webidl.errors.exception({ + header: 'Cache.addAll', + message: 'Expected http/s scheme.' + }) + } + + // 5.4 + r.initiator = 'fetch' + r.destination = 'subresource' + + // 5.5 + requestList.push(r) + + // 5.6 + const responsePromise = createDeferredPromise() + + // 5.7 + fetchControllers.push(fetching({ + request: r, + dispatcher: getGlobalDispatcher(), + processResponse (response) { + // 1. + if (response.type === 'error' || response.status === 206 || response.status < 200 || response.status > 299) { + responsePromise.reject(webidl.errors.exception({ + header: 'Cache.addAll', + message: 'Received an invalid status code or the request failed.' + })) + } else if (response.headersList.contains('vary')) { // 2. + // 2.1 + const fieldValues = getFieldValues(response.headersList.get('vary')) + + // 2.2 + for (const fieldValue of fieldValues) { + // 2.2.1 + if (fieldValue === '*') { + responsePromise.reject(webidl.errors.exception({ + header: 'Cache.addAll', + message: 'invalid vary field value' + })) + + for (const controller of fetchControllers) { + controller.abort() + } + + return + } + } + } + }, + processResponseEndOfBody (response) { + // 1. + if (response.aborted) { + responsePromise.reject(new DOMException('aborted', 'AbortError')) + return + } + + // 2. + responsePromise.resolve(response) + } + })) + + // 5.8 + responsePromises.push(responsePromise.promise) + } + + // 6. + const p = Promise.all(responsePromises) + + // 7. + const responses = await p + + // 7.1 + const operations = [] + + // 7.2 + let index = 0 + + // 7.3 + for (const response of responses) { + // 7.3.1 + /** @type {CacheBatchOperation} */ + const operation = { + type: 'put', // 7.3.2 + request: requestList[index], // 7.3.3 + response // 7.3.4 + } + + operations.push(operation) // 7.3.5 + + index++ // 7.3.6 + } + + // 7.5 + const cacheJobPromise = createDeferredPromise() + + // 7.6.1 + let errorData = null + + // 7.6.2 + try { + this.#batchCacheOperations(operations) + } catch (e) { + errorData = e + } + + // 7.6.3 + queueMicrotask(() => { + // 7.6.3.1 + if (errorData === null) { + cacheJobPromise.resolve(undefined) + } else { + // 7.6.3.2 + cacheJobPromise.reject(errorData) + } + }) + + // 7.7 + return cacheJobPromise.promise + } + + async put (request, response) { + webidl.brandCheck(this, Cache) + webidl.argumentLengthCheck(arguments, 2, { header: 'Cache.put' }) + + request = webidl.converters.RequestInfo(request) + response = webidl.converters.Response(response) + + // 1. + let innerRequest = null + + // 2. + if (request instanceof Request) { + innerRequest = request[kState] + } else { // 3. + innerRequest = new Request(request)[kState] + } + + // 4. + if (!urlIsHttpHttpsScheme(innerRequest.url) || innerRequest.method !== 'GET') { + throw webidl.errors.exception({ + header: 'Cache.put', + message: 'Expected an http/s scheme when method is not GET' + }) + } + + // 5. + const innerResponse = response[kState] + + // 6. + if (innerResponse.status === 206) { + throw webidl.errors.exception({ + header: 'Cache.put', + message: 'Got 206 status' + }) + } + + // 7. + if (innerResponse.headersList.contains('vary')) { + // 7.1. + const fieldValues = getFieldValues(innerResponse.headersList.get('vary')) + + // 7.2. + for (const fieldValue of fieldValues) { + // 7.2.1 + if (fieldValue === '*') { + throw webidl.errors.exception({ + header: 'Cache.put', + message: 'Got * vary field value' + }) + } + } + } + + // 8. + if (innerResponse.body && (isDisturbed(innerResponse.body.stream) || innerResponse.body.stream.locked)) { + throw webidl.errors.exception({ + header: 'Cache.put', + message: 'Response body is locked or disturbed' + }) + } + + // 9. + const clonedResponse = cloneResponse(innerResponse) + + // 10. + const bodyReadPromise = createDeferredPromise() + + // 11. + if (innerResponse.body != null) { + // 11.1 + const stream = innerResponse.body.stream + + // 11.2 + const reader = stream.getReader() + + // 11.3 + readAllBytes(reader).then(bodyReadPromise.resolve, bodyReadPromise.reject) + } else { + bodyReadPromise.resolve(undefined) + } + + // 12. + /** @type {CacheBatchOperation[]} */ + const operations = [] + + // 13. + /** @type {CacheBatchOperation} */ + const operation = { + type: 'put', // 14. + request: innerRequest, // 15. + response: clonedResponse // 16. + } + + // 17. + operations.push(operation) + + // 19. + const bytes = await bodyReadPromise.promise + + if (clonedResponse.body != null) { + clonedResponse.body.source = bytes + } + + // 19.1 + const cacheJobPromise = createDeferredPromise() + + // 19.2.1 + let errorData = null + + // 19.2.2 + try { + this.#batchCacheOperations(operations) + } catch (e) { + errorData = e + } + + // 19.2.3 + queueMicrotask(() => { + // 19.2.3.1 + if (errorData === null) { + cacheJobPromise.resolve() + } else { // 19.2.3.2 + cacheJobPromise.reject(errorData) + } + }) + + return cacheJobPromise.promise + } + + async delete (request, options = {}) { + webidl.brandCheck(this, Cache) + webidl.argumentLengthCheck(arguments, 1, { header: 'Cache.delete' }) + + request = webidl.converters.RequestInfo(request) + options = webidl.converters.CacheQueryOptions(options) + + /** + * @type {Request} + */ + let r = null + + if (request instanceof Request) { + r = request[kState] + + if (r.method !== 'GET' && !options.ignoreMethod) { + return false + } + } else { + assert(typeof request === 'string') + + r = new Request(request)[kState] + } + + /** @type {CacheBatchOperation[]} */ + const operations = [] + + /** @type {CacheBatchOperation} */ + const operation = { + type: 'delete', + request: r, + options + } + + operations.push(operation) + + const cacheJobPromise = createDeferredPromise() + + let errorData = null + let requestResponses + + try { + requestResponses = this.#batchCacheOperations(operations) + } catch (e) { + errorData = e + } + + queueMicrotask(() => { + if (errorData === null) { + cacheJobPromise.resolve(!!requestResponses?.length) + } else { + cacheJobPromise.reject(errorData) + } + }) + + return cacheJobPromise.promise + } + + /** + * @see https://w3c.github.io/ServiceWorker/#dom-cache-keys + * @param {any} request + * @param {import('../../types/cache').CacheQueryOptions} options + * @returns {readonly Request[]} + */ + async keys (request = undefined, options = {}) { + webidl.brandCheck(this, Cache) + + if (request !== undefined) request = webidl.converters.RequestInfo(request) + options = webidl.converters.CacheQueryOptions(options) + + // 1. + let r = null + + // 2. + if (request !== undefined) { + // 2.1 + if (request instanceof Request) { + // 2.1.1 + r = request[kState] + + // 2.1.2 + if (r.method !== 'GET' && !options.ignoreMethod) { + return [] + } + } else if (typeof request === 'string') { // 2.2 + r = new Request(request)[kState] + } + } + + // 4. + const promise = createDeferredPromise() + + // 5. + // 5.1 + const requests = [] + + // 5.2 + if (request === undefined) { + // 5.2.1 + for (const requestResponse of this.#relevantRequestResponseList) { + // 5.2.1.1 + requests.push(requestResponse[0]) + } + } else { // 5.3 + // 5.3.1 + const requestResponses = this.#queryCache(r, options) + + // 5.3.2 + for (const requestResponse of requestResponses) { + // 5.3.2.1 + requests.push(requestResponse[0]) + } + } + + // 5.4 + queueMicrotask(() => { + // 5.4.1 + const requestList = [] + + // 5.4.2 + for (const request of requests) { + const requestObject = new Request('https://a') + requestObject[kState] = request + requestObject[kHeaders][kHeadersList] = request.headersList + requestObject[kHeaders][kGuard] = 'immutable' + requestObject[kRealm] = request.client + + // 5.4.2.1 + requestList.push(requestObject) + } + + // 5.4.3 + promise.resolve(Object.freeze(requestList)) + }) + + return promise.promise + } + + /** + * @see https://w3c.github.io/ServiceWorker/#batch-cache-operations-algorithm + * @param {CacheBatchOperation[]} operations + * @returns {requestResponseList} + */ + #batchCacheOperations (operations) { + // 1. + const cache = this.#relevantRequestResponseList + + // 2. + const backupCache = [...cache] + + // 3. + const addedItems = [] + + // 4.1 + const resultList = [] + + try { + // 4.2 + for (const operation of operations) { + // 4.2.1 + if (operation.type !== 'delete' && operation.type !== 'put') { + throw webidl.errors.exception({ + header: 'Cache.#batchCacheOperations', + message: 'operation type does not match "delete" or "put"' + }) + } + + // 4.2.2 + if (operation.type === 'delete' && operation.response != null) { + throw webidl.errors.exception({ + header: 'Cache.#batchCacheOperations', + message: 'delete operation should not have an associated response' + }) + } + + // 4.2.3 + if (this.#queryCache(operation.request, operation.options, addedItems).length) { + throw new DOMException('???', 'InvalidStateError') + } + + // 4.2.4 + let requestResponses + + // 4.2.5 + if (operation.type === 'delete') { + // 4.2.5.1 + requestResponses = this.#queryCache(operation.request, operation.options) + + // TODO: the spec is wrong, this is needed to pass WPTs + if (requestResponses.length === 0) { + return [] + } + + // 4.2.5.2 + for (const requestResponse of requestResponses) { + const idx = cache.indexOf(requestResponse) + assert(idx !== -1) + + // 4.2.5.2.1 + cache.splice(idx, 1) + } + } else if (operation.type === 'put') { // 4.2.6 + // 4.2.6.1 + if (operation.response == null) { + throw webidl.errors.exception({ + header: 'Cache.#batchCacheOperations', + message: 'put operation should have an associated response' + }) + } + + // 4.2.6.2 + const r = operation.request + + // 4.2.6.3 + if (!urlIsHttpHttpsScheme(r.url)) { + throw webidl.errors.exception({ + header: 'Cache.#batchCacheOperations', + message: 'expected http or https scheme' + }) + } + + // 4.2.6.4 + if (r.method !== 'GET') { + throw webidl.errors.exception({ + header: 'Cache.#batchCacheOperations', + message: 'not get method' + }) + } + + // 4.2.6.5 + if (operation.options != null) { + throw webidl.errors.exception({ + header: 'Cache.#batchCacheOperations', + message: 'options must not be defined' + }) + } + + // 4.2.6.6 + requestResponses = this.#queryCache(operation.request) + + // 4.2.6.7 + for (const requestResponse of requestResponses) { + const idx = cache.indexOf(requestResponse) + assert(idx !== -1) + + // 4.2.6.7.1 + cache.splice(idx, 1) + } + + // 4.2.6.8 + cache.push([operation.request, operation.response]) + + // 4.2.6.10 + addedItems.push([operation.request, operation.response]) + } + + // 4.2.7 + resultList.push([operation.request, operation.response]) + } + + // 4.3 + return resultList + } catch (e) { // 5. + // 5.1 + this.#relevantRequestResponseList.length = 0 + + // 5.2 + this.#relevantRequestResponseList = backupCache + + // 5.3 + throw e + } + } + + /** + * @see https://w3c.github.io/ServiceWorker/#query-cache + * @param {any} requestQuery + * @param {import('../../types/cache').CacheQueryOptions} options + * @param {requestResponseList} targetStorage + * @returns {requestResponseList} + */ + #queryCache (requestQuery, options, targetStorage) { + /** @type {requestResponseList} */ + const resultList = [] + + const storage = targetStorage ?? this.#relevantRequestResponseList + + for (const requestResponse of storage) { + const [cachedRequest, cachedResponse] = requestResponse + if (this.#requestMatchesCachedItem(requestQuery, cachedRequest, cachedResponse, options)) { + resultList.push(requestResponse) + } + } + + return resultList + } + + /** + * @see https://w3c.github.io/ServiceWorker/#request-matches-cached-item-algorithm + * @param {any} requestQuery + * @param {any} request + * @param {any | null} response + * @param {import('../../types/cache').CacheQueryOptions | undefined} options + * @returns {boolean} + */ + #requestMatchesCachedItem (requestQuery, request, response = null, options) { + // if (options?.ignoreMethod === false && request.method === 'GET') { + // return false + // } + + const queryURL = new URL(requestQuery.url) + + const cachedURL = new URL(request.url) + + if (options?.ignoreSearch) { + cachedURL.search = '' + + queryURL.search = '' + } + + if (!urlEquals(queryURL, cachedURL, true)) { + return false + } + + if ( + response == null || + options?.ignoreVary || + !response.headersList.contains('vary') + ) { + return true + } + + const fieldValues = getFieldValues(response.headersList.get('vary')) + + for (const fieldValue of fieldValues) { + if (fieldValue === '*') { + return false + } + + const requestValue = request.headersList.get(fieldValue) + const queryValue = requestQuery.headersList.get(fieldValue) + + // If one has the header and the other doesn't, or one has + // a different value than the other, return false + if (requestValue !== queryValue) { + return false + } + } + + return true + } +} + +Object.defineProperties(Cache.prototype, { + [Symbol.toStringTag]: { + value: 'Cache', + configurable: true + }, + match: kEnumerableProperty, + matchAll: kEnumerableProperty, + add: kEnumerableProperty, + addAll: kEnumerableProperty, + put: kEnumerableProperty, + delete: kEnumerableProperty, + keys: kEnumerableProperty +}) + +const cacheQueryOptionConverters = [ + { + key: 'ignoreSearch', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'ignoreMethod', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'ignoreVary', + converter: webidl.converters.boolean, + defaultValue: false + } +] + +webidl.converters.CacheQueryOptions = webidl.dictionaryConverter(cacheQueryOptionConverters) + +webidl.converters.MultiCacheQueryOptions = webidl.dictionaryConverter([ + ...cacheQueryOptionConverters, + { + key: 'cacheName', + converter: webidl.converters.DOMString + } +]) + +webidl.converters.Response = webidl.interfaceConverter(Response) + +webidl.converters['sequence'] = webidl.sequenceConverter( + webidl.converters.RequestInfo +) + +module.exports = { + Cache +} + + +/***/ }), + +/***/ 7907: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { kConstruct } = __nccwpck_require__(9174) +const { Cache } = __nccwpck_require__(6101) +const { webidl } = __nccwpck_require__(1744) +const { kEnumerableProperty } = __nccwpck_require__(3983) + +class CacheStorage { + /** + * @see https://w3c.github.io/ServiceWorker/#dfn-relevant-name-to-cache-map + * @type {Map} + */ + async has (cacheName) { + webidl.brandCheck(this, CacheStorage) + webidl.argumentLengthCheck(arguments, 1, { header: 'CacheStorage.has' }) + + cacheName = webidl.converters.DOMString(cacheName) + + // 2.1.1 + // 2.2 + return this.#caches.has(cacheName) + } + + /** + * @see https://w3c.github.io/ServiceWorker/#dom-cachestorage-open + * @param {string} cacheName + * @returns {Promise} + */ + async open (cacheName) { + webidl.brandCheck(this, CacheStorage) + webidl.argumentLengthCheck(arguments, 1, { header: 'CacheStorage.open' }) + + cacheName = webidl.converters.DOMString(cacheName) + + // 2.1 + if (this.#caches.has(cacheName)) { + // await caches.open('v1') !== await caches.open('v1') + + // 2.1.1 + const cache = this.#caches.get(cacheName) + + // 2.1.1.1 + return new Cache(kConstruct, cache) + } + + // 2.2 + const cache = [] + + // 2.3 + this.#caches.set(cacheName, cache) + + // 2.4 + return new Cache(kConstruct, cache) + } + + /** + * @see https://w3c.github.io/ServiceWorker/#cache-storage-delete + * @param {string} cacheName + * @returns {Promise} + */ + async delete (cacheName) { + webidl.brandCheck(this, CacheStorage) + webidl.argumentLengthCheck(arguments, 1, { header: 'CacheStorage.delete' }) + + cacheName = webidl.converters.DOMString(cacheName) + + return this.#caches.delete(cacheName) + } + + /** + * @see https://w3c.github.io/ServiceWorker/#cache-storage-keys + * @returns {string[]} + */ + async keys () { + webidl.brandCheck(this, CacheStorage) + + // 2.1 + const keys = this.#caches.keys() + + // 2.2 + return [...keys] + } +} + +Object.defineProperties(CacheStorage.prototype, { + [Symbol.toStringTag]: { + value: 'CacheStorage', + configurable: true + }, + match: kEnumerableProperty, + has: kEnumerableProperty, + open: kEnumerableProperty, + delete: kEnumerableProperty, + keys: kEnumerableProperty +}) + +module.exports = { + CacheStorage +} + + +/***/ }), + +/***/ 9174: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +module.exports = { + kConstruct: (__nccwpck_require__(2785).kConstruct) +} + + +/***/ }), + +/***/ 2396: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const assert = __nccwpck_require__(9491) +const { URLSerializer } = __nccwpck_require__(685) +const { isValidHeaderName } = __nccwpck_require__(2538) + +/** + * @see https://url.spec.whatwg.org/#concept-url-equals + * @param {URL} A + * @param {URL} B + * @param {boolean | undefined} excludeFragment + * @returns {boolean} + */ +function urlEquals (A, B, excludeFragment = false) { + const serializedA = URLSerializer(A, excludeFragment) + + const serializedB = URLSerializer(B, excludeFragment) + + return serializedA === serializedB +} + +/** + * @see https://github.com/chromium/chromium/blob/694d20d134cb553d8d89e5500b9148012b1ba299/content/browser/cache_storage/cache_storage_cache.cc#L260-L262 + * @param {string} header + */ +function fieldValues (header) { + assert(header !== null) + + const values = [] + + for (let value of header.split(',')) { + value = value.trim() + + if (!value.length) { + continue + } else if (!isValidHeaderName(value)) { + continue + } + + values.push(value) + } + + return values +} + +module.exports = { + urlEquals, + fieldValues +} + + +/***/ }), + +/***/ 3598: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; +// @ts-check + + + +/* global WebAssembly */ + +const assert = __nccwpck_require__(9491) +const net = __nccwpck_require__(1808) +const http = __nccwpck_require__(3685) +const { pipeline } = __nccwpck_require__(2781) +const util = __nccwpck_require__(3983) +const timers = __nccwpck_require__(9459) +const Request = __nccwpck_require__(2905) +const DispatcherBase = __nccwpck_require__(4839) +const { + RequestContentLengthMismatchError, + ResponseContentLengthMismatchError, + InvalidArgumentError, + RequestAbortedError, + HeadersTimeoutError, + HeadersOverflowError, + SocketError, + InformationalError, + BodyTimeoutError, + HTTPParserError, + ResponseExceededMaxSizeError, + ClientDestroyedError +} = __nccwpck_require__(8045) +const buildConnector = __nccwpck_require__(2067) +const { + kUrl, + kReset, + kServerName, + kClient, + kBusy, + kParser, + kConnect, + kBlocking, + kResuming, + kRunning, + kPending, + kSize, + kWriting, + kQueue, + kConnected, + kConnecting, + kNeedDrain, + kNoRef, + kKeepAliveDefaultTimeout, + kHostHeader, + kPendingIdx, + kRunningIdx, + kError, + kPipelining, + kSocket, + kKeepAliveTimeoutValue, + kMaxHeadersSize, + kKeepAliveMaxTimeout, + kKeepAliveTimeoutThreshold, + kHeadersTimeout, + kBodyTimeout, + kStrictContentLength, + kConnector, + kMaxRedirections, + kMaxRequests, + kCounter, + kClose, + kDestroy, + kDispatch, + kInterceptors, + kLocalAddress, + kMaxResponseSize, + kHTTPConnVersion, + // HTTP2 + kHost, + kHTTP2Session, + kHTTP2SessionState, + kHTTP2BuildRequest, + kHTTP2CopyHeaders, + kHTTP1BuildRequest +} = __nccwpck_require__(2785) + +/** @type {import('http2')} */ +let http2 +try { + http2 = __nccwpck_require__(5158) +} catch { + // @ts-ignore + http2 = { constants: {} } +} + +const { + constants: { + HTTP2_HEADER_AUTHORITY, + HTTP2_HEADER_METHOD, + HTTP2_HEADER_PATH, + HTTP2_HEADER_SCHEME, + HTTP2_HEADER_CONTENT_LENGTH, + HTTP2_HEADER_EXPECT, + HTTP2_HEADER_STATUS + } +} = http2 + +// Experimental +let h2ExperimentalWarned = false + +const FastBuffer = Buffer[Symbol.species] + +const kClosedResolve = Symbol('kClosedResolve') + +const channels = {} + +try { + const diagnosticsChannel = __nccwpck_require__(7643) + channels.sendHeaders = diagnosticsChannel.channel('undici:client:sendHeaders') + channels.beforeConnect = diagnosticsChannel.channel('undici:client:beforeConnect') + channels.connectError = diagnosticsChannel.channel('undici:client:connectError') + channels.connected = diagnosticsChannel.channel('undici:client:connected') +} catch { + channels.sendHeaders = { hasSubscribers: false } + channels.beforeConnect = { hasSubscribers: false } + channels.connectError = { hasSubscribers: false } + channels.connected = { hasSubscribers: false } +} + +/** + * @type {import('../types/client').default} + */ +class Client extends DispatcherBase { + /** + * + * @param {string|URL} url + * @param {import('../types/client').Client.Options} options + */ + constructor (url, { + interceptors, + maxHeaderSize, + headersTimeout, + socketTimeout, + requestTimeout, + connectTimeout, + bodyTimeout, + idleTimeout, + keepAlive, + keepAliveTimeout, + maxKeepAliveTimeout, + keepAliveMaxTimeout, + keepAliveTimeoutThreshold, + socketPath, + pipelining, + tls, + strictContentLength, + maxCachedSessions, + maxRedirections, + connect, + maxRequestsPerClient, + localAddress, + maxResponseSize, + autoSelectFamily, + autoSelectFamilyAttemptTimeout, + // h2 + allowH2, + maxConcurrentStreams + } = {}) { + super() + + if (keepAlive !== undefined) { + throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead') + } + + if (socketTimeout !== undefined) { + throw new InvalidArgumentError('unsupported socketTimeout, use headersTimeout & bodyTimeout instead') + } + + if (requestTimeout !== undefined) { + throw new InvalidArgumentError('unsupported requestTimeout, use headersTimeout & bodyTimeout instead') + } + + if (idleTimeout !== undefined) { + throw new InvalidArgumentError('unsupported idleTimeout, use keepAliveTimeout instead') + } + + if (maxKeepAliveTimeout !== undefined) { + throw new InvalidArgumentError('unsupported maxKeepAliveTimeout, use keepAliveMaxTimeout instead') + } + + if (maxHeaderSize != null && !Number.isFinite(maxHeaderSize)) { + throw new InvalidArgumentError('invalid maxHeaderSize') + } + + if (socketPath != null && typeof socketPath !== 'string') { + throw new InvalidArgumentError('invalid socketPath') + } + + if (connectTimeout != null && (!Number.isFinite(connectTimeout) || connectTimeout < 0)) { + throw new InvalidArgumentError('invalid connectTimeout') + } + + if (keepAliveTimeout != null && (!Number.isFinite(keepAliveTimeout) || keepAliveTimeout <= 0)) { + throw new InvalidArgumentError('invalid keepAliveTimeout') + } + + if (keepAliveMaxTimeout != null && (!Number.isFinite(keepAliveMaxTimeout) || keepAliveMaxTimeout <= 0)) { + throw new InvalidArgumentError('invalid keepAliveMaxTimeout') + } + + if (keepAliveTimeoutThreshold != null && !Number.isFinite(keepAliveTimeoutThreshold)) { + throw new InvalidArgumentError('invalid keepAliveTimeoutThreshold') + } + + if (headersTimeout != null && (!Number.isInteger(headersTimeout) || headersTimeout < 0)) { + throw new InvalidArgumentError('headersTimeout must be a positive integer or zero') + } + + if (bodyTimeout != null && (!Number.isInteger(bodyTimeout) || bodyTimeout < 0)) { + throw new InvalidArgumentError('bodyTimeout must be a positive integer or zero') + } + + if (connect != null && typeof connect !== 'function' && typeof connect !== 'object') { + throw new InvalidArgumentError('connect must be a function or an object') + } + + if (maxRedirections != null && (!Number.isInteger(maxRedirections) || maxRedirections < 0)) { + throw new InvalidArgumentError('maxRedirections must be a positive number') + } + + if (maxRequestsPerClient != null && (!Number.isInteger(maxRequestsPerClient) || maxRequestsPerClient < 0)) { + throw new InvalidArgumentError('maxRequestsPerClient must be a positive number') + } + + if (localAddress != null && (typeof localAddress !== 'string' || net.isIP(localAddress) === 0)) { + throw new InvalidArgumentError('localAddress must be valid string IP address') + } + + if (maxResponseSize != null && (!Number.isInteger(maxResponseSize) || maxResponseSize < -1)) { + throw new InvalidArgumentError('maxResponseSize must be a positive number') + } + + if ( + autoSelectFamilyAttemptTimeout != null && + (!Number.isInteger(autoSelectFamilyAttemptTimeout) || autoSelectFamilyAttemptTimeout < -1) + ) { + throw new InvalidArgumentError('autoSelectFamilyAttemptTimeout must be a positive number') + } + + // h2 + if (allowH2 != null && typeof allowH2 !== 'boolean') { + throw new InvalidArgumentError('allowH2 must be a valid boolean value') + } + + if (maxConcurrentStreams != null && (typeof maxConcurrentStreams !== 'number' || maxConcurrentStreams < 1)) { + throw new InvalidArgumentError('maxConcurrentStreams must be a possitive integer, greater than 0') + } + + if (typeof connect !== 'function') { + connect = buildConnector({ + ...tls, + maxCachedSessions, + allowH2, + socketPath, + timeout: connectTimeout, + ...(util.nodeHasAutoSelectFamily && autoSelectFamily ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined), + ...connect + }) + } + + this[kInterceptors] = interceptors && interceptors.Client && Array.isArray(interceptors.Client) + ? interceptors.Client + : [createRedirectInterceptor({ maxRedirections })] + this[kUrl] = util.parseOrigin(url) + this[kConnector] = connect + this[kSocket] = null + this[kPipelining] = pipelining != null ? pipelining : 1 + this[kMaxHeadersSize] = maxHeaderSize || http.maxHeaderSize + this[kKeepAliveDefaultTimeout] = keepAliveTimeout == null ? 4e3 : keepAliveTimeout + this[kKeepAliveMaxTimeout] = keepAliveMaxTimeout == null ? 600e3 : keepAliveMaxTimeout + this[kKeepAliveTimeoutThreshold] = keepAliveTimeoutThreshold == null ? 1e3 : keepAliveTimeoutThreshold + this[kKeepAliveTimeoutValue] = this[kKeepAliveDefaultTimeout] + this[kServerName] = null + this[kLocalAddress] = localAddress != null ? localAddress : null + this[kResuming] = 0 // 0, idle, 1, scheduled, 2 resuming + this[kNeedDrain] = 0 // 0, idle, 1, scheduled, 2 resuming + this[kHostHeader] = `host: ${this[kUrl].hostname}${this[kUrl].port ? `:${this[kUrl].port}` : ''}\r\n` + this[kBodyTimeout] = bodyTimeout != null ? bodyTimeout : 300e3 + this[kHeadersTimeout] = headersTimeout != null ? headersTimeout : 300e3 + this[kStrictContentLength] = strictContentLength == null ? true : strictContentLength + this[kMaxRedirections] = maxRedirections + this[kMaxRequests] = maxRequestsPerClient + this[kClosedResolve] = null + this[kMaxResponseSize] = maxResponseSize > -1 ? maxResponseSize : -1 + this[kHTTPConnVersion] = 'h1' + + // HTTP/2 + this[kHTTP2Session] = null + this[kHTTP2SessionState] = !allowH2 + ? null + : { + // streams: null, // Fixed queue of streams - For future support of `push` + openStreams: 0, // Keep track of them to decide wether or not unref the session + maxConcurrentStreams: maxConcurrentStreams != null ? maxConcurrentStreams : 100 // Max peerConcurrentStreams for a Node h2 server + } + this[kHost] = `${this[kUrl].hostname}${this[kUrl].port ? `:${this[kUrl].port}` : ''}` + + // kQueue is built up of 3 sections separated by + // the kRunningIdx and kPendingIdx indices. + // | complete | running | pending | + // ^ kRunningIdx ^ kPendingIdx ^ kQueue.length + // kRunningIdx points to the first running element. + // kPendingIdx points to the first pending element. + // This implements a fast queue with an amortized + // time of O(1). + + this[kQueue] = [] + this[kRunningIdx] = 0 + this[kPendingIdx] = 0 + } + + get pipelining () { + return this[kPipelining] + } + + set pipelining (value) { + this[kPipelining] = value + resume(this, true) + } + + get [kPending] () { + return this[kQueue].length - this[kPendingIdx] + } + + get [kRunning] () { + return this[kPendingIdx] - this[kRunningIdx] + } + + get [kSize] () { + return this[kQueue].length - this[kRunningIdx] + } + + get [kConnected] () { + return !!this[kSocket] && !this[kConnecting] && !this[kSocket].destroyed + } + + get [kBusy] () { + const socket = this[kSocket] + return ( + (socket && (socket[kReset] || socket[kWriting] || socket[kBlocking])) || + (this[kSize] >= (this[kPipelining] || 1)) || + this[kPending] > 0 + ) + } + + /* istanbul ignore: only used for test */ + [kConnect] (cb) { + connect(this) + this.once('connect', cb) + } + + [kDispatch] (opts, handler) { + const origin = opts.origin || this[kUrl].origin + + const request = this[kHTTPConnVersion] === 'h2' + ? Request[kHTTP2BuildRequest](origin, opts, handler) + : Request[kHTTP1BuildRequest](origin, opts, handler) + + this[kQueue].push(request) + if (this[kResuming]) { + // Do nothing. + } else if (util.bodyLength(request.body) == null && util.isIterable(request.body)) { + // Wait a tick in case stream/iterator is ended in the same tick. + this[kResuming] = 1 + process.nextTick(resume, this) + } else { + resume(this, true) + } + + if (this[kResuming] && this[kNeedDrain] !== 2 && this[kBusy]) { + this[kNeedDrain] = 2 + } + + return this[kNeedDrain] < 2 + } + + async [kClose] () { + // TODO: for H2 we need to gracefully flush the remaining enqueued + // request and close each stream. + return new Promise((resolve) => { + if (!this[kSize]) { + resolve(null) + } else { + this[kClosedResolve] = resolve + } + }) + } + + async [kDestroy] (err) { + return new Promise((resolve) => { + const requests = this[kQueue].splice(this[kPendingIdx]) + for (let i = 0; i < requests.length; i++) { + const request = requests[i] + errorRequest(this, request, err) + } + + const callback = () => { + if (this[kClosedResolve]) { + // TODO (fix): Should we error here with ClientDestroyedError? + this[kClosedResolve]() + this[kClosedResolve] = null + } + resolve() + } + + if (this[kHTTP2Session] != null) { + util.destroy(this[kHTTP2Session], err) + this[kHTTP2Session] = null + this[kHTTP2SessionState] = null + } + + if (!this[kSocket]) { + queueMicrotask(callback) + } else { + util.destroy(this[kSocket].on('close', callback), err) + } + + resume(this) + }) + } +} + +function onHttp2SessionError (err) { + assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') + + this[kSocket][kError] = err + + onError(this[kClient], err) +} + +function onHttp2FrameError (type, code, id) { + const err = new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`) + + if (id === 0) { + this[kSocket][kError] = err + onError(this[kClient], err) + } +} + +function onHttp2SessionEnd () { + util.destroy(this, new SocketError('other side closed')) + util.destroy(this[kSocket], new SocketError('other side closed')) +} + +function onHTTP2GoAway (code) { + const client = this[kClient] + const err = new InformationalError(`HTTP/2: "GOAWAY" frame received with code ${code}`) + client[kSocket] = null + client[kHTTP2Session] = null + + if (client.destroyed) { + assert(this[kPending] === 0) + + // Fail entire queue. + const requests = client[kQueue].splice(client[kRunningIdx]) + for (let i = 0; i < requests.length; i++) { + const request = requests[i] + errorRequest(this, request, err) + } + } else if (client[kRunning] > 0) { + // Fail head of pipeline. + const request = client[kQueue][client[kRunningIdx]] + client[kQueue][client[kRunningIdx]++] = null + + errorRequest(client, request, err) + } + + client[kPendingIdx] = client[kRunningIdx] + + assert(client[kRunning] === 0) + + client.emit('disconnect', + client[kUrl], + [client], + err + ) + + resume(client) +} + +const constants = __nccwpck_require__(953) +const createRedirectInterceptor = __nccwpck_require__(8861) +const EMPTY_BUF = Buffer.alloc(0) + +async function lazyllhttp () { + const llhttpWasmData = process.env.JEST_WORKER_ID ? __nccwpck_require__(1145) : undefined + + let mod + try { + mod = await WebAssembly.compile(Buffer.from(__nccwpck_require__(5627), 'base64')) + } catch (e) { + /* istanbul ignore next */ + + // We could check if the error was caused by the simd option not + // being enabled, but the occurring of this other error + // * https://github.com/emscripten-core/emscripten/issues/11495 + // got me to remove that check to avoid breaking Node 12. + mod = await WebAssembly.compile(Buffer.from(llhttpWasmData || __nccwpck_require__(1145), 'base64')) + } + + return await WebAssembly.instantiate(mod, { + env: { + /* eslint-disable camelcase */ + + wasm_on_url: (p, at, len) => { + /* istanbul ignore next */ + return 0 + }, + wasm_on_status: (p, at, len) => { + assert.strictEqual(currentParser.ptr, p) + const start = at - currentBufferPtr + currentBufferRef.byteOffset + return currentParser.onStatus(new FastBuffer(currentBufferRef.buffer, start, len)) || 0 + }, + wasm_on_message_begin: (p) => { + assert.strictEqual(currentParser.ptr, p) + return currentParser.onMessageBegin() || 0 + }, + wasm_on_header_field: (p, at, len) => { + assert.strictEqual(currentParser.ptr, p) + const start = at - currentBufferPtr + currentBufferRef.byteOffset + return currentParser.onHeaderField(new FastBuffer(currentBufferRef.buffer, start, len)) || 0 + }, + wasm_on_header_value: (p, at, len) => { + assert.strictEqual(currentParser.ptr, p) + const start = at - currentBufferPtr + currentBufferRef.byteOffset + return currentParser.onHeaderValue(new FastBuffer(currentBufferRef.buffer, start, len)) || 0 + }, + wasm_on_headers_complete: (p, statusCode, upgrade, shouldKeepAlive) => { + assert.strictEqual(currentParser.ptr, p) + return currentParser.onHeadersComplete(statusCode, Boolean(upgrade), Boolean(shouldKeepAlive)) || 0 + }, + wasm_on_body: (p, at, len) => { + assert.strictEqual(currentParser.ptr, p) + const start = at - currentBufferPtr + currentBufferRef.byteOffset + return currentParser.onBody(new FastBuffer(currentBufferRef.buffer, start, len)) || 0 + }, + wasm_on_message_complete: (p) => { + assert.strictEqual(currentParser.ptr, p) + return currentParser.onMessageComplete() || 0 + } + + /* eslint-enable camelcase */ + } + }) +} + +let llhttpInstance = null +let llhttpPromise = lazyllhttp() +llhttpPromise.catch() + +let currentParser = null +let currentBufferRef = null +let currentBufferSize = 0 +let currentBufferPtr = null + +const TIMEOUT_HEADERS = 1 +const TIMEOUT_BODY = 2 +const TIMEOUT_IDLE = 3 + +class Parser { + constructor (client, socket, { exports }) { + assert(Number.isFinite(client[kMaxHeadersSize]) && client[kMaxHeadersSize] > 0) + + this.llhttp = exports + this.ptr = this.llhttp.llhttp_alloc(constants.TYPE.RESPONSE) + this.client = client + this.socket = socket + this.timeout = null + this.timeoutValue = null + this.timeoutType = null + this.statusCode = null + this.statusText = '' + this.upgrade = false + this.headers = [] + this.headersSize = 0 + this.headersMaxSize = client[kMaxHeadersSize] + this.shouldKeepAlive = false + this.paused = false + this.resume = this.resume.bind(this) + + this.bytesRead = 0 + + this.keepAlive = '' + this.contentLength = '' + this.connection = '' + this.maxResponseSize = client[kMaxResponseSize] + } + + setTimeout (value, type) { + this.timeoutType = type + if (value !== this.timeoutValue) { + timers.clearTimeout(this.timeout) + if (value) { + this.timeout = timers.setTimeout(onParserTimeout, value, this) + // istanbul ignore else: only for jest + if (this.timeout.unref) { + this.timeout.unref() + } + } else { + this.timeout = null + } + this.timeoutValue = value + } else if (this.timeout) { + // istanbul ignore else: only for jest + if (this.timeout.refresh) { + this.timeout.refresh() + } + } + } + + resume () { + if (this.socket.destroyed || !this.paused) { + return + } + + assert(this.ptr != null) + assert(currentParser == null) + + this.llhttp.llhttp_resume(this.ptr) + + assert(this.timeoutType === TIMEOUT_BODY) + if (this.timeout) { + // istanbul ignore else: only for jest + if (this.timeout.refresh) { + this.timeout.refresh() + } + } + + this.paused = false + this.execute(this.socket.read() || EMPTY_BUF) // Flush parser. + this.readMore() + } + + readMore () { + while (!this.paused && this.ptr) { + const chunk = this.socket.read() + if (chunk === null) { + break + } + this.execute(chunk) + } + } + + execute (data) { + assert(this.ptr != null) + assert(currentParser == null) + assert(!this.paused) + + const { socket, llhttp } = this + + if (data.length > currentBufferSize) { + if (currentBufferPtr) { + llhttp.free(currentBufferPtr) + } + currentBufferSize = Math.ceil(data.length / 4096) * 4096 + currentBufferPtr = llhttp.malloc(currentBufferSize) + } + + new Uint8Array(llhttp.memory.buffer, currentBufferPtr, currentBufferSize).set(data) + + // Call `execute` on the wasm parser. + // We pass the `llhttp_parser` pointer address, the pointer address of buffer view data, + // and finally the length of bytes to parse. + // The return value is an error code or `constants.ERROR.OK`. + try { + let ret + + try { + currentBufferRef = data + currentParser = this + ret = llhttp.llhttp_execute(this.ptr, currentBufferPtr, data.length) + /* eslint-disable-next-line no-useless-catch */ + } catch (err) { + /* istanbul ignore next: difficult to make a test case for */ + throw err + } finally { + currentParser = null + currentBufferRef = null + } + + const offset = llhttp.llhttp_get_error_pos(this.ptr) - currentBufferPtr + + if (ret === constants.ERROR.PAUSED_UPGRADE) { + this.onUpgrade(data.slice(offset)) + } else if (ret === constants.ERROR.PAUSED) { + this.paused = true + socket.unshift(data.slice(offset)) + } else if (ret !== constants.ERROR.OK) { + const ptr = llhttp.llhttp_get_error_reason(this.ptr) + let message = '' + /* istanbul ignore else: difficult to make a test case for */ + if (ptr) { + const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0) + message = + 'Response does not match the HTTP/1.1 protocol (' + + Buffer.from(llhttp.memory.buffer, ptr, len).toString() + + ')' + } + throw new HTTPParserError(message, constants.ERROR[ret], data.slice(offset)) + } + } catch (err) { + util.destroy(socket, err) + } + } + + destroy () { + assert(this.ptr != null) + assert(currentParser == null) + + this.llhttp.llhttp_free(this.ptr) + this.ptr = null + + timers.clearTimeout(this.timeout) + this.timeout = null + this.timeoutValue = null + this.timeoutType = null + + this.paused = false + } + + onStatus (buf) { + this.statusText = buf.toString() + } + + onMessageBegin () { + const { socket, client } = this + + /* istanbul ignore next: difficult to make a test case for */ + if (socket.destroyed) { + return -1 + } + + const request = client[kQueue][client[kRunningIdx]] + if (!request) { + return -1 + } + } + + onHeaderField (buf) { + const len = this.headers.length + + if ((len & 1) === 0) { + this.headers.push(buf) + } else { + this.headers[len - 1] = Buffer.concat([this.headers[len - 1], buf]) + } + + this.trackHeader(buf.length) + } + + onHeaderValue (buf) { + let len = this.headers.length + + if ((len & 1) === 1) { + this.headers.push(buf) + len += 1 + } else { + this.headers[len - 1] = Buffer.concat([this.headers[len - 1], buf]) + } + + const key = this.headers[len - 2] + if (key.length === 10 && key.toString().toLowerCase() === 'keep-alive') { + this.keepAlive += buf.toString() + } else if (key.length === 10 && key.toString().toLowerCase() === 'connection') { + this.connection += buf.toString() + } else if (key.length === 14 && key.toString().toLowerCase() === 'content-length') { + this.contentLength += buf.toString() + } + + this.trackHeader(buf.length) + } + + trackHeader (len) { + this.headersSize += len + if (this.headersSize >= this.headersMaxSize) { + util.destroy(this.socket, new HeadersOverflowError()) + } + } + + onUpgrade (head) { + const { upgrade, client, socket, headers, statusCode } = this + + assert(upgrade) + + const request = client[kQueue][client[kRunningIdx]] + assert(request) + + assert(!socket.destroyed) + assert(socket === client[kSocket]) + assert(!this.paused) + assert(request.upgrade || request.method === 'CONNECT') + + this.statusCode = null + this.statusText = '' + this.shouldKeepAlive = null + + assert(this.headers.length % 2 === 0) + this.headers = [] + this.headersSize = 0 + + socket.unshift(head) + + socket[kParser].destroy() + socket[kParser] = null + + socket[kClient] = null + socket[kError] = null + socket + .removeListener('error', onSocketError) + .removeListener('readable', onSocketReadable) + .removeListener('end', onSocketEnd) + .removeListener('close', onSocketClose) + + client[kSocket] = null + client[kQueue][client[kRunningIdx]++] = null + client.emit('disconnect', client[kUrl], [client], new InformationalError('upgrade')) + + try { + request.onUpgrade(statusCode, headers, socket) + } catch (err) { + util.destroy(socket, err) + } + + resume(client) + } + + onHeadersComplete (statusCode, upgrade, shouldKeepAlive) { + const { client, socket, headers, statusText } = this + + /* istanbul ignore next: difficult to make a test case for */ + if (socket.destroyed) { + return -1 + } + + const request = client[kQueue][client[kRunningIdx]] + + /* istanbul ignore next: difficult to make a test case for */ + if (!request) { + return -1 + } + + assert(!this.upgrade) + assert(this.statusCode < 200) + + if (statusCode === 100) { + util.destroy(socket, new SocketError('bad response', util.getSocketInfo(socket))) + return -1 + } + + /* this can only happen if server is misbehaving */ + if (upgrade && !request.upgrade) { + util.destroy(socket, new SocketError('bad upgrade', util.getSocketInfo(socket))) + return -1 + } + + assert.strictEqual(this.timeoutType, TIMEOUT_HEADERS) + + this.statusCode = statusCode + this.shouldKeepAlive = ( + shouldKeepAlive || + // Override llhttp value which does not allow keepAlive for HEAD. + (request.method === 'HEAD' && !socket[kReset] && this.connection.toLowerCase() === 'keep-alive') + ) + + if (this.statusCode >= 200) { + const bodyTimeout = request.bodyTimeout != null + ? request.bodyTimeout + : client[kBodyTimeout] + this.setTimeout(bodyTimeout, TIMEOUT_BODY) + } else if (this.timeout) { + // istanbul ignore else: only for jest + if (this.timeout.refresh) { + this.timeout.refresh() + } + } + + if (request.method === 'CONNECT') { + assert(client[kRunning] === 1) + this.upgrade = true + return 2 + } + + if (upgrade) { + assert(client[kRunning] === 1) + this.upgrade = true + return 2 + } + + assert(this.headers.length % 2 === 0) + this.headers = [] + this.headersSize = 0 + + if (this.shouldKeepAlive && client[kPipelining]) { + const keepAliveTimeout = this.keepAlive ? util.parseKeepAliveTimeout(this.keepAlive) : null + + if (keepAliveTimeout != null) { + const timeout = Math.min( + keepAliveTimeout - client[kKeepAliveTimeoutThreshold], + client[kKeepAliveMaxTimeout] + ) + if (timeout <= 0) { + socket[kReset] = true + } else { + client[kKeepAliveTimeoutValue] = timeout + } + } else { + client[kKeepAliveTimeoutValue] = client[kKeepAliveDefaultTimeout] + } + } else { + // Stop more requests from being dispatched. + socket[kReset] = true + } + + const pause = request.onHeaders(statusCode, headers, this.resume, statusText) === false + + if (request.aborted) { + return -1 + } + + if (request.method === 'HEAD') { + return 1 + } + + if (statusCode < 200) { + return 1 + } + + if (socket[kBlocking]) { + socket[kBlocking] = false + resume(client) + } + + return pause ? constants.ERROR.PAUSED : 0 + } + + onBody (buf) { + const { client, socket, statusCode, maxResponseSize } = this + + if (socket.destroyed) { + return -1 + } + + const request = client[kQueue][client[kRunningIdx]] + assert(request) + + assert.strictEqual(this.timeoutType, TIMEOUT_BODY) + if (this.timeout) { + // istanbul ignore else: only for jest + if (this.timeout.refresh) { + this.timeout.refresh() + } + } + + assert(statusCode >= 200) + + if (maxResponseSize > -1 && this.bytesRead + buf.length > maxResponseSize) { + util.destroy(socket, new ResponseExceededMaxSizeError()) + return -1 + } + + this.bytesRead += buf.length + + if (request.onData(buf) === false) { + return constants.ERROR.PAUSED + } + } + + onMessageComplete () { + const { client, socket, statusCode, upgrade, headers, contentLength, bytesRead, shouldKeepAlive } = this + + if (socket.destroyed && (!statusCode || shouldKeepAlive)) { + return -1 + } + + if (upgrade) { + return + } + + const request = client[kQueue][client[kRunningIdx]] + assert(request) + + assert(statusCode >= 100) + + this.statusCode = null + this.statusText = '' + this.bytesRead = 0 + this.contentLength = '' + this.keepAlive = '' + this.connection = '' + + assert(this.headers.length % 2 === 0) + this.headers = [] + this.headersSize = 0 + + if (statusCode < 200) { + return + } + + /* istanbul ignore next: should be handled by llhttp? */ + if (request.method !== 'HEAD' && contentLength && bytesRead !== parseInt(contentLength, 10)) { + util.destroy(socket, new ResponseContentLengthMismatchError()) + return -1 + } + + request.onComplete(headers) + + client[kQueue][client[kRunningIdx]++] = null + + if (socket[kWriting]) { + assert.strictEqual(client[kRunning], 0) + // Response completed before request. + util.destroy(socket, new InformationalError('reset')) + return constants.ERROR.PAUSED + } else if (!shouldKeepAlive) { + util.destroy(socket, new InformationalError('reset')) + return constants.ERROR.PAUSED + } else if (socket[kReset] && client[kRunning] === 0) { + // Destroy socket once all requests have completed. + // The request at the tail of the pipeline is the one + // that requested reset and no further requests should + // have been queued since then. + util.destroy(socket, new InformationalError('reset')) + return constants.ERROR.PAUSED + } else if (client[kPipelining] === 1) { + // We must wait a full event loop cycle to reuse this socket to make sure + // that non-spec compliant servers are not closing the connection even if they + // said they won't. + setImmediate(resume, client) + } else { + resume(client) + } + } +} + +function onParserTimeout (parser) { + const { socket, timeoutType, client } = parser + + /* istanbul ignore else */ + if (timeoutType === TIMEOUT_HEADERS) { + if (!socket[kWriting] || socket.writableNeedDrain || client[kRunning] > 1) { + assert(!parser.paused, 'cannot be paused while waiting for headers') + util.destroy(socket, new HeadersTimeoutError()) + } + } else if (timeoutType === TIMEOUT_BODY) { + if (!parser.paused) { + util.destroy(socket, new BodyTimeoutError()) + } + } else if (timeoutType === TIMEOUT_IDLE) { + assert(client[kRunning] === 0 && client[kKeepAliveTimeoutValue]) + util.destroy(socket, new InformationalError('socket idle timeout')) + } +} + +function onSocketReadable () { + const { [kParser]: parser } = this + if (parser) { + parser.readMore() + } +} + +function onSocketError (err) { + const { [kClient]: client, [kParser]: parser } = this + + assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') + + if (client[kHTTPConnVersion] !== 'h2') { + // On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded + // to the user. + if (err.code === 'ECONNRESET' && parser.statusCode && !parser.shouldKeepAlive) { + // We treat all incoming data so for as a valid response. + parser.onMessageComplete() + return + } + } + + this[kError] = err + + onError(this[kClient], err) +} + +function onError (client, err) { + if ( + client[kRunning] === 0 && + err.code !== 'UND_ERR_INFO' && + err.code !== 'UND_ERR_SOCKET' + ) { + // Error is not caused by running request and not a recoverable + // socket error. + + assert(client[kPendingIdx] === client[kRunningIdx]) + + const requests = client[kQueue].splice(client[kRunningIdx]) + for (let i = 0; i < requests.length; i++) { + const request = requests[i] + errorRequest(client, request, err) + } + assert(client[kSize] === 0) + } +} + +function onSocketEnd () { + const { [kParser]: parser, [kClient]: client } = this + + if (client[kHTTPConnVersion] !== 'h2') { + if (parser.statusCode && !parser.shouldKeepAlive) { + // We treat all incoming data so far as a valid response. + parser.onMessageComplete() + return + } + } + + util.destroy(this, new SocketError('other side closed', util.getSocketInfo(this))) +} + +function onSocketClose () { + const { [kClient]: client, [kParser]: parser } = this + + if (client[kHTTPConnVersion] === 'h1' && parser) { + if (!this[kError] && parser.statusCode && !parser.shouldKeepAlive) { + // We treat all incoming data so far as a valid response. + parser.onMessageComplete() + } + + this[kParser].destroy() + this[kParser] = null + } + + const err = this[kError] || new SocketError('closed', util.getSocketInfo(this)) + + client[kSocket] = null + + if (client.destroyed) { + assert(client[kPending] === 0) + + // Fail entire queue. + const requests = client[kQueue].splice(client[kRunningIdx]) + for (let i = 0; i < requests.length; i++) { + const request = requests[i] + errorRequest(client, request, err) + } + } else if (client[kRunning] > 0 && err.code !== 'UND_ERR_INFO') { + // Fail head of pipeline. + const request = client[kQueue][client[kRunningIdx]] + client[kQueue][client[kRunningIdx]++] = null + + errorRequest(client, request, err) + } + + client[kPendingIdx] = client[kRunningIdx] + + assert(client[kRunning] === 0) + + client.emit('disconnect', client[kUrl], [client], err) + + resume(client) +} + +async function connect (client) { + assert(!client[kConnecting]) + assert(!client[kSocket]) + + let { host, hostname, protocol, port } = client[kUrl] + + // Resolve ipv6 + if (hostname[0] === '[') { + const idx = hostname.indexOf(']') + + assert(idx !== -1) + const ip = hostname.substring(1, idx) + + assert(net.isIP(ip)) + hostname = ip + } + + client[kConnecting] = true + + if (channels.beforeConnect.hasSubscribers) { + channels.beforeConnect.publish({ + connectParams: { + host, + hostname, + protocol, + port, + servername: client[kServerName], + localAddress: client[kLocalAddress] + }, + connector: client[kConnector] + }) + } + + try { + const socket = await new Promise((resolve, reject) => { + client[kConnector]({ + host, + hostname, + protocol, + port, + servername: client[kServerName], + localAddress: client[kLocalAddress] + }, (err, socket) => { + if (err) { + reject(err) + } else { + resolve(socket) + } + }) + }) + + if (client.destroyed) { + util.destroy(socket.on('error', () => {}), new ClientDestroyedError()) + return + } + + client[kConnecting] = false + + assert(socket) + + const isH2 = socket.alpnProtocol === 'h2' + if (isH2) { + if (!h2ExperimentalWarned) { + h2ExperimentalWarned = true + process.emitWarning('H2 support is experimental, expect them to change at any time.', { + code: 'UNDICI-H2' + }) + } + + const session = http2.connect(client[kUrl], { + createConnection: () => socket, + peerMaxConcurrentStreams: client[kHTTP2SessionState].maxConcurrentStreams + }) + + client[kHTTPConnVersion] = 'h2' + session[kClient] = client + session[kSocket] = socket + session.on('error', onHttp2SessionError) + session.on('frameError', onHttp2FrameError) + session.on('end', onHttp2SessionEnd) + session.on('goaway', onHTTP2GoAway) + session.on('close', onSocketClose) + session.unref() + + client[kHTTP2Session] = session + socket[kHTTP2Session] = session + } else { + if (!llhttpInstance) { + llhttpInstance = await llhttpPromise + llhttpPromise = null + } + + socket[kNoRef] = false + socket[kWriting] = false + socket[kReset] = false + socket[kBlocking] = false + socket[kParser] = new Parser(client, socket, llhttpInstance) + } + + socket[kCounter] = 0 + socket[kMaxRequests] = client[kMaxRequests] + socket[kClient] = client + socket[kError] = null + + socket + .on('error', onSocketError) + .on('readable', onSocketReadable) + .on('end', onSocketEnd) + .on('close', onSocketClose) + + client[kSocket] = socket + + if (channels.connected.hasSubscribers) { + channels.connected.publish({ + connectParams: { + host, + hostname, + protocol, + port, + servername: client[kServerName], + localAddress: client[kLocalAddress] + }, + connector: client[kConnector], + socket + }) + } + client.emit('connect', client[kUrl], [client]) + } catch (err) { + if (client.destroyed) { + return + } + + client[kConnecting] = false + + if (channels.connectError.hasSubscribers) { + channels.connectError.publish({ + connectParams: { + host, + hostname, + protocol, + port, + servername: client[kServerName], + localAddress: client[kLocalAddress] + }, + connector: client[kConnector], + error: err + }) + } + + if (err.code === 'ERR_TLS_CERT_ALTNAME_INVALID') { + assert(client[kRunning] === 0) + while (client[kPending] > 0 && client[kQueue][client[kPendingIdx]].servername === client[kServerName]) { + const request = client[kQueue][client[kPendingIdx]++] + errorRequest(client, request, err) + } + } else { + onError(client, err) + } + + client.emit('connectionError', client[kUrl], [client], err) + } + + resume(client) +} + +function emitDrain (client) { + client[kNeedDrain] = 0 + client.emit('drain', client[kUrl], [client]) +} + +function resume (client, sync) { + if (client[kResuming] === 2) { + return + } + + client[kResuming] = 2 + + _resume(client, sync) + client[kResuming] = 0 + + if (client[kRunningIdx] > 256) { + client[kQueue].splice(0, client[kRunningIdx]) + client[kPendingIdx] -= client[kRunningIdx] + client[kRunningIdx] = 0 + } +} + +function _resume (client, sync) { + while (true) { + if (client.destroyed) { + assert(client[kPending] === 0) + return + } + + if (client[kClosedResolve] && !client[kSize]) { + client[kClosedResolve]() + client[kClosedResolve] = null + return + } + + const socket = client[kSocket] + + if (socket && !socket.destroyed && socket.alpnProtocol !== 'h2') { + if (client[kSize] === 0) { + if (!socket[kNoRef] && socket.unref) { + socket.unref() + socket[kNoRef] = true + } + } else if (socket[kNoRef] && socket.ref) { + socket.ref() + socket[kNoRef] = false + } + + if (client[kSize] === 0) { + if (socket[kParser].timeoutType !== TIMEOUT_IDLE) { + socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_IDLE) + } + } else if (client[kRunning] > 0 && socket[kParser].statusCode < 200) { + if (socket[kParser].timeoutType !== TIMEOUT_HEADERS) { + const request = client[kQueue][client[kRunningIdx]] + const headersTimeout = request.headersTimeout != null + ? request.headersTimeout + : client[kHeadersTimeout] + socket[kParser].setTimeout(headersTimeout, TIMEOUT_HEADERS) + } + } + } + + if (client[kBusy]) { + client[kNeedDrain] = 2 + } else if (client[kNeedDrain] === 2) { + if (sync) { + client[kNeedDrain] = 1 + process.nextTick(emitDrain, client) + } else { + emitDrain(client) + } + continue + } + + if (client[kPending] === 0) { + return + } + + if (client[kRunning] >= (client[kPipelining] || 1)) { + return + } + + const request = client[kQueue][client[kPendingIdx]] + + if (client[kUrl].protocol === 'https:' && client[kServerName] !== request.servername) { + if (client[kRunning] > 0) { + return + } + + client[kServerName] = request.servername + + if (socket && socket.servername !== request.servername) { + util.destroy(socket, new InformationalError('servername changed')) + return + } + } + + if (client[kConnecting]) { + return + } + + if (!socket && !client[kHTTP2Session]) { + connect(client) + return + } + + if (socket.destroyed || socket[kWriting] || socket[kReset] || socket[kBlocking]) { + return + } + + if (client[kRunning] > 0 && !request.idempotent) { + // Non-idempotent request cannot be retried. + // Ensure that no other requests are inflight and + // could cause failure. + return + } + + if (client[kRunning] > 0 && (request.upgrade || request.method === 'CONNECT')) { + // Don't dispatch an upgrade until all preceding requests have completed. + // A misbehaving server might upgrade the connection before all pipelined + // request has completed. + return + } + + if (client[kRunning] > 0 && util.bodyLength(request.body) !== 0 && + (util.isStream(request.body) || util.isAsyncIterable(request.body))) { + // Request with stream or iterator body can error while other requests + // are inflight and indirectly error those as well. + // Ensure this doesn't happen by waiting for inflight + // to complete before dispatching. + + // Request with stream or iterator body cannot be retried. + // Ensure that no other requests are inflight and + // could cause failure. + return + } + + if (!request.aborted && write(client, request)) { + client[kPendingIdx]++ + } else { + client[kQueue].splice(client[kPendingIdx], 1) + } + } +} + +// https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2 +function shouldSendContentLength (method) { + return method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' && method !== 'TRACE' && method !== 'CONNECT' +} + +function write (client, request) { + if (client[kHTTPConnVersion] === 'h2') { + writeH2(client, client[kHTTP2Session], request) + return + } + + const { body, method, path, host, upgrade, headers, blocking, reset } = request + + // https://tools.ietf.org/html/rfc7231#section-4.3.1 + // https://tools.ietf.org/html/rfc7231#section-4.3.2 + // https://tools.ietf.org/html/rfc7231#section-4.3.5 + + // Sending a payload body on a request that does not + // expect it can cause undefined behavior on some + // servers and corrupt connection state. Do not + // re-use the connection for further requests. + + const expectsPayload = ( + method === 'PUT' || + method === 'POST' || + method === 'PATCH' + ) + + if (body && typeof body.read === 'function') { + // Try to read EOF in order to get length. + body.read(0) + } + + const bodyLength = util.bodyLength(body) + + let contentLength = bodyLength + + if (contentLength === null) { + contentLength = request.contentLength + } + + if (contentLength === 0 && !expectsPayload) { + // https://tools.ietf.org/html/rfc7230#section-3.3.2 + // A user agent SHOULD NOT send a Content-Length header field when + // the request message does not contain a payload body and the method + // semantics do not anticipate such a body. + + contentLength = null + } + + // https://github.com/nodejs/undici/issues/2046 + // A user agent may send a Content-Length header with 0 value, this should be allowed. + if (shouldSendContentLength(method) && contentLength > 0 && request.contentLength !== null && request.contentLength !== contentLength) { + if (client[kStrictContentLength]) { + errorRequest(client, request, new RequestContentLengthMismatchError()) + return false + } + + process.emitWarning(new RequestContentLengthMismatchError()) + } + + const socket = client[kSocket] + + try { + request.onConnect((err) => { + if (request.aborted || request.completed) { + return + } + + errorRequest(client, request, err || new RequestAbortedError()) + + util.destroy(socket, new InformationalError('aborted')) + }) + } catch (err) { + errorRequest(client, request, err) + } + + if (request.aborted) { + return false + } + + if (method === 'HEAD') { + // https://github.com/mcollina/undici/issues/258 + // Close after a HEAD request to interop with misbehaving servers + // that may send a body in the response. + + socket[kReset] = true + } + + if (upgrade || method === 'CONNECT') { + // On CONNECT or upgrade, block pipeline from dispatching further + // requests on this connection. + + socket[kReset] = true + } + + if (reset != null) { + socket[kReset] = reset + } + + if (client[kMaxRequests] && socket[kCounter]++ >= client[kMaxRequests]) { + socket[kReset] = true + } + + if (blocking) { + socket[kBlocking] = true + } + + let header = `${method} ${path} HTTP/1.1\r\n` + + if (typeof host === 'string') { + header += `host: ${host}\r\n` + } else { + header += client[kHostHeader] + } + + if (upgrade) { + header += `connection: upgrade\r\nupgrade: ${upgrade}\r\n` + } else if (client[kPipelining] && !socket[kReset]) { + header += 'connection: keep-alive\r\n' + } else { + header += 'connection: close\r\n' + } + + if (headers) { + header += headers + } + + if (channels.sendHeaders.hasSubscribers) { + channels.sendHeaders.publish({ request, headers: header, socket }) + } + + /* istanbul ignore else: assertion */ + if (!body || bodyLength === 0) { + if (contentLength === 0) { + socket.write(`${header}content-length: 0\r\n\r\n`, 'latin1') + } else { + assert(contentLength === null, 'no body must not have content length') + socket.write(`${header}\r\n`, 'latin1') + } + request.onRequestSent() + } else if (util.isBuffer(body)) { + assert(contentLength === body.byteLength, 'buffer body must have content length') + + socket.cork() + socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1') + socket.write(body) + socket.uncork() + request.onBodySent(body) + request.onRequestSent() + if (!expectsPayload) { + socket[kReset] = true + } + } else if (util.isBlobLike(body)) { + if (typeof body.stream === 'function') { + writeIterable({ body: body.stream(), client, request, socket, contentLength, header, expectsPayload }) + } else { + writeBlob({ body, client, request, socket, contentLength, header, expectsPayload }) + } + } else if (util.isStream(body)) { + writeStream({ body, client, request, socket, contentLength, header, expectsPayload }) + } else if (util.isIterable(body)) { + writeIterable({ body, client, request, socket, contentLength, header, expectsPayload }) + } else { + assert(false) + } + + return true +} + +function writeH2 (client, session, request) { + const { body, method, path, host, upgrade, expectContinue, signal, headers: reqHeaders } = request + + let headers + if (typeof reqHeaders === 'string') headers = Request[kHTTP2CopyHeaders](reqHeaders.trim()) + else headers = reqHeaders + + if (upgrade) { + errorRequest(client, request, new Error('Upgrade not supported for H2')) + return false + } + + try { + // TODO(HTTP/2): Should we call onConnect immediately or on stream ready event? + request.onConnect((err) => { + if (request.aborted || request.completed) { + return + } + + errorRequest(client, request, err || new RequestAbortedError()) + }) + } catch (err) { + errorRequest(client, request, err) + } + + if (request.aborted) { + return false + } + + /** @type {import('node:http2').ClientHttp2Stream} */ + let stream + const h2State = client[kHTTP2SessionState] + + headers[HTTP2_HEADER_AUTHORITY] = host || client[kHost] + headers[HTTP2_HEADER_METHOD] = method + + if (method === 'CONNECT') { + session.ref() + // we are already connected, streams are pending, first request + // will create a new stream. We trigger a request to create the stream and wait until + // `ready` event is triggered + // We disabled endStream to allow the user to write to the stream + stream = session.request(headers, { endStream: false, signal }) + + if (stream.id && !stream.pending) { + request.onUpgrade(null, null, stream) + ++h2State.openStreams + } else { + stream.once('ready', () => { + request.onUpgrade(null, null, stream) + ++h2State.openStreams + }) + } + + stream.once('close', () => { + h2State.openStreams -= 1 + // TODO(HTTP/2): unref only if current streams count is 0 + if (h2State.openStreams === 0) session.unref() + }) + + return true + } + + // https://tools.ietf.org/html/rfc7540#section-8.3 + // :path and :scheme headers must be omited when sending CONNECT + + headers[HTTP2_HEADER_PATH] = path + headers[HTTP2_HEADER_SCHEME] = 'https' + + // https://tools.ietf.org/html/rfc7231#section-4.3.1 + // https://tools.ietf.org/html/rfc7231#section-4.3.2 + // https://tools.ietf.org/html/rfc7231#section-4.3.5 + + // Sending a payload body on a request that does not + // expect it can cause undefined behavior on some + // servers and corrupt connection state. Do not + // re-use the connection for further requests. + + const expectsPayload = ( + method === 'PUT' || + method === 'POST' || + method === 'PATCH' + ) + + if (body && typeof body.read === 'function') { + // Try to read EOF in order to get length. + body.read(0) + } + + let contentLength = util.bodyLength(body) + + if (contentLength == null) { + contentLength = request.contentLength + } + + if (contentLength === 0 || !expectsPayload) { + // https://tools.ietf.org/html/rfc7230#section-3.3.2 + // A user agent SHOULD NOT send a Content-Length header field when + // the request message does not contain a payload body and the method + // semantics do not anticipate such a body. + + contentLength = null + } + + // https://github.com/nodejs/undici/issues/2046 + // A user agent may send a Content-Length header with 0 value, this should be allowed. + if (shouldSendContentLength(method) && contentLength > 0 && request.contentLength != null && request.contentLength !== contentLength) { + if (client[kStrictContentLength]) { + errorRequest(client, request, new RequestContentLengthMismatchError()) + return false + } + + process.emitWarning(new RequestContentLengthMismatchError()) + } + + if (contentLength != null) { + assert(body, 'no body must not have content length') + headers[HTTP2_HEADER_CONTENT_LENGTH] = `${contentLength}` + } + + session.ref() + + const shouldEndStream = method === 'GET' || method === 'HEAD' + if (expectContinue) { + headers[HTTP2_HEADER_EXPECT] = '100-continue' + stream = session.request(headers, { endStream: shouldEndStream, signal }) + + stream.once('continue', writeBodyH2) + } else { + stream = session.request(headers, { + endStream: shouldEndStream, + signal + }) + writeBodyH2() + } + + // Increment counter as we have new several streams open + ++h2State.openStreams + + stream.once('response', headers => { + const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers + + if (request.onHeaders(Number(statusCode), realHeaders, stream.resume.bind(stream), '') === false) { + stream.pause() + } + }) + + stream.once('end', () => { + request.onComplete([]) + }) + + stream.on('data', (chunk) => { + if (request.onData(chunk) === false) { + stream.pause() + } + }) + + stream.once('close', () => { + h2State.openStreams -= 1 + // TODO(HTTP/2): unref only if current streams count is 0 + if (h2State.openStreams === 0) { + session.unref() + } + }) + + stream.once('error', function (err) { + if (client[kHTTP2Session] && !client[kHTTP2Session].destroyed && !this.closed && !this.destroyed) { + h2State.streams -= 1 + util.destroy(stream, err) + } + }) + + stream.once('frameError', (type, code) => { + const err = new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`) + errorRequest(client, request, err) + + if (client[kHTTP2Session] && !client[kHTTP2Session].destroyed && !this.closed && !this.destroyed) { + h2State.streams -= 1 + util.destroy(stream, err) + } + }) + + // stream.on('aborted', () => { + // // TODO(HTTP/2): Support aborted + // }) + + // stream.on('timeout', () => { + // // TODO(HTTP/2): Support timeout + // }) + + // stream.on('push', headers => { + // // TODO(HTTP/2): Suppor push + // }) + + // stream.on('trailers', headers => { + // // TODO(HTTP/2): Support trailers + // }) + + return true + + function writeBodyH2 () { + /* istanbul ignore else: assertion */ + if (!body) { + request.onRequestSent() + } else if (util.isBuffer(body)) { + assert(contentLength === body.byteLength, 'buffer body must have content length') + stream.cork() + stream.write(body) + stream.uncork() + stream.end() + request.onBodySent(body) + request.onRequestSent() + } else if (util.isBlobLike(body)) { + if (typeof body.stream === 'function') { + writeIterable({ + client, + request, + contentLength, + h2stream: stream, + expectsPayload, + body: body.stream(), + socket: client[kSocket], + header: '' + }) + } else { + writeBlob({ + body, + client, + request, + contentLength, + expectsPayload, + h2stream: stream, + header: '', + socket: client[kSocket] + }) + } + } else if (util.isStream(body)) { + writeStream({ + body, + client, + request, + contentLength, + expectsPayload, + socket: client[kSocket], + h2stream: stream, + header: '' + }) + } else if (util.isIterable(body)) { + writeIterable({ + body, + client, + request, + contentLength, + expectsPayload, + header: '', + h2stream: stream, + socket: client[kSocket] + }) + } else { + assert(false) + } + } +} + +function writeStream ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) { + assert(contentLength !== 0 || client[kRunning] === 0, 'stream body cannot be pipelined') + + if (client[kHTTPConnVersion] === 'h2') { + // For HTTP/2, is enough to pipe the stream + const pipe = pipeline( + body, + h2stream, + (err) => { + if (err) { + util.destroy(body, err) + util.destroy(h2stream, err) + } else { + request.onRequestSent() + } + } + ) + + pipe.on('data', onPipeData) + pipe.once('end', () => { + pipe.removeListener('data', onPipeData) + util.destroy(pipe) + }) + + function onPipeData (chunk) { + request.onBodySent(chunk) + } + + return + } + + let finished = false + + const writer = new AsyncWriter({ socket, request, contentLength, client, expectsPayload, header }) + + const onData = function (chunk) { + if (finished) { + return + } + + try { + if (!writer.write(chunk) && this.pause) { + this.pause() + } + } catch (err) { + util.destroy(this, err) + } + } + const onDrain = function () { + if (finished) { + return + } + + if (body.resume) { + body.resume() + } + } + const onAbort = function () { + if (finished) { + return + } + const err = new RequestAbortedError() + queueMicrotask(() => onFinished(err)) + } + const onFinished = function (err) { + if (finished) { + return + } + + finished = true + + assert(socket.destroyed || (socket[kWriting] && client[kRunning] <= 1)) + + socket + .off('drain', onDrain) + .off('error', onFinished) + + body + .removeListener('data', onData) + .removeListener('end', onFinished) + .removeListener('error', onFinished) + .removeListener('close', onAbort) + + if (!err) { + try { + writer.end() + } catch (er) { + err = er + } + } + + writer.destroy(err) + + if (err && (err.code !== 'UND_ERR_INFO' || err.message !== 'reset')) { + util.destroy(body, err) + } else { + util.destroy(body) + } + } + + body + .on('data', onData) + .on('end', onFinished) + .on('error', onFinished) + .on('close', onAbort) + + if (body.resume) { + body.resume() + } + + socket + .on('drain', onDrain) + .on('error', onFinished) +} + +async function writeBlob ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) { + assert(contentLength === body.size, 'blob body must have content length') + + const isH2 = client[kHTTPConnVersion] === 'h2' + try { + if (contentLength != null && contentLength !== body.size) { + throw new RequestContentLengthMismatchError() + } + + const buffer = Buffer.from(await body.arrayBuffer()) + + if (isH2) { + h2stream.cork() + h2stream.write(buffer) + h2stream.uncork() + } else { + socket.cork() + socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1') + socket.write(buffer) + socket.uncork() + } + + request.onBodySent(buffer) + request.onRequestSent() + + if (!expectsPayload) { + socket[kReset] = true + } + + resume(client) + } catch (err) { + util.destroy(isH2 ? h2stream : socket, err) + } +} + +async function writeIterable ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) { + assert(contentLength !== 0 || client[kRunning] === 0, 'iterator body cannot be pipelined') + + let callback = null + function onDrain () { + if (callback) { + const cb = callback + callback = null + cb() + } + } + + const waitForDrain = () => new Promise((resolve, reject) => { + assert(callback === null) + + if (socket[kError]) { + reject(socket[kError]) + } else { + callback = resolve + } + }) + + if (client[kHTTPConnVersion] === 'h2') { + h2stream + .on('close', onDrain) + .on('drain', onDrain) + + try { + // It's up to the user to somehow abort the async iterable. + for await (const chunk of body) { + if (socket[kError]) { + throw socket[kError] + } + + const res = h2stream.write(chunk) + request.onBodySent(chunk) + if (!res) { + await waitForDrain() + } + } + } catch (err) { + h2stream.destroy(err) + } finally { + request.onRequestSent() + h2stream.end() + h2stream + .off('close', onDrain) + .off('drain', onDrain) + } + + return + } + + socket + .on('close', onDrain) + .on('drain', onDrain) + + const writer = new AsyncWriter({ socket, request, contentLength, client, expectsPayload, header }) + try { + // It's up to the user to somehow abort the async iterable. + for await (const chunk of body) { + if (socket[kError]) { + throw socket[kError] + } + + if (!writer.write(chunk)) { + await waitForDrain() + } + } + + writer.end() + } catch (err) { + writer.destroy(err) + } finally { + socket + .off('close', onDrain) + .off('drain', onDrain) + } +} + +class AsyncWriter { + constructor ({ socket, request, contentLength, client, expectsPayload, header }) { + this.socket = socket + this.request = request + this.contentLength = contentLength + this.client = client + this.bytesWritten = 0 + this.expectsPayload = expectsPayload + this.header = header + + socket[kWriting] = true + } + + write (chunk) { + const { socket, request, contentLength, client, bytesWritten, expectsPayload, header } = this + + if (socket[kError]) { + throw socket[kError] + } + + if (socket.destroyed) { + return false + } + + const len = Buffer.byteLength(chunk) + if (!len) { + return true + } + + // We should defer writing chunks. + if (contentLength !== null && bytesWritten + len > contentLength) { + if (client[kStrictContentLength]) { + throw new RequestContentLengthMismatchError() + } + + process.emitWarning(new RequestContentLengthMismatchError()) + } + + socket.cork() + + if (bytesWritten === 0) { + if (!expectsPayload) { + socket[kReset] = true + } + + if (contentLength === null) { + socket.write(`${header}transfer-encoding: chunked\r\n`, 'latin1') + } else { + socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1') + } + } + + if (contentLength === null) { + socket.write(`\r\n${len.toString(16)}\r\n`, 'latin1') + } + + this.bytesWritten += len + + const ret = socket.write(chunk) + + socket.uncork() + + request.onBodySent(chunk) + + if (!ret) { + if (socket[kParser].timeout && socket[kParser].timeoutType === TIMEOUT_HEADERS) { + // istanbul ignore else: only for jest + if (socket[kParser].timeout.refresh) { + socket[kParser].timeout.refresh() + } + } + } + + return ret + } + + end () { + const { socket, contentLength, client, bytesWritten, expectsPayload, header, request } = this + request.onRequestSent() + + socket[kWriting] = false + + if (socket[kError]) { + throw socket[kError] + } + + if (socket.destroyed) { + return + } + + if (bytesWritten === 0) { + if (expectsPayload) { + // https://tools.ietf.org/html/rfc7230#section-3.3.2 + // A user agent SHOULD send a Content-Length in a request message when + // no Transfer-Encoding is sent and the request method defines a meaning + // for an enclosed payload body. + + socket.write(`${header}content-length: 0\r\n\r\n`, 'latin1') + } else { + socket.write(`${header}\r\n`, 'latin1') + } + } else if (contentLength === null) { + socket.write('\r\n0\r\n\r\n', 'latin1') + } + + if (contentLength !== null && bytesWritten !== contentLength) { + if (client[kStrictContentLength]) { + throw new RequestContentLengthMismatchError() + } else { + process.emitWarning(new RequestContentLengthMismatchError()) + } + } + + if (socket[kParser].timeout && socket[kParser].timeoutType === TIMEOUT_HEADERS) { + // istanbul ignore else: only for jest + if (socket[kParser].timeout.refresh) { + socket[kParser].timeout.refresh() + } + } + + resume(client) + } + + destroy (err) { + const { socket, client } = this + + socket[kWriting] = false + + if (err) { + assert(client[kRunning] <= 1, 'pipeline should only contain this request') + util.destroy(socket, err) + } + } +} + +function errorRequest (client, request, err) { + try { + request.onError(err) + assert(request.aborted) + } catch (err) { + client.emit('error', err) + } +} + +module.exports = Client + + +/***/ }), + +/***/ 6436: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +/* istanbul ignore file: only for Node 12 */ + +const { kConnected, kSize } = __nccwpck_require__(2785) + +class CompatWeakRef { + constructor (value) { + this.value = value + } + + deref () { + return this.value[kConnected] === 0 && this.value[kSize] === 0 + ? undefined + : this.value + } +} + +class CompatFinalizer { + constructor (finalizer) { + this.finalizer = finalizer + } + + register (dispatcher, key) { + if (dispatcher.on) { + dispatcher.on('disconnect', () => { + if (dispatcher[kConnected] === 0 && dispatcher[kSize] === 0) { + this.finalizer(key) + } + }) + } + } +} + +module.exports = function () { + // FIXME: remove workaround when the Node bug is fixed + // https://github.com/nodejs/node/issues/49344#issuecomment-1741776308 + if (process.env.NODE_V8_COVERAGE) { + return { + WeakRef: CompatWeakRef, + FinalizationRegistry: CompatFinalizer + } + } + return { + WeakRef: global.WeakRef || CompatWeakRef, + FinalizationRegistry: global.FinalizationRegistry || CompatFinalizer + } +} + + +/***/ }), + +/***/ 663: +/***/ ((module) => { + +"use strict"; + + +// https://wicg.github.io/cookie-store/#cookie-maximum-attribute-value-size +const maxAttributeValueSize = 1024 + +// https://wicg.github.io/cookie-store/#cookie-maximum-name-value-pair-size +const maxNameValuePairSize = 4096 + +module.exports = { + maxAttributeValueSize, + maxNameValuePairSize +} + + +/***/ }), + +/***/ 1724: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { parseSetCookie } = __nccwpck_require__(4408) +const { stringify, getHeadersList } = __nccwpck_require__(3121) +const { webidl } = __nccwpck_require__(1744) +const { Headers } = __nccwpck_require__(554) + +/** + * @typedef {Object} Cookie + * @property {string} name + * @property {string} value + * @property {Date|number|undefined} expires + * @property {number|undefined} maxAge + * @property {string|undefined} domain + * @property {string|undefined} path + * @property {boolean|undefined} secure + * @property {boolean|undefined} httpOnly + * @property {'Strict'|'Lax'|'None'} sameSite + * @property {string[]} unparsed + */ + +/** + * @param {Headers} headers + * @returns {Record} + */ +function getCookies (headers) { + webidl.argumentLengthCheck(arguments, 1, { header: 'getCookies' }) + + webidl.brandCheck(headers, Headers, { strict: false }) + + const cookie = headers.get('cookie') + const out = {} + + if (!cookie) { + return out + } + + for (const piece of cookie.split(';')) { + const [name, ...value] = piece.split('=') + + out[name.trim()] = value.join('=') + } + + return out +} + +/** + * @param {Headers} headers + * @param {string} name + * @param {{ path?: string, domain?: string }|undefined} attributes + * @returns {void} + */ +function deleteCookie (headers, name, attributes) { + webidl.argumentLengthCheck(arguments, 2, { header: 'deleteCookie' }) + + webidl.brandCheck(headers, Headers, { strict: false }) + + name = webidl.converters.DOMString(name) + attributes = webidl.converters.DeleteCookieAttributes(attributes) + + // Matches behavior of + // https://github.com/denoland/deno_std/blob/63827b16330b82489a04614027c33b7904e08be5/http/cookie.ts#L278 + setCookie(headers, { + name, + value: '', + expires: new Date(0), + ...attributes + }) +} + +/** + * @param {Headers} headers + * @returns {Cookie[]} + */ +function getSetCookies (headers) { + webidl.argumentLengthCheck(arguments, 1, { header: 'getSetCookies' }) + + webidl.brandCheck(headers, Headers, { strict: false }) + + const cookies = getHeadersList(headers).cookies + + if (!cookies) { + return [] + } + + // In older versions of undici, cookies is a list of name:value. + return cookies.map((pair) => parseSetCookie(Array.isArray(pair) ? pair[1] : pair)) +} + +/** + * @param {Headers} headers + * @param {Cookie} cookie + * @returns {void} + */ +function setCookie (headers, cookie) { + webidl.argumentLengthCheck(arguments, 2, { header: 'setCookie' }) + + webidl.brandCheck(headers, Headers, { strict: false }) + + cookie = webidl.converters.Cookie(cookie) + + const str = stringify(cookie) + + if (str) { + headers.append('Set-Cookie', stringify(cookie)) + } +} + +webidl.converters.DeleteCookieAttributes = webidl.dictionaryConverter([ + { + converter: webidl.nullableConverter(webidl.converters.DOMString), + key: 'path', + defaultValue: null + }, + { + converter: webidl.nullableConverter(webidl.converters.DOMString), + key: 'domain', + defaultValue: null + } +]) + +webidl.converters.Cookie = webidl.dictionaryConverter([ + { + converter: webidl.converters.DOMString, + key: 'name' + }, + { + converter: webidl.converters.DOMString, + key: 'value' + }, + { + converter: webidl.nullableConverter((value) => { + if (typeof value === 'number') { + return webidl.converters['unsigned long long'](value) + } + + return new Date(value) + }), + key: 'expires', + defaultValue: null + }, + { + converter: webidl.nullableConverter(webidl.converters['long long']), + key: 'maxAge', + defaultValue: null + }, + { + converter: webidl.nullableConverter(webidl.converters.DOMString), + key: 'domain', + defaultValue: null + }, + { + converter: webidl.nullableConverter(webidl.converters.DOMString), + key: 'path', + defaultValue: null + }, + { + converter: webidl.nullableConverter(webidl.converters.boolean), + key: 'secure', + defaultValue: null + }, + { + converter: webidl.nullableConverter(webidl.converters.boolean), + key: 'httpOnly', + defaultValue: null + }, + { + converter: webidl.converters.USVString, + key: 'sameSite', + allowedValues: ['Strict', 'Lax', 'None'] + }, + { + converter: webidl.sequenceConverter(webidl.converters.DOMString), + key: 'unparsed', + defaultValue: [] + } +]) + +module.exports = { + getCookies, + deleteCookie, + getSetCookies, + setCookie +} + + +/***/ }), + +/***/ 4408: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { maxNameValuePairSize, maxAttributeValueSize } = __nccwpck_require__(663) +const { isCTLExcludingHtab } = __nccwpck_require__(3121) +const { collectASequenceOfCodePointsFast } = __nccwpck_require__(685) +const assert = __nccwpck_require__(9491) + +/** + * @description Parses the field-value attributes of a set-cookie header string. + * @see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4 + * @param {string} header + * @returns if the header is invalid, null will be returned + */ +function parseSetCookie (header) { + // 1. If the set-cookie-string contains a %x00-08 / %x0A-1F / %x7F + // character (CTL characters excluding HTAB): Abort these steps and + // ignore the set-cookie-string entirely. + if (isCTLExcludingHtab(header)) { + return null + } + + let nameValuePair = '' + let unparsedAttributes = '' + let name = '' + let value = '' + + // 2. If the set-cookie-string contains a %x3B (";") character: + if (header.includes(';')) { + // 1. The name-value-pair string consists of the characters up to, + // but not including, the first %x3B (";"), and the unparsed- + // attributes consist of the remainder of the set-cookie-string + // (including the %x3B (";") in question). + const position = { position: 0 } + + nameValuePair = collectASequenceOfCodePointsFast(';', header, position) + unparsedAttributes = header.slice(position.position) + } else { + // Otherwise: + + // 1. The name-value-pair string consists of all the characters + // contained in the set-cookie-string, and the unparsed- + // attributes is the empty string. + nameValuePair = header + } + + // 3. If the name-value-pair string lacks a %x3D ("=") character, then + // the name string is empty, and the value string is the value of + // name-value-pair. + if (!nameValuePair.includes('=')) { + value = nameValuePair + } else { + // Otherwise, the name string consists of the characters up to, but + // not including, the first %x3D ("=") character, and the (possibly + // empty) value string consists of the characters after the first + // %x3D ("=") character. + const position = { position: 0 } + name = collectASequenceOfCodePointsFast( + '=', + nameValuePair, + position + ) + value = nameValuePair.slice(position.position + 1) + } + + // 4. Remove any leading or trailing WSP characters from the name + // string and the value string. + name = name.trim() + value = value.trim() + + // 5. If the sum of the lengths of the name string and the value string + // is more than 4096 octets, abort these steps and ignore the set- + // cookie-string entirely. + if (name.length + value.length > maxNameValuePairSize) { + return null + } + + // 6. The cookie-name is the name string, and the cookie-value is the + // value string. + return { + name, value, ...parseUnparsedAttributes(unparsedAttributes) + } +} + +/** + * Parses the remaining attributes of a set-cookie header + * @see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4 + * @param {string} unparsedAttributes + * @param {[Object.]={}} cookieAttributeList + */ +function parseUnparsedAttributes (unparsedAttributes, cookieAttributeList = {}) { + // 1. If the unparsed-attributes string is empty, skip the rest of + // these steps. + if (unparsedAttributes.length === 0) { + return cookieAttributeList + } + + // 2. Discard the first character of the unparsed-attributes (which + // will be a %x3B (";") character). + assert(unparsedAttributes[0] === ';') + unparsedAttributes = unparsedAttributes.slice(1) + + let cookieAv = '' + + // 3. If the remaining unparsed-attributes contains a %x3B (";") + // character: + if (unparsedAttributes.includes(';')) { + // 1. Consume the characters of the unparsed-attributes up to, but + // not including, the first %x3B (";") character. + cookieAv = collectASequenceOfCodePointsFast( + ';', + unparsedAttributes, + { position: 0 } + ) + unparsedAttributes = unparsedAttributes.slice(cookieAv.length) + } else { + // Otherwise: + + // 1. Consume the remainder of the unparsed-attributes. + cookieAv = unparsedAttributes + unparsedAttributes = '' + } + + // Let the cookie-av string be the characters consumed in this step. + + let attributeName = '' + let attributeValue = '' + + // 4. If the cookie-av string contains a %x3D ("=") character: + if (cookieAv.includes('=')) { + // 1. The (possibly empty) attribute-name string consists of the + // characters up to, but not including, the first %x3D ("=") + // character, and the (possibly empty) attribute-value string + // consists of the characters after the first %x3D ("=") + // character. + const position = { position: 0 } + + attributeName = collectASequenceOfCodePointsFast( + '=', + cookieAv, + position + ) + attributeValue = cookieAv.slice(position.position + 1) + } else { + // Otherwise: + + // 1. The attribute-name string consists of the entire cookie-av + // string, and the attribute-value string is empty. + attributeName = cookieAv + } + + // 5. Remove any leading or trailing WSP characters from the attribute- + // name string and the attribute-value string. + attributeName = attributeName.trim() + attributeValue = attributeValue.trim() + + // 6. If the attribute-value is longer than 1024 octets, ignore the + // cookie-av string and return to Step 1 of this algorithm. + if (attributeValue.length > maxAttributeValueSize) { + return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList) + } + + // 7. Process the attribute-name and attribute-value according to the + // requirements in the following subsections. (Notice that + // attributes with unrecognized attribute-names are ignored.) + const attributeNameLowercase = attributeName.toLowerCase() + + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.1 + // If the attribute-name case-insensitively matches the string + // "Expires", the user agent MUST process the cookie-av as follows. + if (attributeNameLowercase === 'expires') { + // 1. Let the expiry-time be the result of parsing the attribute-value + // as cookie-date (see Section 5.1.1). + const expiryTime = new Date(attributeValue) + + // 2. If the attribute-value failed to parse as a cookie date, ignore + // the cookie-av. + + cookieAttributeList.expires = expiryTime + } else if (attributeNameLowercase === 'max-age') { + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.2 + // If the attribute-name case-insensitively matches the string "Max- + // Age", the user agent MUST process the cookie-av as follows. + + // 1. If the first character of the attribute-value is not a DIGIT or a + // "-" character, ignore the cookie-av. + const charCode = attributeValue.charCodeAt(0) + + if ((charCode < 48 || charCode > 57) && attributeValue[0] !== '-') { + return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList) + } + + // 2. If the remainder of attribute-value contains a non-DIGIT + // character, ignore the cookie-av. + if (!/^\d+$/.test(attributeValue)) { + return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList) + } + + // 3. Let delta-seconds be the attribute-value converted to an integer. + const deltaSeconds = Number(attributeValue) + + // 4. Let cookie-age-limit be the maximum age of the cookie (which + // SHOULD be 400 days or less, see Section 4.1.2.2). + + // 5. Set delta-seconds to the smaller of its present value and cookie- + // age-limit. + // deltaSeconds = Math.min(deltaSeconds * 1000, maxExpiresMs) + + // 6. If delta-seconds is less than or equal to zero (0), let expiry- + // time be the earliest representable date and time. Otherwise, let + // the expiry-time be the current date and time plus delta-seconds + // seconds. + // const expiryTime = deltaSeconds <= 0 ? Date.now() : Date.now() + deltaSeconds + + // 7. Append an attribute to the cookie-attribute-list with an + // attribute-name of Max-Age and an attribute-value of expiry-time. + cookieAttributeList.maxAge = deltaSeconds + } else if (attributeNameLowercase === 'domain') { + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.3 + // If the attribute-name case-insensitively matches the string "Domain", + // the user agent MUST process the cookie-av as follows. + + // 1. Let cookie-domain be the attribute-value. + let cookieDomain = attributeValue + + // 2. If cookie-domain starts with %x2E ("."), let cookie-domain be + // cookie-domain without its leading %x2E ("."). + if (cookieDomain[0] === '.') { + cookieDomain = cookieDomain.slice(1) + } + + // 3. Convert the cookie-domain to lower case. + cookieDomain = cookieDomain.toLowerCase() + + // 4. Append an attribute to the cookie-attribute-list with an + // attribute-name of Domain and an attribute-value of cookie-domain. + cookieAttributeList.domain = cookieDomain + } else if (attributeNameLowercase === 'path') { + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.4 + // If the attribute-name case-insensitively matches the string "Path", + // the user agent MUST process the cookie-av as follows. + + // 1. If the attribute-value is empty or if the first character of the + // attribute-value is not %x2F ("/"): + let cookiePath = '' + if (attributeValue.length === 0 || attributeValue[0] !== '/') { + // 1. Let cookie-path be the default-path. + cookiePath = '/' + } else { + // Otherwise: + + // 1. Let cookie-path be the attribute-value. + cookiePath = attributeValue + } + + // 2. Append an attribute to the cookie-attribute-list with an + // attribute-name of Path and an attribute-value of cookie-path. + cookieAttributeList.path = cookiePath + } else if (attributeNameLowercase === 'secure') { + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.5 + // If the attribute-name case-insensitively matches the string "Secure", + // the user agent MUST append an attribute to the cookie-attribute-list + // with an attribute-name of Secure and an empty attribute-value. + + cookieAttributeList.secure = true + } else if (attributeNameLowercase === 'httponly') { + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.6 + // If the attribute-name case-insensitively matches the string + // "HttpOnly", the user agent MUST append an attribute to the cookie- + // attribute-list with an attribute-name of HttpOnly and an empty + // attribute-value. + + cookieAttributeList.httpOnly = true + } else if (attributeNameLowercase === 'samesite') { + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.7 + // If the attribute-name case-insensitively matches the string + // "SameSite", the user agent MUST process the cookie-av as follows: + + // 1. Let enforcement be "Default". + let enforcement = 'Default' + + const attributeValueLowercase = attributeValue.toLowerCase() + // 2. If cookie-av's attribute-value is a case-insensitive match for + // "None", set enforcement to "None". + if (attributeValueLowercase.includes('none')) { + enforcement = 'None' + } + + // 3. If cookie-av's attribute-value is a case-insensitive match for + // "Strict", set enforcement to "Strict". + if (attributeValueLowercase.includes('strict')) { + enforcement = 'Strict' + } + + // 4. If cookie-av's attribute-value is a case-insensitive match for + // "Lax", set enforcement to "Lax". + if (attributeValueLowercase.includes('lax')) { + enforcement = 'Lax' + } + + // 5. Append an attribute to the cookie-attribute-list with an + // attribute-name of "SameSite" and an attribute-value of + // enforcement. + cookieAttributeList.sameSite = enforcement + } else { + cookieAttributeList.unparsed ??= [] + + cookieAttributeList.unparsed.push(`${attributeName}=${attributeValue}`) + } + + // 8. Return to Step 1 of this algorithm. + return parseUnparsedAttributes(unparsedAttributes, cookieAttributeList) +} + +module.exports = { + parseSetCookie, + parseUnparsedAttributes +} + + +/***/ }), + +/***/ 3121: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const assert = __nccwpck_require__(9491) +const { kHeadersList } = __nccwpck_require__(2785) + +function isCTLExcludingHtab (value) { + if (value.length === 0) { + return false + } + + for (const char of value) { + const code = char.charCodeAt(0) + + if ( + (code >= 0x00 || code <= 0x08) || + (code >= 0x0A || code <= 0x1F) || + code === 0x7F + ) { + return false + } + } +} + +/** + CHAR = + token = 1* + separators = "(" | ")" | "<" | ">" | "@" + | "," | ";" | ":" | "\" | <"> + | "/" | "[" | "]" | "?" | "=" + | "{" | "}" | SP | HT + * @param {string} name + */ +function validateCookieName (name) { + for (const char of name) { + const code = char.charCodeAt(0) + + if ( + (code <= 0x20 || code > 0x7F) || + char === '(' || + char === ')' || + char === '>' || + char === '<' || + char === '@' || + char === ',' || + char === ';' || + char === ':' || + char === '\\' || + char === '"' || + char === '/' || + char === '[' || + char === ']' || + char === '?' || + char === '=' || + char === '{' || + char === '}' + ) { + throw new Error('Invalid cookie name') + } + } +} + +/** + cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE ) + cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E + ; US-ASCII characters excluding CTLs, + ; whitespace DQUOTE, comma, semicolon, + ; and backslash + * @param {string} value + */ +function validateCookieValue (value) { + for (const char of value) { + const code = char.charCodeAt(0) + + if ( + code < 0x21 || // exclude CTLs (0-31) + code === 0x22 || + code === 0x2C || + code === 0x3B || + code === 0x5C || + code > 0x7E // non-ascii + ) { + throw new Error('Invalid header value') + } + } +} + +/** + * path-value = + * @param {string} path + */ +function validateCookiePath (path) { + for (const char of path) { + const code = char.charCodeAt(0) + + if (code < 0x21 || char === ';') { + throw new Error('Invalid cookie path') + } + } +} + +/** + * I have no idea why these values aren't allowed to be honest, + * but Deno tests these. - Khafra + * @param {string} domain + */ +function validateCookieDomain (domain) { + if ( + domain.startsWith('-') || + domain.endsWith('.') || + domain.endsWith('-') + ) { + throw new Error('Invalid cookie domain') + } +} + +/** + * @see https://www.rfc-editor.org/rfc/rfc7231#section-7.1.1.1 + * @param {number|Date} date + IMF-fixdate = day-name "," SP date1 SP time-of-day SP GMT + ; fixed length/zone/capitalization subset of the format + ; see Section 3.3 of [RFC5322] + + day-name = %x4D.6F.6E ; "Mon", case-sensitive + / %x54.75.65 ; "Tue", case-sensitive + / %x57.65.64 ; "Wed", case-sensitive + / %x54.68.75 ; "Thu", case-sensitive + / %x46.72.69 ; "Fri", case-sensitive + / %x53.61.74 ; "Sat", case-sensitive + / %x53.75.6E ; "Sun", case-sensitive + date1 = day SP month SP year + ; e.g., 02 Jun 1982 + + day = 2DIGIT + month = %x4A.61.6E ; "Jan", case-sensitive + / %x46.65.62 ; "Feb", case-sensitive + / %x4D.61.72 ; "Mar", case-sensitive + / %x41.70.72 ; "Apr", case-sensitive + / %x4D.61.79 ; "May", case-sensitive + / %x4A.75.6E ; "Jun", case-sensitive + / %x4A.75.6C ; "Jul", case-sensitive + / %x41.75.67 ; "Aug", case-sensitive + / %x53.65.70 ; "Sep", case-sensitive + / %x4F.63.74 ; "Oct", case-sensitive + / %x4E.6F.76 ; "Nov", case-sensitive + / %x44.65.63 ; "Dec", case-sensitive + year = 4DIGIT + + GMT = %x47.4D.54 ; "GMT", case-sensitive + + time-of-day = hour ":" minute ":" second + ; 00:00:00 - 23:59:60 (leap second) + + hour = 2DIGIT + minute = 2DIGIT + second = 2DIGIT + */ +function toIMFDate (date) { + if (typeof date === 'number') { + date = new Date(date) + } + + const days = [ + 'Sun', 'Mon', 'Tue', 'Wed', + 'Thu', 'Fri', 'Sat' + ] + + const months = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' + ] + + const dayName = days[date.getUTCDay()] + const day = date.getUTCDate().toString().padStart(2, '0') + const month = months[date.getUTCMonth()] + const year = date.getUTCFullYear() + const hour = date.getUTCHours().toString().padStart(2, '0') + const minute = date.getUTCMinutes().toString().padStart(2, '0') + const second = date.getUTCSeconds().toString().padStart(2, '0') + + return `${dayName}, ${day} ${month} ${year} ${hour}:${minute}:${second} GMT` +} + +/** + max-age-av = "Max-Age=" non-zero-digit *DIGIT + ; In practice, both expires-av and max-age-av + ; are limited to dates representable by the + ; user agent. + * @param {number} maxAge + */ +function validateCookieMaxAge (maxAge) { + if (maxAge < 0) { + throw new Error('Invalid cookie max-age') + } +} + +/** + * @see https://www.rfc-editor.org/rfc/rfc6265#section-4.1.1 + * @param {import('./index').Cookie} cookie + */ +function stringify (cookie) { + if (cookie.name.length === 0) { + return null + } + + validateCookieName(cookie.name) + validateCookieValue(cookie.value) + + const out = [`${cookie.name}=${cookie.value}`] + + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-cookie-prefixes-00#section-3.1 + // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-cookie-prefixes-00#section-3.2 + if (cookie.name.startsWith('__Secure-')) { + cookie.secure = true + } + + if (cookie.name.startsWith('__Host-')) { + cookie.secure = true + cookie.domain = null + cookie.path = '/' + } + + if (cookie.secure) { + out.push('Secure') + } + + if (cookie.httpOnly) { + out.push('HttpOnly') + } + + if (typeof cookie.maxAge === 'number') { + validateCookieMaxAge(cookie.maxAge) + out.push(`Max-Age=${cookie.maxAge}`) + } + + if (cookie.domain) { + validateCookieDomain(cookie.domain) + out.push(`Domain=${cookie.domain}`) + } + + if (cookie.path) { + validateCookiePath(cookie.path) + out.push(`Path=${cookie.path}`) + } + + if (cookie.expires && cookie.expires.toString() !== 'Invalid Date') { + out.push(`Expires=${toIMFDate(cookie.expires)}`) + } + + if (cookie.sameSite) { + out.push(`SameSite=${cookie.sameSite}`) + } + + for (const part of cookie.unparsed) { + if (!part.includes('=')) { + throw new Error('Invalid unparsed') + } + + const [key, ...value] = part.split('=') + + out.push(`${key.trim()}=${value.join('=')}`) + } + + return out.join('; ') +} + +let kHeadersListNode + +function getHeadersList (headers) { + if (headers[kHeadersList]) { + return headers[kHeadersList] + } + + if (!kHeadersListNode) { + kHeadersListNode = Object.getOwnPropertySymbols(headers).find( + (symbol) => symbol.description === 'headers list' + ) + + assert(kHeadersListNode, 'Headers cannot be parsed') + } + + const headersList = headers[kHeadersListNode] + assert(headersList) + + return headersList +} + +module.exports = { + isCTLExcludingHtab, + stringify, + getHeadersList +} + + +/***/ }), + +/***/ 2067: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const net = __nccwpck_require__(1808) +const assert = __nccwpck_require__(9491) +const util = __nccwpck_require__(3983) +const { InvalidArgumentError, ConnectTimeoutError } = __nccwpck_require__(8045) + +let tls // include tls conditionally since it is not always available + +// TODO: session re-use does not wait for the first +// connection to resolve the session and might therefore +// resolve the same servername multiple times even when +// re-use is enabled. + +let SessionCache +// FIXME: remove workaround when the Node bug is fixed +// https://github.com/nodejs/node/issues/49344#issuecomment-1741776308 +if (global.FinalizationRegistry && !process.env.NODE_V8_COVERAGE) { + SessionCache = class WeakSessionCache { + constructor (maxCachedSessions) { + this._maxCachedSessions = maxCachedSessions + this._sessionCache = new Map() + this._sessionRegistry = new global.FinalizationRegistry((key) => { + if (this._sessionCache.size < this._maxCachedSessions) { + return + } + + const ref = this._sessionCache.get(key) + if (ref !== undefined && ref.deref() === undefined) { + this._sessionCache.delete(key) + } + }) + } + + get (sessionKey) { + const ref = this._sessionCache.get(sessionKey) + return ref ? ref.deref() : null + } + + set (sessionKey, session) { + if (this._maxCachedSessions === 0) { + return + } + + this._sessionCache.set(sessionKey, new WeakRef(session)) + this._sessionRegistry.register(session, sessionKey) + } + } +} else { + SessionCache = class SimpleSessionCache { + constructor (maxCachedSessions) { + this._maxCachedSessions = maxCachedSessions + this._sessionCache = new Map() + } + + get (sessionKey) { + return this._sessionCache.get(sessionKey) + } + + set (sessionKey, session) { + if (this._maxCachedSessions === 0) { + return + } + + if (this._sessionCache.size >= this._maxCachedSessions) { + // remove the oldest session + const { value: oldestKey } = this._sessionCache.keys().next() + this._sessionCache.delete(oldestKey) + } + + this._sessionCache.set(sessionKey, session) + } + } +} + +function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, ...opts }) { + if (maxCachedSessions != null && (!Number.isInteger(maxCachedSessions) || maxCachedSessions < 0)) { + throw new InvalidArgumentError('maxCachedSessions must be a positive integer or zero') + } + + const options = { path: socketPath, ...opts } + const sessionCache = new SessionCache(maxCachedSessions == null ? 100 : maxCachedSessions) + timeout = timeout == null ? 10e3 : timeout + allowH2 = allowH2 != null ? allowH2 : false + return function connect ({ hostname, host, protocol, port, servername, localAddress, httpSocket }, callback) { + let socket + if (protocol === 'https:') { + if (!tls) { + tls = __nccwpck_require__(4404) + } + servername = servername || options.servername || util.getServerName(host) || null + + const sessionKey = servername || hostname + const session = sessionCache.get(sessionKey) || null + + assert(sessionKey) + + socket = tls.connect({ + highWaterMark: 16384, // TLS in node can't have bigger HWM anyway... + ...options, + servername, + session, + localAddress, + // TODO(HTTP/2): Add support for h2c + ALPNProtocols: allowH2 ? ['http/1.1', 'h2'] : ['http/1.1'], + socket: httpSocket, // upgrade socket connection + port: port || 443, + host: hostname + }) + + socket + .on('session', function (session) { + // TODO (fix): Can a session become invalid once established? Don't think so? + sessionCache.set(sessionKey, session) + }) + } else { + assert(!httpSocket, 'httpSocket can only be sent on TLS update') + socket = net.connect({ + highWaterMark: 64 * 1024, // Same as nodejs fs streams. + ...options, + localAddress, + port: port || 80, + host: hostname + }) + } + + // Set TCP keep alive options on the socket here instead of in connect() for the case of assigning the socket + if (options.keepAlive == null || options.keepAlive) { + const keepAliveInitialDelay = options.keepAliveInitialDelay === undefined ? 60e3 : options.keepAliveInitialDelay + socket.setKeepAlive(true, keepAliveInitialDelay) + } + + const cancelTimeout = setupTimeout(() => onConnectTimeout(socket), timeout) + + socket + .setNoDelay(true) + .once(protocol === 'https:' ? 'secureConnect' : 'connect', function () { + cancelTimeout() + + if (callback) { + const cb = callback + callback = null + cb(null, this) + } + }) + .on('error', function (err) { + cancelTimeout() + + if (callback) { + const cb = callback + callback = null + cb(err) + } + }) + + return socket + } +} + +function setupTimeout (onConnectTimeout, timeout) { + if (!timeout) { + return () => {} + } + + let s1 = null + let s2 = null + const timeoutId = setTimeout(() => { + // setImmediate is added to make sure that we priotorise socket error events over timeouts + s1 = setImmediate(() => { + if (process.platform === 'win32') { + // Windows needs an extra setImmediate probably due to implementation differences in the socket logic + s2 = setImmediate(() => onConnectTimeout()) + } else { + onConnectTimeout() + } + }) + }, timeout) + return () => { + clearTimeout(timeoutId) + clearImmediate(s1) + clearImmediate(s2) + } +} + +function onConnectTimeout (socket) { + util.destroy(socket, new ConnectTimeoutError()) +} + +module.exports = buildConnector + + +/***/ }), + +/***/ 8045: +/***/ ((module) => { + +"use strict"; + + +class UndiciError extends Error { + constructor (message) { + super(message) + this.name = 'UndiciError' + this.code = 'UND_ERR' + } +} + +class ConnectTimeoutError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, ConnectTimeoutError) + this.name = 'ConnectTimeoutError' + this.message = message || 'Connect Timeout Error' + this.code = 'UND_ERR_CONNECT_TIMEOUT' + } +} + +class HeadersTimeoutError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, HeadersTimeoutError) + this.name = 'HeadersTimeoutError' + this.message = message || 'Headers Timeout Error' + this.code = 'UND_ERR_HEADERS_TIMEOUT' + } +} + +class HeadersOverflowError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, HeadersOverflowError) + this.name = 'HeadersOverflowError' + this.message = message || 'Headers Overflow Error' + this.code = 'UND_ERR_HEADERS_OVERFLOW' + } +} + +class BodyTimeoutError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, BodyTimeoutError) + this.name = 'BodyTimeoutError' + this.message = message || 'Body Timeout Error' + this.code = 'UND_ERR_BODY_TIMEOUT' + } +} + +class ResponseStatusCodeError extends UndiciError { + constructor (message, statusCode, headers, body) { + super(message) + Error.captureStackTrace(this, ResponseStatusCodeError) + this.name = 'ResponseStatusCodeError' + this.message = message || 'Response Status Code Error' + this.code = 'UND_ERR_RESPONSE_STATUS_CODE' + this.body = body + this.status = statusCode + this.statusCode = statusCode + this.headers = headers + } +} + +class InvalidArgumentError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, InvalidArgumentError) + this.name = 'InvalidArgumentError' + this.message = message || 'Invalid Argument Error' + this.code = 'UND_ERR_INVALID_ARG' + } +} + +class InvalidReturnValueError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, InvalidReturnValueError) + this.name = 'InvalidReturnValueError' + this.message = message || 'Invalid Return Value Error' + this.code = 'UND_ERR_INVALID_RETURN_VALUE' + } +} + +class RequestAbortedError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, RequestAbortedError) + this.name = 'AbortError' + this.message = message || 'Request aborted' + this.code = 'UND_ERR_ABORTED' + } +} + +class InformationalError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, InformationalError) + this.name = 'InformationalError' + this.message = message || 'Request information' + this.code = 'UND_ERR_INFO' + } +} + +class RequestContentLengthMismatchError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, RequestContentLengthMismatchError) + this.name = 'RequestContentLengthMismatchError' + this.message = message || 'Request body length does not match content-length header' + this.code = 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH' + } +} + +class ResponseContentLengthMismatchError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, ResponseContentLengthMismatchError) + this.name = 'ResponseContentLengthMismatchError' + this.message = message || 'Response body length does not match content-length header' + this.code = 'UND_ERR_RES_CONTENT_LENGTH_MISMATCH' + } +} + +class ClientDestroyedError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, ClientDestroyedError) + this.name = 'ClientDestroyedError' + this.message = message || 'The client is destroyed' + this.code = 'UND_ERR_DESTROYED' + } +} + +class ClientClosedError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, ClientClosedError) + this.name = 'ClientClosedError' + this.message = message || 'The client is closed' + this.code = 'UND_ERR_CLOSED' + } +} + +class SocketError extends UndiciError { + constructor (message, socket) { + super(message) + Error.captureStackTrace(this, SocketError) + this.name = 'SocketError' + this.message = message || 'Socket error' + this.code = 'UND_ERR_SOCKET' + this.socket = socket + } +} + +class NotSupportedError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, NotSupportedError) + this.name = 'NotSupportedError' + this.message = message || 'Not supported error' + this.code = 'UND_ERR_NOT_SUPPORTED' + } +} + +class BalancedPoolMissingUpstreamError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, NotSupportedError) + this.name = 'MissingUpstreamError' + this.message = message || 'No upstream has been added to the BalancedPool' + this.code = 'UND_ERR_BPL_MISSING_UPSTREAM' + } +} + +class HTTPParserError extends Error { + constructor (message, code, data) { + super(message) + Error.captureStackTrace(this, HTTPParserError) + this.name = 'HTTPParserError' + this.code = code ? `HPE_${code}` : undefined + this.data = data ? data.toString() : undefined + } +} + +class ResponseExceededMaxSizeError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, ResponseExceededMaxSizeError) + this.name = 'ResponseExceededMaxSizeError' + this.message = message || 'Response content exceeded max size' + this.code = 'UND_ERR_RES_EXCEEDED_MAX_SIZE' + } +} + +class RequestRetryError extends UndiciError { + constructor (message, code, { headers, data }) { + super(message) + Error.captureStackTrace(this, RequestRetryError) + this.name = 'RequestRetryError' + this.message = message || 'Request retry error' + this.code = 'UND_ERR_REQ_RETRY' + this.statusCode = code + this.data = data + this.headers = headers + } +} + +module.exports = { + HTTPParserError, + UndiciError, + HeadersTimeoutError, + HeadersOverflowError, + BodyTimeoutError, + RequestContentLengthMismatchError, + ConnectTimeoutError, + ResponseStatusCodeError, + InvalidArgumentError, + InvalidReturnValueError, + RequestAbortedError, + ClientDestroyedError, + ClientClosedError, + InformationalError, + SocketError, + NotSupportedError, + ResponseContentLengthMismatchError, + BalancedPoolMissingUpstreamError, + ResponseExceededMaxSizeError, + RequestRetryError +} + + +/***/ }), + +/***/ 2905: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { + InvalidArgumentError, + NotSupportedError +} = __nccwpck_require__(8045) +const assert = __nccwpck_require__(9491) +const { kHTTP2BuildRequest, kHTTP2CopyHeaders, kHTTP1BuildRequest } = __nccwpck_require__(2785) +const util = __nccwpck_require__(3983) + +// tokenRegExp and headerCharRegex have been lifted from +// https://github.com/nodejs/node/blob/main/lib/_http_common.js + +/** + * Verifies that the given val is a valid HTTP token + * per the rules defined in RFC 7230 + * See https://tools.ietf.org/html/rfc7230#section-3.2.6 + */ +const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/ + +/** + * Matches if val contains an invalid field-vchar + * field-value = *( field-content / obs-fold ) + * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] + * field-vchar = VCHAR / obs-text + */ +const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/ + +// Verifies that a given path is valid does not contain control chars \x00 to \x20 +const invalidPathRegex = /[^\u0021-\u00ff]/ + +const kHandler = Symbol('handler') + +const channels = {} + +let extractBody + +try { + const diagnosticsChannel = __nccwpck_require__(7643) + channels.create = diagnosticsChannel.channel('undici:request:create') + channels.bodySent = diagnosticsChannel.channel('undici:request:bodySent') + channels.headers = diagnosticsChannel.channel('undici:request:headers') + channels.trailers = diagnosticsChannel.channel('undici:request:trailers') + channels.error = diagnosticsChannel.channel('undici:request:error') +} catch { + channels.create = { hasSubscribers: false } + channels.bodySent = { hasSubscribers: false } + channels.headers = { hasSubscribers: false } + channels.trailers = { hasSubscribers: false } + channels.error = { hasSubscribers: false } +} + +class Request { + constructor (origin, { + path, + method, + body, + headers, + query, + idempotent, + blocking, + upgrade, + headersTimeout, + bodyTimeout, + reset, + throwOnError, + expectContinue + }, handler) { + if (typeof path !== 'string') { + throw new InvalidArgumentError('path must be a string') + } else if ( + path[0] !== '/' && + !(path.startsWith('http://') || path.startsWith('https://')) && + method !== 'CONNECT' + ) { + throw new InvalidArgumentError('path must be an absolute URL or start with a slash') + } else if (invalidPathRegex.exec(path) !== null) { + throw new InvalidArgumentError('invalid request path') + } + + if (typeof method !== 'string') { + throw new InvalidArgumentError('method must be a string') + } else if (tokenRegExp.exec(method) === null) { + throw new InvalidArgumentError('invalid request method') + } + + if (upgrade && typeof upgrade !== 'string') { + throw new InvalidArgumentError('upgrade must be a string') + } + + if (headersTimeout != null && (!Number.isFinite(headersTimeout) || headersTimeout < 0)) { + throw new InvalidArgumentError('invalid headersTimeout') + } + + if (bodyTimeout != null && (!Number.isFinite(bodyTimeout) || bodyTimeout < 0)) { + throw new InvalidArgumentError('invalid bodyTimeout') + } + + if (reset != null && typeof reset !== 'boolean') { + throw new InvalidArgumentError('invalid reset') + } + + if (expectContinue != null && typeof expectContinue !== 'boolean') { + throw new InvalidArgumentError('invalid expectContinue') + } + + this.headersTimeout = headersTimeout + + this.bodyTimeout = bodyTimeout + + this.throwOnError = throwOnError === true + + this.method = method + + this.abort = null + + if (body == null) { + this.body = null + } else if (util.isStream(body)) { + this.body = body + + const rState = this.body._readableState + if (!rState || !rState.autoDestroy) { + this.endHandler = function autoDestroy () { + util.destroy(this) + } + this.body.on('end', this.endHandler) + } + + this.errorHandler = err => { + if (this.abort) { + this.abort(err) + } else { + this.error = err + } + } + this.body.on('error', this.errorHandler) + } else if (util.isBuffer(body)) { + this.body = body.byteLength ? body : null + } else if (ArrayBuffer.isView(body)) { + this.body = body.buffer.byteLength ? Buffer.from(body.buffer, body.byteOffset, body.byteLength) : null + } else if (body instanceof ArrayBuffer) { + this.body = body.byteLength ? Buffer.from(body) : null + } else if (typeof body === 'string') { + this.body = body.length ? Buffer.from(body) : null + } else if (util.isFormDataLike(body) || util.isIterable(body) || util.isBlobLike(body)) { + this.body = body + } else { + throw new InvalidArgumentError('body must be a string, a Buffer, a Readable stream, an iterable, or an async iterable') + } + + this.completed = false + + this.aborted = false + + this.upgrade = upgrade || null + + this.path = query ? util.buildURL(path, query) : path + + this.origin = origin + + this.idempotent = idempotent == null + ? method === 'HEAD' || method === 'GET' + : idempotent + + this.blocking = blocking == null ? false : blocking + + this.reset = reset == null ? null : reset + + this.host = null + + this.contentLength = null + + this.contentType = null + + this.headers = '' + + // Only for H2 + this.expectContinue = expectContinue != null ? expectContinue : false + + if (Array.isArray(headers)) { + if (headers.length % 2 !== 0) { + throw new InvalidArgumentError('headers array must be even') + } + for (let i = 0; i < headers.length; i += 2) { + processHeader(this, headers[i], headers[i + 1]) + } + } else if (headers && typeof headers === 'object') { + const keys = Object.keys(headers) + for (let i = 0; i < keys.length; i++) { + const key = keys[i] + processHeader(this, key, headers[key]) + } + } else if (headers != null) { + throw new InvalidArgumentError('headers must be an object or an array') + } + + if (util.isFormDataLike(this.body)) { + if (util.nodeMajor < 16 || (util.nodeMajor === 16 && util.nodeMinor < 8)) { + throw new InvalidArgumentError('Form-Data bodies are only supported in node v16.8 and newer.') + } + + if (!extractBody) { + extractBody = (__nccwpck_require__(1472).extractBody) + } + + const [bodyStream, contentType] = extractBody(body) + if (this.contentType == null) { + this.contentType = contentType + this.headers += `content-type: ${contentType}\r\n` + } + this.body = bodyStream.stream + this.contentLength = bodyStream.length + } else if (util.isBlobLike(body) && this.contentType == null && body.type) { + this.contentType = body.type + this.headers += `content-type: ${body.type}\r\n` + } + + util.validateHandler(handler, method, upgrade) + + this.servername = util.getServerName(this.host) + + this[kHandler] = handler + + if (channels.create.hasSubscribers) { + channels.create.publish({ request: this }) + } + } + + onBodySent (chunk) { + if (this[kHandler].onBodySent) { + try { + return this[kHandler].onBodySent(chunk) + } catch (err) { + this.abort(err) + } + } + } + + onRequestSent () { + if (channels.bodySent.hasSubscribers) { + channels.bodySent.publish({ request: this }) + } + + if (this[kHandler].onRequestSent) { + try { + return this[kHandler].onRequestSent() + } catch (err) { + this.abort(err) + } + } + } + + onConnect (abort) { + assert(!this.aborted) + assert(!this.completed) + + if (this.error) { + abort(this.error) + } else { + this.abort = abort + return this[kHandler].onConnect(abort) + } + } + + onHeaders (statusCode, headers, resume, statusText) { + assert(!this.aborted) + assert(!this.completed) + + if (channels.headers.hasSubscribers) { + channels.headers.publish({ request: this, response: { statusCode, headers, statusText } }) + } + + try { + return this[kHandler].onHeaders(statusCode, headers, resume, statusText) + } catch (err) { + this.abort(err) + } + } + + onData (chunk) { + assert(!this.aborted) + assert(!this.completed) + + try { + return this[kHandler].onData(chunk) + } catch (err) { + this.abort(err) + return false + } + } + + onUpgrade (statusCode, headers, socket) { + assert(!this.aborted) + assert(!this.completed) + + return this[kHandler].onUpgrade(statusCode, headers, socket) + } + + onComplete (trailers) { + this.onFinally() + + assert(!this.aborted) + + this.completed = true + if (channels.trailers.hasSubscribers) { + channels.trailers.publish({ request: this, trailers }) + } + + try { + return this[kHandler].onComplete(trailers) + } catch (err) { + // TODO (fix): This might be a bad idea? + this.onError(err) + } + } + + onError (error) { + this.onFinally() + + if (channels.error.hasSubscribers) { + channels.error.publish({ request: this, error }) + } + + if (this.aborted) { + return + } + this.aborted = true + + return this[kHandler].onError(error) + } + + onFinally () { + if (this.errorHandler) { + this.body.off('error', this.errorHandler) + this.errorHandler = null + } + + if (this.endHandler) { + this.body.off('end', this.endHandler) + this.endHandler = null + } + } + + // TODO: adjust to support H2 + addHeader (key, value) { + processHeader(this, key, value) + return this + } + + static [kHTTP1BuildRequest] (origin, opts, handler) { + // TODO: Migrate header parsing here, to make Requests + // HTTP agnostic + return new Request(origin, opts, handler) + } + + static [kHTTP2BuildRequest] (origin, opts, handler) { + const headers = opts.headers + opts = { ...opts, headers: null } + + const request = new Request(origin, opts, handler) + + request.headers = {} + + if (Array.isArray(headers)) { + if (headers.length % 2 !== 0) { + throw new InvalidArgumentError('headers array must be even') + } + for (let i = 0; i < headers.length; i += 2) { + processHeader(request, headers[i], headers[i + 1], true) + } + } else if (headers && typeof headers === 'object') { + const keys = Object.keys(headers) + for (let i = 0; i < keys.length; i++) { + const key = keys[i] + processHeader(request, key, headers[key], true) + } + } else if (headers != null) { + throw new InvalidArgumentError('headers must be an object or an array') + } + + return request + } + + static [kHTTP2CopyHeaders] (raw) { + const rawHeaders = raw.split('\r\n') + const headers = {} + + for (const header of rawHeaders) { + const [key, value] = header.split(': ') + + if (value == null || value.length === 0) continue + + if (headers[key]) headers[key] += `,${value}` + else headers[key] = value + } + + return headers + } +} + +function processHeaderValue (key, val, skipAppend) { + if (val && typeof val === 'object') { + throw new InvalidArgumentError(`invalid ${key} header`) + } + + val = val != null ? `${val}` : '' + + if (headerCharRegex.exec(val) !== null) { + throw new InvalidArgumentError(`invalid ${key} header`) + } + + return skipAppend ? val : `${key}: ${val}\r\n` +} + +function processHeader (request, key, val, skipAppend = false) { + if (val && (typeof val === 'object' && !Array.isArray(val))) { + throw new InvalidArgumentError(`invalid ${key} header`) + } else if (val === undefined) { + return + } + + if ( + request.host === null && + key.length === 4 && + key.toLowerCase() === 'host' + ) { + if (headerCharRegex.exec(val) !== null) { + throw new InvalidArgumentError(`invalid ${key} header`) + } + // Consumed by Client + request.host = val + } else if ( + request.contentLength === null && + key.length === 14 && + key.toLowerCase() === 'content-length' + ) { + request.contentLength = parseInt(val, 10) + if (!Number.isFinite(request.contentLength)) { + throw new InvalidArgumentError('invalid content-length header') + } + } else if ( + request.contentType === null && + key.length === 12 && + key.toLowerCase() === 'content-type' + ) { + request.contentType = val + if (skipAppend) request.headers[key] = processHeaderValue(key, val, skipAppend) + else request.headers += processHeaderValue(key, val) + } else if ( + key.length === 17 && + key.toLowerCase() === 'transfer-encoding' + ) { + throw new InvalidArgumentError('invalid transfer-encoding header') + } else if ( + key.length === 10 && + key.toLowerCase() === 'connection' + ) { + const value = typeof val === 'string' ? val.toLowerCase() : null + if (value !== 'close' && value !== 'keep-alive') { + throw new InvalidArgumentError('invalid connection header') + } else if (value === 'close') { + request.reset = true + } + } else if ( + key.length === 10 && + key.toLowerCase() === 'keep-alive' + ) { + throw new InvalidArgumentError('invalid keep-alive header') + } else if ( + key.length === 7 && + key.toLowerCase() === 'upgrade' + ) { + throw new InvalidArgumentError('invalid upgrade header') + } else if ( + key.length === 6 && + key.toLowerCase() === 'expect' + ) { + throw new NotSupportedError('expect header not supported') + } else if (tokenRegExp.exec(key) === null) { + throw new InvalidArgumentError('invalid header key') + } else { + if (Array.isArray(val)) { + for (let i = 0; i < val.length; i++) { + if (skipAppend) { + if (request.headers[key]) request.headers[key] += `,${processHeaderValue(key, val[i], skipAppend)}` + else request.headers[key] = processHeaderValue(key, val[i], skipAppend) + } else { + request.headers += processHeaderValue(key, val[i]) + } + } + } else { + if (skipAppend) request.headers[key] = processHeaderValue(key, val, skipAppend) + else request.headers += processHeaderValue(key, val) + } + } +} + +module.exports = Request + + +/***/ }), + +/***/ 2785: +/***/ ((module) => { + +module.exports = { + kClose: Symbol('close'), + kDestroy: Symbol('destroy'), + kDispatch: Symbol('dispatch'), + kUrl: Symbol('url'), + kWriting: Symbol('writing'), + kResuming: Symbol('resuming'), + kQueue: Symbol('queue'), + kConnect: Symbol('connect'), + kConnecting: Symbol('connecting'), + kHeadersList: Symbol('headers list'), + kKeepAliveDefaultTimeout: Symbol('default keep alive timeout'), + kKeepAliveMaxTimeout: Symbol('max keep alive timeout'), + kKeepAliveTimeoutThreshold: Symbol('keep alive timeout threshold'), + kKeepAliveTimeoutValue: Symbol('keep alive timeout'), + kKeepAlive: Symbol('keep alive'), + kHeadersTimeout: Symbol('headers timeout'), + kBodyTimeout: Symbol('body timeout'), + kServerName: Symbol('server name'), + kLocalAddress: Symbol('local address'), + kHost: Symbol('host'), + kNoRef: Symbol('no ref'), + kBodyUsed: Symbol('used'), + kRunning: Symbol('running'), + kBlocking: Symbol('blocking'), + kPending: Symbol('pending'), + kSize: Symbol('size'), + kBusy: Symbol('busy'), + kQueued: Symbol('queued'), + kFree: Symbol('free'), + kConnected: Symbol('connected'), + kClosed: Symbol('closed'), + kNeedDrain: Symbol('need drain'), + kReset: Symbol('reset'), + kDestroyed: Symbol.for('nodejs.stream.destroyed'), + kMaxHeadersSize: Symbol('max headers size'), + kRunningIdx: Symbol('running index'), + kPendingIdx: Symbol('pending index'), + kError: Symbol('error'), + kClients: Symbol('clients'), + kClient: Symbol('client'), + kParser: Symbol('parser'), + kOnDestroyed: Symbol('destroy callbacks'), + kPipelining: Symbol('pipelining'), + kSocket: Symbol('socket'), + kHostHeader: Symbol('host header'), + kConnector: Symbol('connector'), + kStrictContentLength: Symbol('strict content length'), + kMaxRedirections: Symbol('maxRedirections'), + kMaxRequests: Symbol('maxRequestsPerClient'), + kProxy: Symbol('proxy agent options'), + kCounter: Symbol('socket request counter'), + kInterceptors: Symbol('dispatch interceptors'), + kMaxResponseSize: Symbol('max response size'), + kHTTP2Session: Symbol('http2Session'), + kHTTP2SessionState: Symbol('http2Session state'), + kHTTP2BuildRequest: Symbol('http2 build request'), + kHTTP1BuildRequest: Symbol('http1 build request'), + kHTTP2CopyHeaders: Symbol('http2 copy headers'), + kHTTPConnVersion: Symbol('http connection version'), + kRetryHandlerDefaultRetry: Symbol('retry agent default retry'), + kConstruct: Symbol('constructable') +} + + +/***/ }), + +/***/ 3983: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const assert = __nccwpck_require__(9491) +const { kDestroyed, kBodyUsed } = __nccwpck_require__(2785) +const { IncomingMessage } = __nccwpck_require__(3685) +const stream = __nccwpck_require__(2781) +const net = __nccwpck_require__(1808) +const { InvalidArgumentError } = __nccwpck_require__(8045) +const { Blob } = __nccwpck_require__(4300) +const nodeUtil = __nccwpck_require__(3837) +const { stringify } = __nccwpck_require__(3477) + +const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(v => Number(v)) + +function nop () {} + +function isStream (obj) { + return obj && typeof obj === 'object' && typeof obj.pipe === 'function' && typeof obj.on === 'function' +} + +// based on https://github.com/node-fetch/fetch-blob/blob/8ab587d34080de94140b54f07168451e7d0b655e/index.js#L229-L241 (MIT License) +function isBlobLike (object) { + return (Blob && object instanceof Blob) || ( + object && + typeof object === 'object' && + (typeof object.stream === 'function' || + typeof object.arrayBuffer === 'function') && + /^(Blob|File)$/.test(object[Symbol.toStringTag]) + ) +} + +function buildURL (url, queryParams) { + if (url.includes('?') || url.includes('#')) { + throw new Error('Query params cannot be passed when url already contains "?" or "#".') + } + + const stringified = stringify(queryParams) + + if (stringified) { + url += '?' + stringified + } + + return url +} + +function parseURL (url) { + if (typeof url === 'string') { + url = new URL(url) + + if (!/^https?:/.test(url.origin || url.protocol)) { + throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.') + } + + return url + } + + if (!url || typeof url !== 'object') { + throw new InvalidArgumentError('Invalid URL: The URL argument must be a non-null object.') + } + + if (!/^https?:/.test(url.origin || url.protocol)) { + throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.') + } + + if (!(url instanceof URL)) { + if (url.port != null && url.port !== '' && !Number.isFinite(parseInt(url.port))) { + throw new InvalidArgumentError('Invalid URL: port must be a valid integer or a string representation of an integer.') + } + + if (url.path != null && typeof url.path !== 'string') { + throw new InvalidArgumentError('Invalid URL path: the path must be a string or null/undefined.') + } + + if (url.pathname != null && typeof url.pathname !== 'string') { + throw new InvalidArgumentError('Invalid URL pathname: the pathname must be a string or null/undefined.') + } + + if (url.hostname != null && typeof url.hostname !== 'string') { + throw new InvalidArgumentError('Invalid URL hostname: the hostname must be a string or null/undefined.') + } + + if (url.origin != null && typeof url.origin !== 'string') { + throw new InvalidArgumentError('Invalid URL origin: the origin must be a string or null/undefined.') + } + + const port = url.port != null + ? url.port + : (url.protocol === 'https:' ? 443 : 80) + let origin = url.origin != null + ? url.origin + : `${url.protocol}//${url.hostname}:${port}` + let path = url.path != null + ? url.path + : `${url.pathname || ''}${url.search || ''}` + + if (origin.endsWith('/')) { + origin = origin.substring(0, origin.length - 1) + } + + if (path && !path.startsWith('/')) { + path = `/${path}` + } + // new URL(path, origin) is unsafe when `path` contains an absolute URL + // From https://developer.mozilla.org/en-US/docs/Web/API/URL/URL: + // If first parameter is a relative URL, second param is required, and will be used as the base URL. + // If first parameter is an absolute URL, a given second param will be ignored. + url = new URL(origin + path) + } + + return url +} + +function parseOrigin (url) { + url = parseURL(url) + + if (url.pathname !== '/' || url.search || url.hash) { + throw new InvalidArgumentError('invalid url') + } + + return url +} + +function getHostname (host) { + if (host[0] === '[') { + const idx = host.indexOf(']') + + assert(idx !== -1) + return host.substring(1, idx) + } + + const idx = host.indexOf(':') + if (idx === -1) return host + + return host.substring(0, idx) +} + +// IP addresses are not valid server names per RFC6066 +// > Currently, the only server names supported are DNS hostnames +function getServerName (host) { + if (!host) { + return null + } + + assert.strictEqual(typeof host, 'string') + + const servername = getHostname(host) + if (net.isIP(servername)) { + return '' + } + + return servername +} + +function deepClone (obj) { + return JSON.parse(JSON.stringify(obj)) +} + +function isAsyncIterable (obj) { + return !!(obj != null && typeof obj[Symbol.asyncIterator] === 'function') +} + +function isIterable (obj) { + return !!(obj != null && (typeof obj[Symbol.iterator] === 'function' || typeof obj[Symbol.asyncIterator] === 'function')) +} + +function bodyLength (body) { + if (body == null) { + return 0 + } else if (isStream(body)) { + const state = body._readableState + return state && state.objectMode === false && state.ended === true && Number.isFinite(state.length) + ? state.length + : null + } else if (isBlobLike(body)) { + return body.size != null ? body.size : null + } else if (isBuffer(body)) { + return body.byteLength + } + + return null +} + +function isDestroyed (stream) { + return !stream || !!(stream.destroyed || stream[kDestroyed]) +} + +function isReadableAborted (stream) { + const state = stream && stream._readableState + return isDestroyed(stream) && state && !state.endEmitted +} + +function destroy (stream, err) { + if (stream == null || !isStream(stream) || isDestroyed(stream)) { + return + } + + if (typeof stream.destroy === 'function') { + if (Object.getPrototypeOf(stream).constructor === IncomingMessage) { + // See: https://github.com/nodejs/node/pull/38505/files + stream.socket = null + } + + stream.destroy(err) + } else if (err) { + process.nextTick((stream, err) => { + stream.emit('error', err) + }, stream, err) + } + + if (stream.destroyed !== true) { + stream[kDestroyed] = true + } +} + +const KEEPALIVE_TIMEOUT_EXPR = /timeout=(\d+)/ +function parseKeepAliveTimeout (val) { + const m = val.toString().match(KEEPALIVE_TIMEOUT_EXPR) + return m ? parseInt(m[1], 10) * 1000 : null +} + +function parseHeaders (headers, obj = {}) { + // For H2 support + if (!Array.isArray(headers)) return headers + + for (let i = 0; i < headers.length; i += 2) { + const key = headers[i].toString().toLowerCase() + let val = obj[key] + + if (!val) { + if (Array.isArray(headers[i + 1])) { + obj[key] = headers[i + 1].map(x => x.toString('utf8')) + } else { + obj[key] = headers[i + 1].toString('utf8') + } + } else { + if (!Array.isArray(val)) { + val = [val] + obj[key] = val + } + val.push(headers[i + 1].toString('utf8')) + } + } + + // See https://github.com/nodejs/node/pull/46528 + if ('content-length' in obj && 'content-disposition' in obj) { + obj['content-disposition'] = Buffer.from(obj['content-disposition']).toString('latin1') + } + + return obj +} + +function parseRawHeaders (headers) { + const ret = [] + let hasContentLength = false + let contentDispositionIdx = -1 + + for (let n = 0; n < headers.length; n += 2) { + const key = headers[n + 0].toString() + const val = headers[n + 1].toString('utf8') + + if (key.length === 14 && (key === 'content-length' || key.toLowerCase() === 'content-length')) { + ret.push(key, val) + hasContentLength = true + } else if (key.length === 19 && (key === 'content-disposition' || key.toLowerCase() === 'content-disposition')) { + contentDispositionIdx = ret.push(key, val) - 1 + } else { + ret.push(key, val) + } + } + + // See https://github.com/nodejs/node/pull/46528 + if (hasContentLength && contentDispositionIdx !== -1) { + ret[contentDispositionIdx] = Buffer.from(ret[contentDispositionIdx]).toString('latin1') + } + + return ret +} + +function isBuffer (buffer) { + // See, https://github.com/mcollina/undici/pull/319 + return buffer instanceof Uint8Array || Buffer.isBuffer(buffer) +} + +function validateHandler (handler, method, upgrade) { + if (!handler || typeof handler !== 'object') { + throw new InvalidArgumentError('handler must be an object') + } + + if (typeof handler.onConnect !== 'function') { + throw new InvalidArgumentError('invalid onConnect method') + } + + if (typeof handler.onError !== 'function') { + throw new InvalidArgumentError('invalid onError method') + } + + if (typeof handler.onBodySent !== 'function' && handler.onBodySent !== undefined) { + throw new InvalidArgumentError('invalid onBodySent method') + } + + if (upgrade || method === 'CONNECT') { + if (typeof handler.onUpgrade !== 'function') { + throw new InvalidArgumentError('invalid onUpgrade method') + } + } else { + if (typeof handler.onHeaders !== 'function') { + throw new InvalidArgumentError('invalid onHeaders method') + } + + if (typeof handler.onData !== 'function') { + throw new InvalidArgumentError('invalid onData method') + } + + if (typeof handler.onComplete !== 'function') { + throw new InvalidArgumentError('invalid onComplete method') + } + } +} + +// A body is disturbed if it has been read from and it cannot +// be re-used without losing state or data. +function isDisturbed (body) { + return !!(body && ( + stream.isDisturbed + ? stream.isDisturbed(body) || body[kBodyUsed] // TODO (fix): Why is body[kBodyUsed] needed? + : body[kBodyUsed] || + body.readableDidRead || + (body._readableState && body._readableState.dataEmitted) || + isReadableAborted(body) + )) +} + +function isErrored (body) { + return !!(body && ( + stream.isErrored + ? stream.isErrored(body) + : /state: 'errored'/.test(nodeUtil.inspect(body) + ))) +} + +function isReadable (body) { + return !!(body && ( + stream.isReadable + ? stream.isReadable(body) + : /state: 'readable'/.test(nodeUtil.inspect(body) + ))) +} + +function getSocketInfo (socket) { + return { + localAddress: socket.localAddress, + localPort: socket.localPort, + remoteAddress: socket.remoteAddress, + remotePort: socket.remotePort, + remoteFamily: socket.remoteFamily, + timeout: socket.timeout, + bytesWritten: socket.bytesWritten, + bytesRead: socket.bytesRead + } +} + +async function * convertIterableToBuffer (iterable) { + for await (const chunk of iterable) { + yield Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk) + } +} + +let ReadableStream +function ReadableStreamFrom (iterable) { + if (!ReadableStream) { + ReadableStream = (__nccwpck_require__(5356).ReadableStream) + } + + if (ReadableStream.from) { + return ReadableStream.from(convertIterableToBuffer(iterable)) + } + + let iterator + return new ReadableStream( + { + async start () { + iterator = iterable[Symbol.asyncIterator]() + }, + async pull (controller) { + const { done, value } = await iterator.next() + if (done) { + queueMicrotask(() => { + controller.close() + }) + } else { + const buf = Buffer.isBuffer(value) ? value : Buffer.from(value) + controller.enqueue(new Uint8Array(buf)) + } + return controller.desiredSize > 0 + }, + async cancel (reason) { + await iterator.return() + } + }, + 0 + ) +} + +// The chunk should be a FormData instance and contains +// all the required methods. +function isFormDataLike (object) { + return ( + object && + typeof object === 'object' && + typeof object.append === 'function' && + typeof object.delete === 'function' && + typeof object.get === 'function' && + typeof object.getAll === 'function' && + typeof object.has === 'function' && + typeof object.set === 'function' && + object[Symbol.toStringTag] === 'FormData' + ) +} + +function throwIfAborted (signal) { + if (!signal) { return } + if (typeof signal.throwIfAborted === 'function') { + signal.throwIfAborted() + } else { + if (signal.aborted) { + // DOMException not available < v17.0.0 + const err = new Error('The operation was aborted') + err.name = 'AbortError' + throw err + } + } +} + +function addAbortListener (signal, listener) { + if ('addEventListener' in signal) { + signal.addEventListener('abort', listener, { once: true }) + return () => signal.removeEventListener('abort', listener) + } + signal.addListener('abort', listener) + return () => signal.removeListener('abort', listener) +} + +const hasToWellFormed = !!String.prototype.toWellFormed + +/** + * @param {string} val + */ +function toUSVString (val) { + if (hasToWellFormed) { + return `${val}`.toWellFormed() + } else if (nodeUtil.toUSVString) { + return nodeUtil.toUSVString(val) + } + + return `${val}` +} + +// Parsed accordingly to RFC 9110 +// https://www.rfc-editor.org/rfc/rfc9110#field.content-range +function parseRangeHeader (range) { + if (range == null || range === '') return { start: 0, end: null, size: null } + + const m = range ? range.match(/^bytes (\d+)-(\d+)\/(\d+)?$/) : null + return m + ? { + start: parseInt(m[1]), + end: m[2] ? parseInt(m[2]) : null, + size: m[3] ? parseInt(m[3]) : null + } + : null +} + +const kEnumerableProperty = Object.create(null) +kEnumerableProperty.enumerable = true + +module.exports = { + kEnumerableProperty, + nop, + isDisturbed, + isErrored, + isReadable, + toUSVString, + isReadableAborted, + isBlobLike, + parseOrigin, + parseURL, + getServerName, + isStream, + isIterable, + isAsyncIterable, + isDestroyed, + parseRawHeaders, + parseHeaders, + parseKeepAliveTimeout, + destroy, + bodyLength, + deepClone, + ReadableStreamFrom, + isBuffer, + validateHandler, + getSocketInfo, + isFormDataLike, + buildURL, + throwIfAborted, + addAbortListener, + parseRangeHeader, + nodeMajor, + nodeMinor, + nodeHasAutoSelectFamily: nodeMajor > 18 || (nodeMajor === 18 && nodeMinor >= 13), + safeHTTPMethods: ['GET', 'HEAD', 'OPTIONS', 'TRACE'] +} + + +/***/ }), + +/***/ 4839: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const Dispatcher = __nccwpck_require__(412) +const { + ClientDestroyedError, + ClientClosedError, + InvalidArgumentError +} = __nccwpck_require__(8045) +const { kDestroy, kClose, kDispatch, kInterceptors } = __nccwpck_require__(2785) + +const kDestroyed = Symbol('destroyed') +const kClosed = Symbol('closed') +const kOnDestroyed = Symbol('onDestroyed') +const kOnClosed = Symbol('onClosed') +const kInterceptedDispatch = Symbol('Intercepted Dispatch') + +class DispatcherBase extends Dispatcher { + constructor () { + super() + + this[kDestroyed] = false + this[kOnDestroyed] = null + this[kClosed] = false + this[kOnClosed] = [] + } + + get destroyed () { + return this[kDestroyed] + } + + get closed () { + return this[kClosed] + } + + get interceptors () { + return this[kInterceptors] + } + + set interceptors (newInterceptors) { + if (newInterceptors) { + for (let i = newInterceptors.length - 1; i >= 0; i--) { + const interceptor = this[kInterceptors][i] + if (typeof interceptor !== 'function') { + throw new InvalidArgumentError('interceptor must be an function') + } + } + } + + this[kInterceptors] = newInterceptors + } + + close (callback) { + if (callback === undefined) { + return new Promise((resolve, reject) => { + this.close((err, data) => { + return err ? reject(err) : resolve(data) + }) + }) + } + + if (typeof callback !== 'function') { + throw new InvalidArgumentError('invalid callback') + } + + if (this[kDestroyed]) { + queueMicrotask(() => callback(new ClientDestroyedError(), null)) + return + } + + if (this[kClosed]) { + if (this[kOnClosed]) { + this[kOnClosed].push(callback) + } else { + queueMicrotask(() => callback(null, null)) + } + return + } + + this[kClosed] = true + this[kOnClosed].push(callback) + + const onClosed = () => { + const callbacks = this[kOnClosed] + this[kOnClosed] = null + for (let i = 0; i < callbacks.length; i++) { + callbacks[i](null, null) + } + } + + // Should not error. + this[kClose]() + .then(() => this.destroy()) + .then(() => { + queueMicrotask(onClosed) + }) + } + + destroy (err, callback) { + if (typeof err === 'function') { + callback = err + err = null + } + + if (callback === undefined) { + return new Promise((resolve, reject) => { + this.destroy(err, (err, data) => { + return err ? /* istanbul ignore next: should never error */ reject(err) : resolve(data) + }) + }) + } + + if (typeof callback !== 'function') { + throw new InvalidArgumentError('invalid callback') + } + + if (this[kDestroyed]) { + if (this[kOnDestroyed]) { + this[kOnDestroyed].push(callback) + } else { + queueMicrotask(() => callback(null, null)) + } + return + } + + if (!err) { + err = new ClientDestroyedError() + } + + this[kDestroyed] = true + this[kOnDestroyed] = this[kOnDestroyed] || [] + this[kOnDestroyed].push(callback) + + const onDestroyed = () => { + const callbacks = this[kOnDestroyed] + this[kOnDestroyed] = null + for (let i = 0; i < callbacks.length; i++) { + callbacks[i](null, null) + } + } + + // Should not error. + this[kDestroy](err).then(() => { + queueMicrotask(onDestroyed) + }) + } + + [kInterceptedDispatch] (opts, handler) { + if (!this[kInterceptors] || this[kInterceptors].length === 0) { + this[kInterceptedDispatch] = this[kDispatch] + return this[kDispatch](opts, handler) + } + + let dispatch = this[kDispatch].bind(this) + for (let i = this[kInterceptors].length - 1; i >= 0; i--) { + dispatch = this[kInterceptors][i](dispatch) + } + this[kInterceptedDispatch] = dispatch + return dispatch(opts, handler) + } + + dispatch (opts, handler) { + if (!handler || typeof handler !== 'object') { + throw new InvalidArgumentError('handler must be an object') + } + + try { + if (!opts || typeof opts !== 'object') { + throw new InvalidArgumentError('opts must be an object.') + } + + if (this[kDestroyed] || this[kOnDestroyed]) { + throw new ClientDestroyedError() + } + + if (this[kClosed]) { + throw new ClientClosedError() + } + + return this[kInterceptedDispatch](opts, handler) + } catch (err) { + if (typeof handler.onError !== 'function') { + throw new InvalidArgumentError('invalid onError method') + } + + handler.onError(err) + + return false + } + } +} + +module.exports = DispatcherBase + + +/***/ }), + +/***/ 412: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const EventEmitter = __nccwpck_require__(2361) + +class Dispatcher extends EventEmitter { + dispatch () { + throw new Error('not implemented') + } + + close () { + throw new Error('not implemented') + } + + destroy () { + throw new Error('not implemented') + } +} + +module.exports = Dispatcher + + +/***/ }), + +/***/ 1472: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const Busboy = __nccwpck_require__(727) +const util = __nccwpck_require__(3983) +const { + ReadableStreamFrom, + isBlobLike, + isReadableStreamLike, + readableStreamClose, + createDeferredPromise, + fullyReadBody +} = __nccwpck_require__(2538) +const { FormData } = __nccwpck_require__(2015) +const { kState } = __nccwpck_require__(5861) +const { webidl } = __nccwpck_require__(1744) +const { DOMException, structuredClone } = __nccwpck_require__(1037) +const { Blob, File: NativeFile } = __nccwpck_require__(4300) +const { kBodyUsed } = __nccwpck_require__(2785) +const assert = __nccwpck_require__(9491) +const { isErrored } = __nccwpck_require__(3983) +const { isUint8Array, isArrayBuffer } = __nccwpck_require__(9830) +const { File: UndiciFile } = __nccwpck_require__(8511) +const { parseMIMEType, serializeAMimeType } = __nccwpck_require__(685) + +let ReadableStream = globalThis.ReadableStream + +/** @type {globalThis['File']} */ +const File = NativeFile ?? UndiciFile +const textEncoder = new TextEncoder() +const textDecoder = new TextDecoder() + +// https://fetch.spec.whatwg.org/#concept-bodyinit-extract +function extractBody (object, keepalive = false) { + if (!ReadableStream) { + ReadableStream = (__nccwpck_require__(5356).ReadableStream) + } + + // 1. Let stream be null. + let stream = null + + // 2. If object is a ReadableStream object, then set stream to object. + if (object instanceof ReadableStream) { + stream = object + } else if (isBlobLike(object)) { + // 3. Otherwise, if object is a Blob object, set stream to the + // result of running object’s get stream. + stream = object.stream() + } else { + // 4. Otherwise, set stream to a new ReadableStream object, and set + // up stream. + stream = new ReadableStream({ + async pull (controller) { + controller.enqueue( + typeof source === 'string' ? textEncoder.encode(source) : source + ) + queueMicrotask(() => readableStreamClose(controller)) + }, + start () {}, + type: undefined + }) + } + + // 5. Assert: stream is a ReadableStream object. + assert(isReadableStreamLike(stream)) + + // 6. Let action be null. + let action = null + + // 7. Let source be null. + let source = null + + // 8. Let length be null. + let length = null + + // 9. Let type be null. + let type = null + + // 10. Switch on object: + if (typeof object === 'string') { + // Set source to the UTF-8 encoding of object. + // Note: setting source to a Uint8Array here breaks some mocking assumptions. + source = object + + // Set type to `text/plain;charset=UTF-8`. + type = 'text/plain;charset=UTF-8' + } else if (object instanceof URLSearchParams) { + // URLSearchParams + + // spec says to run application/x-www-form-urlencoded on body.list + // this is implemented in Node.js as apart of an URLSearchParams instance toString method + // See: https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/lib/internal/url.js#L490 + // and https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/lib/internal/url.js#L1100 + + // Set source to the result of running the application/x-www-form-urlencoded serializer with object’s list. + source = object.toString() + + // Set type to `application/x-www-form-urlencoded;charset=UTF-8`. + type = 'application/x-www-form-urlencoded;charset=UTF-8' + } else if (isArrayBuffer(object)) { + // BufferSource/ArrayBuffer + + // Set source to a copy of the bytes held by object. + source = new Uint8Array(object.slice()) + } else if (ArrayBuffer.isView(object)) { + // BufferSource/ArrayBufferView + + // Set source to a copy of the bytes held by object. + source = new Uint8Array(object.buffer.slice(object.byteOffset, object.byteOffset + object.byteLength)) + } else if (util.isFormDataLike(object)) { + const boundary = `----formdata-undici-0${`${Math.floor(Math.random() * 1e11)}`.padStart(11, '0')}` + const prefix = `--${boundary}\r\nContent-Disposition: form-data` + + /*! formdata-polyfill. MIT License. Jimmy Wärting */ + const escape = (str) => + str.replace(/\n/g, '%0A').replace(/\r/g, '%0D').replace(/"/g, '%22') + const normalizeLinefeeds = (value) => value.replace(/\r?\n|\r/g, '\r\n') + + // Set action to this step: run the multipart/form-data + // encoding algorithm, with object’s entry list and UTF-8. + // - This ensures that the body is immutable and can't be changed afterwords + // - That the content-length is calculated in advance. + // - And that all parts are pre-encoded and ready to be sent. + + const blobParts = [] + const rn = new Uint8Array([13, 10]) // '\r\n' + length = 0 + let hasUnknownSizeValue = false + + for (const [name, value] of object) { + if (typeof value === 'string') { + const chunk = textEncoder.encode(prefix + + `; name="${escape(normalizeLinefeeds(name))}"` + + `\r\n\r\n${normalizeLinefeeds(value)}\r\n`) + blobParts.push(chunk) + length += chunk.byteLength + } else { + const chunk = textEncoder.encode(`${prefix}; name="${escape(normalizeLinefeeds(name))}"` + + (value.name ? `; filename="${escape(value.name)}"` : '') + '\r\n' + + `Content-Type: ${ + value.type || 'application/octet-stream' + }\r\n\r\n`) + blobParts.push(chunk, value, rn) + if (typeof value.size === 'number') { + length += chunk.byteLength + value.size + rn.byteLength + } else { + hasUnknownSizeValue = true + } + } + } + + const chunk = textEncoder.encode(`--${boundary}--`) + blobParts.push(chunk) + length += chunk.byteLength + if (hasUnknownSizeValue) { + length = null + } + + // Set source to object. + source = object + + action = async function * () { + for (const part of blobParts) { + if (part.stream) { + yield * part.stream() + } else { + yield part + } + } + } + + // Set type to `multipart/form-data; boundary=`, + // followed by the multipart/form-data boundary string generated + // by the multipart/form-data encoding algorithm. + type = 'multipart/form-data; boundary=' + boundary + } else if (isBlobLike(object)) { + // Blob + + // Set source to object. + source = object + + // Set length to object’s size. + length = object.size + + // If object’s type attribute is not the empty byte sequence, set + // type to its value. + if (object.type) { + type = object.type + } + } else if (typeof object[Symbol.asyncIterator] === 'function') { + // If keepalive is true, then throw a TypeError. + if (keepalive) { + throw new TypeError('keepalive') + } + + // If object is disturbed or locked, then throw a TypeError. + if (util.isDisturbed(object) || object.locked) { + throw new TypeError( + 'Response body object should not be disturbed or locked' + ) + } + + stream = + object instanceof ReadableStream ? object : ReadableStreamFrom(object) + } + + // 11. If source is a byte sequence, then set action to a + // step that returns source and length to source’s length. + if (typeof source === 'string' || util.isBuffer(source)) { + length = Buffer.byteLength(source) + } + + // 12. If action is non-null, then run these steps in in parallel: + if (action != null) { + // Run action. + let iterator + stream = new ReadableStream({ + async start () { + iterator = action(object)[Symbol.asyncIterator]() + }, + async pull (controller) { + const { value, done } = await iterator.next() + if (done) { + // When running action is done, close stream. + queueMicrotask(() => { + controller.close() + }) + } else { + // Whenever one or more bytes are available and stream is not errored, + // enqueue a Uint8Array wrapping an ArrayBuffer containing the available + // bytes into stream. + if (!isErrored(stream)) { + controller.enqueue(new Uint8Array(value)) + } + } + return controller.desiredSize > 0 + }, + async cancel (reason) { + await iterator.return() + }, + type: undefined + }) + } + + // 13. Let body be a body whose stream is stream, source is source, + // and length is length. + const body = { stream, source, length } + + // 14. Return (body, type). + return [body, type] +} + +// https://fetch.spec.whatwg.org/#bodyinit-safely-extract +function safelyExtractBody (object, keepalive = false) { + if (!ReadableStream) { + // istanbul ignore next + ReadableStream = (__nccwpck_require__(5356).ReadableStream) + } + + // To safely extract a body and a `Content-Type` value from + // a byte sequence or BodyInit object object, run these steps: + + // 1. If object is a ReadableStream object, then: + if (object instanceof ReadableStream) { + // Assert: object is neither disturbed nor locked. + // istanbul ignore next + assert(!util.isDisturbed(object), 'The body has already been consumed.') + // istanbul ignore next + assert(!object.locked, 'The stream is locked.') + } + + // 2. Return the results of extracting object. + return extractBody(object, keepalive) +} + +function cloneBody (body) { + // To clone a body body, run these steps: + + // https://fetch.spec.whatwg.org/#concept-body-clone + + // 1. Let « out1, out2 » be the result of teeing body’s stream. + const [out1, out2] = body.stream.tee() + const out2Clone = structuredClone(out2, { transfer: [out2] }) + // This, for whatever reasons, unrefs out2Clone which allows + // the process to exit by itself. + const [, finalClone] = out2Clone.tee() + + // 2. Set body’s stream to out1. + body.stream = out1 + + // 3. Return a body whose stream is out2 and other members are copied from body. + return { + stream: finalClone, + length: body.length, + source: body.source + } +} + +async function * consumeBody (body) { + if (body) { + if (isUint8Array(body)) { + yield body + } else { + const stream = body.stream + + if (util.isDisturbed(stream)) { + throw new TypeError('The body has already been consumed.') + } + + if (stream.locked) { + throw new TypeError('The stream is locked.') + } + + // Compat. + stream[kBodyUsed] = true + + yield * stream + } + } +} + +function throwIfAborted (state) { + if (state.aborted) { + throw new DOMException('The operation was aborted.', 'AbortError') + } +} + +function bodyMixinMethods (instance) { + const methods = { + blob () { + // The blob() method steps are to return the result of + // running consume body with this and the following step + // given a byte sequence bytes: return a Blob whose + // contents are bytes and whose type attribute is this’s + // MIME type. + return specConsumeBody(this, (bytes) => { + let mimeType = bodyMimeType(this) + + if (mimeType === 'failure') { + mimeType = '' + } else if (mimeType) { + mimeType = serializeAMimeType(mimeType) + } + + // Return a Blob whose contents are bytes and type attribute + // is mimeType. + return new Blob([bytes], { type: mimeType }) + }, instance) + }, + + arrayBuffer () { + // The arrayBuffer() method steps are to return the result + // of running consume body with this and the following step + // given a byte sequence bytes: return a new ArrayBuffer + // whose contents are bytes. + return specConsumeBody(this, (bytes) => { + return new Uint8Array(bytes).buffer + }, instance) + }, + + text () { + // The text() method steps are to return the result of running + // consume body with this and UTF-8 decode. + return specConsumeBody(this, utf8DecodeBytes, instance) + }, + + json () { + // The json() method steps are to return the result of running + // consume body with this and parse JSON from bytes. + return specConsumeBody(this, parseJSONFromBytes, instance) + }, + + async formData () { + webidl.brandCheck(this, instance) + + throwIfAborted(this[kState]) + + const contentType = this.headers.get('Content-Type') + + // If mimeType’s essence is "multipart/form-data", then: + if (/multipart\/form-data/.test(contentType)) { + const headers = {} + for (const [key, value] of this.headers) headers[key.toLowerCase()] = value + + const responseFormData = new FormData() + + let busboy + + try { + busboy = new Busboy({ + headers, + preservePath: true + }) + } catch (err) { + throw new DOMException(`${err}`, 'AbortError') + } + + busboy.on('field', (name, value) => { + responseFormData.append(name, value) + }) + busboy.on('file', (name, value, filename, encoding, mimeType) => { + const chunks = [] + + if (encoding === 'base64' || encoding.toLowerCase() === 'base64') { + let base64chunk = '' + + value.on('data', (chunk) => { + base64chunk += chunk.toString().replace(/[\r\n]/gm, '') + + const end = base64chunk.length - base64chunk.length % 4 + chunks.push(Buffer.from(base64chunk.slice(0, end), 'base64')) + + base64chunk = base64chunk.slice(end) + }) + value.on('end', () => { + chunks.push(Buffer.from(base64chunk, 'base64')) + responseFormData.append(name, new File(chunks, filename, { type: mimeType })) + }) + } else { + value.on('data', (chunk) => { + chunks.push(chunk) + }) + value.on('end', () => { + responseFormData.append(name, new File(chunks, filename, { type: mimeType })) + }) + } + }) + + const busboyResolve = new Promise((resolve, reject) => { + busboy.on('finish', resolve) + busboy.on('error', (err) => reject(new TypeError(err))) + }) + + if (this.body !== null) for await (const chunk of consumeBody(this[kState].body)) busboy.write(chunk) + busboy.end() + await busboyResolve + + return responseFormData + } else if (/application\/x-www-form-urlencoded/.test(contentType)) { + // Otherwise, if mimeType’s essence is "application/x-www-form-urlencoded", then: + + // 1. Let entries be the result of parsing bytes. + let entries + try { + let text = '' + // application/x-www-form-urlencoded parser will keep the BOM. + // https://url.spec.whatwg.org/#concept-urlencoded-parser + // Note that streaming decoder is stateful and cannot be reused + const streamingDecoder = new TextDecoder('utf-8', { ignoreBOM: true }) + + for await (const chunk of consumeBody(this[kState].body)) { + if (!isUint8Array(chunk)) { + throw new TypeError('Expected Uint8Array chunk') + } + text += streamingDecoder.decode(chunk, { stream: true }) + } + text += streamingDecoder.decode() + entries = new URLSearchParams(text) + } catch (err) { + // istanbul ignore next: Unclear when new URLSearchParams can fail on a string. + // 2. If entries is failure, then throw a TypeError. + throw Object.assign(new TypeError(), { cause: err }) + } + + // 3. Return a new FormData object whose entries are entries. + const formData = new FormData() + for (const [name, value] of entries) { + formData.append(name, value) + } + return formData + } else { + // Wait a tick before checking if the request has been aborted. + // Otherwise, a TypeError can be thrown when an AbortError should. + await Promise.resolve() + + throwIfAborted(this[kState]) + + // Otherwise, throw a TypeError. + throw webidl.errors.exception({ + header: `${instance.name}.formData`, + message: 'Could not parse content as FormData.' + }) + } + } + } + + return methods +} + +function mixinBody (prototype) { + Object.assign(prototype.prototype, bodyMixinMethods(prototype)) +} + +/** + * @see https://fetch.spec.whatwg.org/#concept-body-consume-body + * @param {Response|Request} object + * @param {(value: unknown) => unknown} convertBytesToJSValue + * @param {Response|Request} instance + */ +async function specConsumeBody (object, convertBytesToJSValue, instance) { + webidl.brandCheck(object, instance) + + throwIfAborted(object[kState]) + + // 1. If object is unusable, then return a promise rejected + // with a TypeError. + if (bodyUnusable(object[kState].body)) { + throw new TypeError('Body is unusable') + } + + // 2. Let promise be a new promise. + const promise = createDeferredPromise() + + // 3. Let errorSteps given error be to reject promise with error. + const errorSteps = (error) => promise.reject(error) + + // 4. Let successSteps given a byte sequence data be to resolve + // promise with the result of running convertBytesToJSValue + // with data. If that threw an exception, then run errorSteps + // with that exception. + const successSteps = (data) => { + try { + promise.resolve(convertBytesToJSValue(data)) + } catch (e) { + errorSteps(e) + } + } + + // 5. If object’s body is null, then run successSteps with an + // empty byte sequence. + if (object[kState].body == null) { + successSteps(new Uint8Array()) + return promise.promise + } + + // 6. Otherwise, fully read object’s body given successSteps, + // errorSteps, and object’s relevant global object. + await fullyReadBody(object[kState].body, successSteps, errorSteps) + + // 7. Return promise. + return promise.promise +} + +// https://fetch.spec.whatwg.org/#body-unusable +function bodyUnusable (body) { + // An object including the Body interface mixin is + // said to be unusable if its body is non-null and + // its body’s stream is disturbed or locked. + return body != null && (body.stream.locked || util.isDisturbed(body.stream)) +} + +/** + * @see https://encoding.spec.whatwg.org/#utf-8-decode + * @param {Buffer} buffer + */ +function utf8DecodeBytes (buffer) { + if (buffer.length === 0) { + return '' + } + + // 1. Let buffer be the result of peeking three bytes from + // ioQueue, converted to a byte sequence. + + // 2. If buffer is 0xEF 0xBB 0xBF, then read three + // bytes from ioQueue. (Do nothing with those bytes.) + if (buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) { + buffer = buffer.subarray(3) + } + + // 3. Process a queue with an instance of UTF-8’s + // decoder, ioQueue, output, and "replacement". + const output = textDecoder.decode(buffer) + + // 4. Return output. + return output +} + +/** + * @see https://infra.spec.whatwg.org/#parse-json-bytes-to-a-javascript-value + * @param {Uint8Array} bytes + */ +function parseJSONFromBytes (bytes) { + return JSON.parse(utf8DecodeBytes(bytes)) +} + +/** + * @see https://fetch.spec.whatwg.org/#concept-body-mime-type + * @param {import('./response').Response|import('./request').Request} object + */ +function bodyMimeType (object) { + const { headersList } = object[kState] + const contentType = headersList.get('content-type') + + if (contentType === null) { + return 'failure' + } + + return parseMIMEType(contentType) +} + +module.exports = { + extractBody, + safelyExtractBody, + cloneBody, + mixinBody +} + + +/***/ }), + +/***/ 1037: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { MessageChannel, receiveMessageOnPort } = __nccwpck_require__(1267) + +const corsSafeListedMethods = ['GET', 'HEAD', 'POST'] +const corsSafeListedMethodsSet = new Set(corsSafeListedMethods) + +const nullBodyStatus = [101, 204, 205, 304] + +const redirectStatus = [301, 302, 303, 307, 308] +const redirectStatusSet = new Set(redirectStatus) + +// https://fetch.spec.whatwg.org/#block-bad-port +const badPorts = [ + '1', '7', '9', '11', '13', '15', '17', '19', '20', '21', '22', '23', '25', '37', '42', '43', '53', '69', '77', '79', + '87', '95', '101', '102', '103', '104', '109', '110', '111', '113', '115', '117', '119', '123', '135', '137', + '139', '143', '161', '179', '389', '427', '465', '512', '513', '514', '515', '526', '530', '531', '532', + '540', '548', '554', '556', '563', '587', '601', '636', '989', '990', '993', '995', '1719', '1720', '1723', + '2049', '3659', '4045', '5060', '5061', '6000', '6566', '6665', '6666', '6667', '6668', '6669', '6697', + '10080' +] + +const badPortsSet = new Set(badPorts) + +// https://w3c.github.io/webappsec-referrer-policy/#referrer-policies +const referrerPolicy = [ + '', + 'no-referrer', + 'no-referrer-when-downgrade', + 'same-origin', + 'origin', + 'strict-origin', + 'origin-when-cross-origin', + 'strict-origin-when-cross-origin', + 'unsafe-url' +] +const referrerPolicySet = new Set(referrerPolicy) + +const requestRedirect = ['follow', 'manual', 'error'] + +const safeMethods = ['GET', 'HEAD', 'OPTIONS', 'TRACE'] +const safeMethodsSet = new Set(safeMethods) + +const requestMode = ['navigate', 'same-origin', 'no-cors', 'cors'] + +const requestCredentials = ['omit', 'same-origin', 'include'] + +const requestCache = [ + 'default', + 'no-store', + 'reload', + 'no-cache', + 'force-cache', + 'only-if-cached' +] + +// https://fetch.spec.whatwg.org/#request-body-header-name +const requestBodyHeader = [ + 'content-encoding', + 'content-language', + 'content-location', + 'content-type', + // See https://github.com/nodejs/undici/issues/2021 + // 'Content-Length' is a forbidden header name, which is typically + // removed in the Headers implementation. However, undici doesn't + // filter out headers, so we add it here. + 'content-length' +] + +// https://fetch.spec.whatwg.org/#enumdef-requestduplex +const requestDuplex = [ + 'half' +] + +// http://fetch.spec.whatwg.org/#forbidden-method +const forbiddenMethods = ['CONNECT', 'TRACE', 'TRACK'] +const forbiddenMethodsSet = new Set(forbiddenMethods) + +const subresource = [ + 'audio', + 'audioworklet', + 'font', + 'image', + 'manifest', + 'paintworklet', + 'script', + 'style', + 'track', + 'video', + 'xslt', + '' +] +const subresourceSet = new Set(subresource) + +/** @type {globalThis['DOMException']} */ +const DOMException = globalThis.DOMException ?? (() => { + // DOMException was only made a global in Node v17.0.0, + // but fetch supports >= v16.8. + try { + atob('~') + } catch (err) { + return Object.getPrototypeOf(err).constructor + } +})() + +let channel + +/** @type {globalThis['structuredClone']} */ +const structuredClone = + globalThis.structuredClone ?? + // https://github.com/nodejs/node/blob/b27ae24dcc4251bad726d9d84baf678d1f707fed/lib/internal/structured_clone.js + // structuredClone was added in v17.0.0, but fetch supports v16.8 + function structuredClone (value, options = undefined) { + if (arguments.length === 0) { + throw new TypeError('missing argument') + } + + if (!channel) { + channel = new MessageChannel() + } + channel.port1.unref() + channel.port2.unref() + channel.port1.postMessage(value, options?.transfer) + return receiveMessageOnPort(channel.port2).message + } + +module.exports = { + DOMException, + structuredClone, + subresource, + forbiddenMethods, + requestBodyHeader, + referrerPolicy, + requestRedirect, + requestMode, + requestCredentials, + requestCache, + redirectStatus, + corsSafeListedMethods, + nullBodyStatus, + safeMethods, + badPorts, + requestDuplex, + subresourceSet, + badPortsSet, + redirectStatusSet, + corsSafeListedMethodsSet, + safeMethodsSet, + forbiddenMethodsSet, + referrerPolicySet +} + + +/***/ }), + +/***/ 685: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const assert = __nccwpck_require__(9491) +const { atob } = __nccwpck_require__(4300) +const { isomorphicDecode } = __nccwpck_require__(2538) + +const encoder = new TextEncoder() + +/** + * @see https://mimesniff.spec.whatwg.org/#http-token-code-point + */ +const HTTP_TOKEN_CODEPOINTS = /^[!#$%&'*+-.^_|~A-Za-z0-9]+$/ +const HTTP_WHITESPACE_REGEX = /(\u000A|\u000D|\u0009|\u0020)/ // eslint-disable-line +/** + * @see https://mimesniff.spec.whatwg.org/#http-quoted-string-token-code-point + */ +const HTTP_QUOTED_STRING_TOKENS = /[\u0009|\u0020-\u007E|\u0080-\u00FF]/ // eslint-disable-line + +// https://fetch.spec.whatwg.org/#data-url-processor +/** @param {URL} dataURL */ +function dataURLProcessor (dataURL) { + // 1. Assert: dataURL’s scheme is "data". + assert(dataURL.protocol === 'data:') + + // 2. Let input be the result of running the URL + // serializer on dataURL with exclude fragment + // set to true. + let input = URLSerializer(dataURL, true) + + // 3. Remove the leading "data:" string from input. + input = input.slice(5) + + // 4. Let position point at the start of input. + const position = { position: 0 } + + // 5. Let mimeType be the result of collecting a + // sequence of code points that are not equal + // to U+002C (,), given position. + let mimeType = collectASequenceOfCodePointsFast( + ',', + input, + position + ) + + // 6. Strip leading and trailing ASCII whitespace + // from mimeType. + // Undici implementation note: we need to store the + // length because if the mimetype has spaces removed, + // the wrong amount will be sliced from the input in + // step #9 + const mimeTypeLength = mimeType.length + mimeType = removeASCIIWhitespace(mimeType, true, true) + + // 7. If position is past the end of input, then + // return failure + if (position.position >= input.length) { + return 'failure' + } + + // 8. Advance position by 1. + position.position++ + + // 9. Let encodedBody be the remainder of input. + const encodedBody = input.slice(mimeTypeLength + 1) + + // 10. Let body be the percent-decoding of encodedBody. + let body = stringPercentDecode(encodedBody) + + // 11. If mimeType ends with U+003B (;), followed by + // zero or more U+0020 SPACE, followed by an ASCII + // case-insensitive match for "base64", then: + if (/;(\u0020){0,}base64$/i.test(mimeType)) { + // 1. Let stringBody be the isomorphic decode of body. + const stringBody = isomorphicDecode(body) + + // 2. Set body to the forgiving-base64 decode of + // stringBody. + body = forgivingBase64(stringBody) + + // 3. If body is failure, then return failure. + if (body === 'failure') { + return 'failure' + } + + // 4. Remove the last 6 code points from mimeType. + mimeType = mimeType.slice(0, -6) + + // 5. Remove trailing U+0020 SPACE code points from mimeType, + // if any. + mimeType = mimeType.replace(/(\u0020)+$/, '') + + // 6. Remove the last U+003B (;) code point from mimeType. + mimeType = mimeType.slice(0, -1) + } + + // 12. If mimeType starts with U+003B (;), then prepend + // "text/plain" to mimeType. + if (mimeType.startsWith(';')) { + mimeType = 'text/plain' + mimeType + } + + // 13. Let mimeTypeRecord be the result of parsing + // mimeType. + let mimeTypeRecord = parseMIMEType(mimeType) + + // 14. If mimeTypeRecord is failure, then set + // mimeTypeRecord to text/plain;charset=US-ASCII. + if (mimeTypeRecord === 'failure') { + mimeTypeRecord = parseMIMEType('text/plain;charset=US-ASCII') + } + + // 15. Return a new data: URL struct whose MIME + // type is mimeTypeRecord and body is body. + // https://fetch.spec.whatwg.org/#data-url-struct + return { mimeType: mimeTypeRecord, body } +} + +// https://url.spec.whatwg.org/#concept-url-serializer +/** + * @param {URL} url + * @param {boolean} excludeFragment + */ +function URLSerializer (url, excludeFragment = false) { + if (!excludeFragment) { + return url.href + } + + const href = url.href + const hashLength = url.hash.length + + return hashLength === 0 ? href : href.substring(0, href.length - hashLength) +} + +// https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points +/** + * @param {(char: string) => boolean} condition + * @param {string} input + * @param {{ position: number }} position + */ +function collectASequenceOfCodePoints (condition, input, position) { + // 1. Let result be the empty string. + let result = '' + + // 2. While position doesn’t point past the end of input and the + // code point at position within input meets the condition condition: + while (position.position < input.length && condition(input[position.position])) { + // 1. Append that code point to the end of result. + result += input[position.position] + + // 2. Advance position by 1. + position.position++ + } + + // 3. Return result. + return result +} + +/** + * A faster collectASequenceOfCodePoints that only works when comparing a single character. + * @param {string} char + * @param {string} input + * @param {{ position: number }} position + */ +function collectASequenceOfCodePointsFast (char, input, position) { + const idx = input.indexOf(char, position.position) + const start = position.position + + if (idx === -1) { + position.position = input.length + return input.slice(start) + } + + position.position = idx + return input.slice(start, position.position) +} + +// https://url.spec.whatwg.org/#string-percent-decode +/** @param {string} input */ +function stringPercentDecode (input) { + // 1. Let bytes be the UTF-8 encoding of input. + const bytes = encoder.encode(input) + + // 2. Return the percent-decoding of bytes. + return percentDecode(bytes) +} + +// https://url.spec.whatwg.org/#percent-decode +/** @param {Uint8Array} input */ +function percentDecode (input) { + // 1. Let output be an empty byte sequence. + /** @type {number[]} */ + const output = [] + + // 2. For each byte byte in input: + for (let i = 0; i < input.length; i++) { + const byte = input[i] + + // 1. If byte is not 0x25 (%), then append byte to output. + if (byte !== 0x25) { + output.push(byte) + + // 2. Otherwise, if byte is 0x25 (%) and the next two bytes + // after byte in input are not in the ranges + // 0x30 (0) to 0x39 (9), 0x41 (A) to 0x46 (F), + // and 0x61 (a) to 0x66 (f), all inclusive, append byte + // to output. + } else if ( + byte === 0x25 && + !/^[0-9A-Fa-f]{2}$/i.test(String.fromCharCode(input[i + 1], input[i + 2])) + ) { + output.push(0x25) + + // 3. Otherwise: + } else { + // 1. Let bytePoint be the two bytes after byte in input, + // decoded, and then interpreted as hexadecimal number. + const nextTwoBytes = String.fromCharCode(input[i + 1], input[i + 2]) + const bytePoint = Number.parseInt(nextTwoBytes, 16) + + // 2. Append a byte whose value is bytePoint to output. + output.push(bytePoint) + + // 3. Skip the next two bytes in input. + i += 2 + } + } + + // 3. Return output. + return Uint8Array.from(output) +} + +// https://mimesniff.spec.whatwg.org/#parse-a-mime-type +/** @param {string} input */ +function parseMIMEType (input) { + // 1. Remove any leading and trailing HTTP whitespace + // from input. + input = removeHTTPWhitespace(input, true, true) + + // 2. Let position be a position variable for input, + // initially pointing at the start of input. + const position = { position: 0 } + + // 3. Let type be the result of collecting a sequence + // of code points that are not U+002F (/) from + // input, given position. + const type = collectASequenceOfCodePointsFast( + '/', + input, + position + ) + + // 4. If type is the empty string or does not solely + // contain HTTP token code points, then return failure. + // https://mimesniff.spec.whatwg.org/#http-token-code-point + if (type.length === 0 || !HTTP_TOKEN_CODEPOINTS.test(type)) { + return 'failure' + } + + // 5. If position is past the end of input, then return + // failure + if (position.position > input.length) { + return 'failure' + } + + // 6. Advance position by 1. (This skips past U+002F (/).) + position.position++ + + // 7. Let subtype be the result of collecting a sequence of + // code points that are not U+003B (;) from input, given + // position. + let subtype = collectASequenceOfCodePointsFast( + ';', + input, + position + ) + + // 8. Remove any trailing HTTP whitespace from subtype. + subtype = removeHTTPWhitespace(subtype, false, true) + + // 9. If subtype is the empty string or does not solely + // contain HTTP token code points, then return failure. + if (subtype.length === 0 || !HTTP_TOKEN_CODEPOINTS.test(subtype)) { + return 'failure' + } + + const typeLowercase = type.toLowerCase() + const subtypeLowercase = subtype.toLowerCase() + + // 10. Let mimeType be a new MIME type record whose type + // is type, in ASCII lowercase, and subtype is subtype, + // in ASCII lowercase. + // https://mimesniff.spec.whatwg.org/#mime-type + const mimeType = { + type: typeLowercase, + subtype: subtypeLowercase, + /** @type {Map} */ + parameters: new Map(), + // https://mimesniff.spec.whatwg.org/#mime-type-essence + essence: `${typeLowercase}/${subtypeLowercase}` + } + + // 11. While position is not past the end of input: + while (position.position < input.length) { + // 1. Advance position by 1. (This skips past U+003B (;).) + position.position++ + + // 2. Collect a sequence of code points that are HTTP + // whitespace from input given position. + collectASequenceOfCodePoints( + // https://fetch.spec.whatwg.org/#http-whitespace + char => HTTP_WHITESPACE_REGEX.test(char), + input, + position + ) + + // 3. Let parameterName be the result of collecting a + // sequence of code points that are not U+003B (;) + // or U+003D (=) from input, given position. + let parameterName = collectASequenceOfCodePoints( + (char) => char !== ';' && char !== '=', + input, + position + ) + + // 4. Set parameterName to parameterName, in ASCII + // lowercase. + parameterName = parameterName.toLowerCase() + + // 5. If position is not past the end of input, then: + if (position.position < input.length) { + // 1. If the code point at position within input is + // U+003B (;), then continue. + if (input[position.position] === ';') { + continue + } + + // 2. Advance position by 1. (This skips past U+003D (=).) + position.position++ + } + + // 6. If position is past the end of input, then break. + if (position.position > input.length) { + break + } + + // 7. Let parameterValue be null. + let parameterValue = null + + // 8. If the code point at position within input is + // U+0022 ("), then: + if (input[position.position] === '"') { + // 1. Set parameterValue to the result of collecting + // an HTTP quoted string from input, given position + // and the extract-value flag. + parameterValue = collectAnHTTPQuotedString(input, position, true) + + // 2. Collect a sequence of code points that are not + // U+003B (;) from input, given position. + collectASequenceOfCodePointsFast( + ';', + input, + position + ) + + // 9. Otherwise: + } else { + // 1. Set parameterValue to the result of collecting + // a sequence of code points that are not U+003B (;) + // from input, given position. + parameterValue = collectASequenceOfCodePointsFast( + ';', + input, + position + ) + + // 2. Remove any trailing HTTP whitespace from parameterValue. + parameterValue = removeHTTPWhitespace(parameterValue, false, true) + + // 3. If parameterValue is the empty string, then continue. + if (parameterValue.length === 0) { + continue + } + } + + // 10. If all of the following are true + // - parameterName is not the empty string + // - parameterName solely contains HTTP token code points + // - parameterValue solely contains HTTP quoted-string token code points + // - mimeType’s parameters[parameterName] does not exist + // then set mimeType’s parameters[parameterName] to parameterValue. + if ( + parameterName.length !== 0 && + HTTP_TOKEN_CODEPOINTS.test(parameterName) && + (parameterValue.length === 0 || HTTP_QUOTED_STRING_TOKENS.test(parameterValue)) && + !mimeType.parameters.has(parameterName) + ) { + mimeType.parameters.set(parameterName, parameterValue) + } + } + + // 12. Return mimeType. + return mimeType +} + +// https://infra.spec.whatwg.org/#forgiving-base64-decode +/** @param {string} data */ +function forgivingBase64 (data) { + // 1. Remove all ASCII whitespace from data. + data = data.replace(/[\u0009\u000A\u000C\u000D\u0020]/g, '') // eslint-disable-line + + // 2. If data’s code point length divides by 4 leaving + // no remainder, then: + if (data.length % 4 === 0) { + // 1. If data ends with one or two U+003D (=) code points, + // then remove them from data. + data = data.replace(/=?=$/, '') + } + + // 3. If data’s code point length divides by 4 leaving + // a remainder of 1, then return failure. + if (data.length % 4 === 1) { + return 'failure' + } + + // 4. If data contains a code point that is not one of + // U+002B (+) + // U+002F (/) + // ASCII alphanumeric + // then return failure. + if (/[^+/0-9A-Za-z]/.test(data)) { + return 'failure' + } + + const binary = atob(data) + const bytes = new Uint8Array(binary.length) + + for (let byte = 0; byte < binary.length; byte++) { + bytes[byte] = binary.charCodeAt(byte) + } + + return bytes +} + +// https://fetch.spec.whatwg.org/#collect-an-http-quoted-string +// tests: https://fetch.spec.whatwg.org/#example-http-quoted-string +/** + * @param {string} input + * @param {{ position: number }} position + * @param {boolean?} extractValue + */ +function collectAnHTTPQuotedString (input, position, extractValue) { + // 1. Let positionStart be position. + const positionStart = position.position + + // 2. Let value be the empty string. + let value = '' + + // 3. Assert: the code point at position within input + // is U+0022 ("). + assert(input[position.position] === '"') + + // 4. Advance position by 1. + position.position++ + + // 5. While true: + while (true) { + // 1. Append the result of collecting a sequence of code points + // that are not U+0022 (") or U+005C (\) from input, given + // position, to value. + value += collectASequenceOfCodePoints( + (char) => char !== '"' && char !== '\\', + input, + position + ) + + // 2. If position is past the end of input, then break. + if (position.position >= input.length) { + break + } + + // 3. Let quoteOrBackslash be the code point at position within + // input. + const quoteOrBackslash = input[position.position] + + // 4. Advance position by 1. + position.position++ + + // 5. If quoteOrBackslash is U+005C (\), then: + if (quoteOrBackslash === '\\') { + // 1. If position is past the end of input, then append + // U+005C (\) to value and break. + if (position.position >= input.length) { + value += '\\' + break + } + + // 2. Append the code point at position within input to value. + value += input[position.position] + + // 3. Advance position by 1. + position.position++ + + // 6. Otherwise: + } else { + // 1. Assert: quoteOrBackslash is U+0022 ("). + assert(quoteOrBackslash === '"') + + // 2. Break. + break + } + } + + // 6. If the extract-value flag is set, then return value. + if (extractValue) { + return value + } + + // 7. Return the code points from positionStart to position, + // inclusive, within input. + return input.slice(positionStart, position.position) +} + +/** + * @see https://mimesniff.spec.whatwg.org/#serialize-a-mime-type + */ +function serializeAMimeType (mimeType) { + assert(mimeType !== 'failure') + const { parameters, essence } = mimeType + + // 1. Let serialization be the concatenation of mimeType’s + // type, U+002F (/), and mimeType’s subtype. + let serialization = essence + + // 2. For each name → value of mimeType’s parameters: + for (let [name, value] of parameters.entries()) { + // 1. Append U+003B (;) to serialization. + serialization += ';' + + // 2. Append name to serialization. + serialization += name + + // 3. Append U+003D (=) to serialization. + serialization += '=' + + // 4. If value does not solely contain HTTP token code + // points or value is the empty string, then: + if (!HTTP_TOKEN_CODEPOINTS.test(value)) { + // 1. Precede each occurence of U+0022 (") or + // U+005C (\) in value with U+005C (\). + value = value.replace(/(\\|")/g, '\\$1') + + // 2. Prepend U+0022 (") to value. + value = '"' + value + + // 3. Append U+0022 (") to value. + value += '"' + } + + // 5. Append value to serialization. + serialization += value + } + + // 3. Return serialization. + return serialization +} + +/** + * @see https://fetch.spec.whatwg.org/#http-whitespace + * @param {string} char + */ +function isHTTPWhiteSpace (char) { + return char === '\r' || char === '\n' || char === '\t' || char === ' ' +} + +/** + * @see https://fetch.spec.whatwg.org/#http-whitespace + * @param {string} str + */ +function removeHTTPWhitespace (str, leading = true, trailing = true) { + let lead = 0 + let trail = str.length - 1 + + if (leading) { + for (; lead < str.length && isHTTPWhiteSpace(str[lead]); lead++); + } + + if (trailing) { + for (; trail > 0 && isHTTPWhiteSpace(str[trail]); trail--); + } + + return str.slice(lead, trail + 1) +} + +/** + * @see https://infra.spec.whatwg.org/#ascii-whitespace + * @param {string} char + */ +function isASCIIWhitespace (char) { + return char === '\r' || char === '\n' || char === '\t' || char === '\f' || char === ' ' +} + +/** + * @see https://infra.spec.whatwg.org/#strip-leading-and-trailing-ascii-whitespace + */ +function removeASCIIWhitespace (str, leading = true, trailing = true) { + let lead = 0 + let trail = str.length - 1 + + if (leading) { + for (; lead < str.length && isASCIIWhitespace(str[lead]); lead++); + } + + if (trailing) { + for (; trail > 0 && isASCIIWhitespace(str[trail]); trail--); + } + + return str.slice(lead, trail + 1) +} + +module.exports = { + dataURLProcessor, + URLSerializer, + collectASequenceOfCodePoints, + collectASequenceOfCodePointsFast, + stringPercentDecode, + parseMIMEType, + collectAnHTTPQuotedString, + serializeAMimeType +} + + +/***/ }), + +/***/ 8511: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { Blob, File: NativeFile } = __nccwpck_require__(4300) +const { types } = __nccwpck_require__(3837) +const { kState } = __nccwpck_require__(5861) +const { isBlobLike } = __nccwpck_require__(2538) +const { webidl } = __nccwpck_require__(1744) +const { parseMIMEType, serializeAMimeType } = __nccwpck_require__(685) +const { kEnumerableProperty } = __nccwpck_require__(3983) +const encoder = new TextEncoder() + +class File extends Blob { + constructor (fileBits, fileName, options = {}) { + // The File constructor is invoked with two or three parameters, depending + // on whether the optional dictionary parameter is used. When the File() + // constructor is invoked, user agents must run the following steps: + webidl.argumentLengthCheck(arguments, 2, { header: 'File constructor' }) + + fileBits = webidl.converters['sequence'](fileBits) + fileName = webidl.converters.USVString(fileName) + options = webidl.converters.FilePropertyBag(options) + + // 1. Let bytes be the result of processing blob parts given fileBits and + // options. + // Note: Blob handles this for us + + // 2. Let n be the fileName argument to the constructor. + const n = fileName + + // 3. Process FilePropertyBag dictionary argument by running the following + // substeps: + + // 1. If the type member is provided and is not the empty string, let t + // be set to the type dictionary member. If t contains any characters + // outside the range U+0020 to U+007E, then set t to the empty string + // and return from these substeps. + // 2. Convert every character in t to ASCII lowercase. + let t = options.type + let d + + // eslint-disable-next-line no-labels + substep: { + if (t) { + t = parseMIMEType(t) + + if (t === 'failure') { + t = '' + // eslint-disable-next-line no-labels + break substep + } + + t = serializeAMimeType(t).toLowerCase() + } + + // 3. If the lastModified member is provided, let d be set to the + // lastModified dictionary member. If it is not provided, set d to the + // current date and time represented as the number of milliseconds since + // the Unix Epoch (which is the equivalent of Date.now() [ECMA-262]). + d = options.lastModified + } + + // 4. Return a new File object F such that: + // F refers to the bytes byte sequence. + // F.size is set to the number of total bytes in bytes. + // F.name is set to n. + // F.type is set to t. + // F.lastModified is set to d. + + super(processBlobParts(fileBits, options), { type: t }) + this[kState] = { + name: n, + lastModified: d, + type: t + } + } + + get name () { + webidl.brandCheck(this, File) + + return this[kState].name + } + + get lastModified () { + webidl.brandCheck(this, File) + + return this[kState].lastModified + } + + get type () { + webidl.brandCheck(this, File) + + return this[kState].type + } +} + +class FileLike { + constructor (blobLike, fileName, options = {}) { + // TODO: argument idl type check + + // The File constructor is invoked with two or three parameters, depending + // on whether the optional dictionary parameter is used. When the File() + // constructor is invoked, user agents must run the following steps: + + // 1. Let bytes be the result of processing blob parts given fileBits and + // options. + + // 2. Let n be the fileName argument to the constructor. + const n = fileName + + // 3. Process FilePropertyBag dictionary argument by running the following + // substeps: + + // 1. If the type member is provided and is not the empty string, let t + // be set to the type dictionary member. If t contains any characters + // outside the range U+0020 to U+007E, then set t to the empty string + // and return from these substeps. + // TODO + const t = options.type + + // 2. Convert every character in t to ASCII lowercase. + // TODO + + // 3. If the lastModified member is provided, let d be set to the + // lastModified dictionary member. If it is not provided, set d to the + // current date and time represented as the number of milliseconds since + // the Unix Epoch (which is the equivalent of Date.now() [ECMA-262]). + const d = options.lastModified ?? Date.now() + + // 4. Return a new File object F such that: + // F refers to the bytes byte sequence. + // F.size is set to the number of total bytes in bytes. + // F.name is set to n. + // F.type is set to t. + // F.lastModified is set to d. + + this[kState] = { + blobLike, + name: n, + type: t, + lastModified: d + } + } + + stream (...args) { + webidl.brandCheck(this, FileLike) + + return this[kState].blobLike.stream(...args) + } + + arrayBuffer (...args) { + webidl.brandCheck(this, FileLike) + + return this[kState].blobLike.arrayBuffer(...args) + } + + slice (...args) { + webidl.brandCheck(this, FileLike) + + return this[kState].blobLike.slice(...args) + } + + text (...args) { + webidl.brandCheck(this, FileLike) + + return this[kState].blobLike.text(...args) + } + + get size () { + webidl.brandCheck(this, FileLike) + + return this[kState].blobLike.size + } + + get type () { + webidl.brandCheck(this, FileLike) + + return this[kState].blobLike.type + } + + get name () { + webidl.brandCheck(this, FileLike) + + return this[kState].name + } + + get lastModified () { + webidl.brandCheck(this, FileLike) + + return this[kState].lastModified + } + + get [Symbol.toStringTag] () { + return 'File' + } +} + +Object.defineProperties(File.prototype, { + [Symbol.toStringTag]: { + value: 'File', + configurable: true + }, + name: kEnumerableProperty, + lastModified: kEnumerableProperty +}) + +webidl.converters.Blob = webidl.interfaceConverter(Blob) + +webidl.converters.BlobPart = function (V, opts) { + if (webidl.util.Type(V) === 'Object') { + if (isBlobLike(V)) { + return webidl.converters.Blob(V, { strict: false }) + } + + if ( + ArrayBuffer.isView(V) || + types.isAnyArrayBuffer(V) + ) { + return webidl.converters.BufferSource(V, opts) + } + } + + return webidl.converters.USVString(V, opts) +} + +webidl.converters['sequence'] = webidl.sequenceConverter( + webidl.converters.BlobPart +) + +// https://www.w3.org/TR/FileAPI/#dfn-FilePropertyBag +webidl.converters.FilePropertyBag = webidl.dictionaryConverter([ + { + key: 'lastModified', + converter: webidl.converters['long long'], + get defaultValue () { + return Date.now() + } + }, + { + key: 'type', + converter: webidl.converters.DOMString, + defaultValue: '' + }, + { + key: 'endings', + converter: (value) => { + value = webidl.converters.DOMString(value) + value = value.toLowerCase() + + if (value !== 'native') { + value = 'transparent' + } + + return value + }, + defaultValue: 'transparent' + } +]) + +/** + * @see https://www.w3.org/TR/FileAPI/#process-blob-parts + * @param {(NodeJS.TypedArray|Blob|string)[]} parts + * @param {{ type: string, endings: string }} options + */ +function processBlobParts (parts, options) { + // 1. Let bytes be an empty sequence of bytes. + /** @type {NodeJS.TypedArray[]} */ + const bytes = [] + + // 2. For each element in parts: + for (const element of parts) { + // 1. If element is a USVString, run the following substeps: + if (typeof element === 'string') { + // 1. Let s be element. + let s = element + + // 2. If the endings member of options is "native", set s + // to the result of converting line endings to native + // of element. + if (options.endings === 'native') { + s = convertLineEndingsNative(s) + } + + // 3. Append the result of UTF-8 encoding s to bytes. + bytes.push(encoder.encode(s)) + } else if ( + types.isAnyArrayBuffer(element) || + types.isTypedArray(element) + ) { + // 2. If element is a BufferSource, get a copy of the + // bytes held by the buffer source, and append those + // bytes to bytes. + if (!element.buffer) { // ArrayBuffer + bytes.push(new Uint8Array(element)) + } else { + bytes.push( + new Uint8Array(element.buffer, element.byteOffset, element.byteLength) + ) + } + } else if (isBlobLike(element)) { + // 3. If element is a Blob, append the bytes it represents + // to bytes. + bytes.push(element) + } + } + + // 3. Return bytes. + return bytes +} + +/** + * @see https://www.w3.org/TR/FileAPI/#convert-line-endings-to-native + * @param {string} s + */ +function convertLineEndingsNative (s) { + // 1. Let native line ending be be the code point U+000A LF. + let nativeLineEnding = '\n' + + // 2. If the underlying platform’s conventions are to + // represent newlines as a carriage return and line feed + // sequence, set native line ending to the code point + // U+000D CR followed by the code point U+000A LF. + if (process.platform === 'win32') { + nativeLineEnding = '\r\n' + } + + return s.replace(/\r?\n/g, nativeLineEnding) +} + +// If this function is moved to ./util.js, some tools (such as +// rollup) will warn about circular dependencies. See: +// https://github.com/nodejs/undici/issues/1629 +function isFileLike (object) { + return ( + (NativeFile && object instanceof NativeFile) || + object instanceof File || ( + object && + (typeof object.stream === 'function' || + typeof object.arrayBuffer === 'function') && + object[Symbol.toStringTag] === 'File' + ) + ) +} + +module.exports = { File, FileLike, isFileLike } + + +/***/ }), + +/***/ 2015: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { isBlobLike, toUSVString, makeIterator } = __nccwpck_require__(2538) +const { kState } = __nccwpck_require__(5861) +const { File: UndiciFile, FileLike, isFileLike } = __nccwpck_require__(8511) +const { webidl } = __nccwpck_require__(1744) +const { Blob, File: NativeFile } = __nccwpck_require__(4300) + +/** @type {globalThis['File']} */ +const File = NativeFile ?? UndiciFile + +// https://xhr.spec.whatwg.org/#formdata +class FormData { + constructor (form) { + if (form !== undefined) { + throw webidl.errors.conversionFailed({ + prefix: 'FormData constructor', + argument: 'Argument 1', + types: ['undefined'] + }) + } + + this[kState] = [] + } + + append (name, value, filename = undefined) { + webidl.brandCheck(this, FormData) + + webidl.argumentLengthCheck(arguments, 2, { header: 'FormData.append' }) + + if (arguments.length === 3 && !isBlobLike(value)) { + throw new TypeError( + "Failed to execute 'append' on 'FormData': parameter 2 is not of type 'Blob'" + ) + } + + // 1. Let value be value if given; otherwise blobValue. + + name = webidl.converters.USVString(name) + value = isBlobLike(value) + ? webidl.converters.Blob(value, { strict: false }) + : webidl.converters.USVString(value) + filename = arguments.length === 3 + ? webidl.converters.USVString(filename) + : undefined + + // 2. Let entry be the result of creating an entry with + // name, value, and filename if given. + const entry = makeEntry(name, value, filename) + + // 3. Append entry to this’s entry list. + this[kState].push(entry) + } + + delete (name) { + webidl.brandCheck(this, FormData) + + webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.delete' }) + + name = webidl.converters.USVString(name) + + // The delete(name) method steps are to remove all entries whose name + // is name from this’s entry list. + this[kState] = this[kState].filter(entry => entry.name !== name) + } + + get (name) { + webidl.brandCheck(this, FormData) + + webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.get' }) + + name = webidl.converters.USVString(name) + + // 1. If there is no entry whose name is name in this’s entry list, + // then return null. + const idx = this[kState].findIndex((entry) => entry.name === name) + if (idx === -1) { + return null + } + + // 2. Return the value of the first entry whose name is name from + // this’s entry list. + return this[kState][idx].value + } + + getAll (name) { + webidl.brandCheck(this, FormData) + + webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.getAll' }) + + name = webidl.converters.USVString(name) + + // 1. If there is no entry whose name is name in this’s entry list, + // then return the empty list. + // 2. Return the values of all entries whose name is name, in order, + // from this’s entry list. + return this[kState] + .filter((entry) => entry.name === name) + .map((entry) => entry.value) + } + + has (name) { + webidl.brandCheck(this, FormData) + + webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.has' }) + + name = webidl.converters.USVString(name) + + // The has(name) method steps are to return true if there is an entry + // whose name is name in this’s entry list; otherwise false. + return this[kState].findIndex((entry) => entry.name === name) !== -1 + } + + set (name, value, filename = undefined) { + webidl.brandCheck(this, FormData) + + webidl.argumentLengthCheck(arguments, 2, { header: 'FormData.set' }) + + if (arguments.length === 3 && !isBlobLike(value)) { + throw new TypeError( + "Failed to execute 'set' on 'FormData': parameter 2 is not of type 'Blob'" + ) + } + + // The set(name, value) and set(name, blobValue, filename) method steps + // are: + + // 1. Let value be value if given; otherwise blobValue. + + name = webidl.converters.USVString(name) + value = isBlobLike(value) + ? webidl.converters.Blob(value, { strict: false }) + : webidl.converters.USVString(value) + filename = arguments.length === 3 + ? toUSVString(filename) + : undefined + + // 2. Let entry be the result of creating an entry with name, value, and + // filename if given. + const entry = makeEntry(name, value, filename) + + // 3. If there are entries in this’s entry list whose name is name, then + // replace the first such entry with entry and remove the others. + const idx = this[kState].findIndex((entry) => entry.name === name) + if (idx !== -1) { + this[kState] = [ + ...this[kState].slice(0, idx), + entry, + ...this[kState].slice(idx + 1).filter((entry) => entry.name !== name) + ] + } else { + // 4. Otherwise, append entry to this’s entry list. + this[kState].push(entry) + } + } + + entries () { + webidl.brandCheck(this, FormData) + + return makeIterator( + () => this[kState].map(pair => [pair.name, pair.value]), + 'FormData', + 'key+value' + ) + } + + keys () { + webidl.brandCheck(this, FormData) + + return makeIterator( + () => this[kState].map(pair => [pair.name, pair.value]), + 'FormData', + 'key' + ) + } + + values () { + webidl.brandCheck(this, FormData) + + return makeIterator( + () => this[kState].map(pair => [pair.name, pair.value]), + 'FormData', + 'value' + ) + } + + /** + * @param {(value: string, key: string, self: FormData) => void} callbackFn + * @param {unknown} thisArg + */ + forEach (callbackFn, thisArg = globalThis) { + webidl.brandCheck(this, FormData) + + webidl.argumentLengthCheck(arguments, 1, { header: 'FormData.forEach' }) + + if (typeof callbackFn !== 'function') { + throw new TypeError( + "Failed to execute 'forEach' on 'FormData': parameter 1 is not of type 'Function'." + ) + } + + for (const [key, value] of this) { + callbackFn.apply(thisArg, [value, key, this]) + } + } +} + +FormData.prototype[Symbol.iterator] = FormData.prototype.entries + +Object.defineProperties(FormData.prototype, { + [Symbol.toStringTag]: { + value: 'FormData', + configurable: true + } +}) + +/** + * @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#create-an-entry + * @param {string} name + * @param {string|Blob} value + * @param {?string} filename + * @returns + */ +function makeEntry (name, value, filename) { + // 1. Set name to the result of converting name into a scalar value string. + // "To convert a string into a scalar value string, replace any surrogates + // with U+FFFD." + // see: https://nodejs.org/dist/latest-v18.x/docs/api/buffer.html#buftostringencoding-start-end + name = Buffer.from(name).toString('utf8') + + // 2. If value is a string, then set value to the result of converting + // value into a scalar value string. + if (typeof value === 'string') { + value = Buffer.from(value).toString('utf8') + } else { + // 3. Otherwise: + + // 1. If value is not a File object, then set value to a new File object, + // representing the same bytes, whose name attribute value is "blob" + if (!isFileLike(value)) { + value = value instanceof Blob + ? new File([value], 'blob', { type: value.type }) + : new FileLike(value, 'blob', { type: value.type }) + } + + // 2. If filename is given, then set value to a new File object, + // representing the same bytes, whose name attribute is filename. + if (filename !== undefined) { + /** @type {FilePropertyBag} */ + const options = { + type: value.type, + lastModified: value.lastModified + } + + value = (NativeFile && value instanceof NativeFile) || value instanceof UndiciFile + ? new File([value], filename, options) + : new FileLike(value, filename, options) + } + } + + // 4. Return an entry whose name is name and whose value is value. + return { name, value } +} + +module.exports = { FormData } + + +/***/ }), + +/***/ 1246: +/***/ ((module) => { + +"use strict"; + + +// In case of breaking changes, increase the version +// number to avoid conflicts. +const globalOrigin = Symbol.for('undici.globalOrigin.1') + +function getGlobalOrigin () { + return globalThis[globalOrigin] +} + +function setGlobalOrigin (newOrigin) { + if (newOrigin === undefined) { + Object.defineProperty(globalThis, globalOrigin, { + value: undefined, + writable: true, + enumerable: false, + configurable: false + }) + + return + } + + const parsedURL = new URL(newOrigin) + + if (parsedURL.protocol !== 'http:' && parsedURL.protocol !== 'https:') { + throw new TypeError(`Only http & https urls are allowed, received ${parsedURL.protocol}`) + } + + Object.defineProperty(globalThis, globalOrigin, { + value: parsedURL, + writable: true, + enumerable: false, + configurable: false + }) +} + +module.exports = { + getGlobalOrigin, + setGlobalOrigin +} + + +/***/ }), + +/***/ 554: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; +// https://github.com/Ethan-Arrowood/undici-fetch + + + +const { kHeadersList, kConstruct } = __nccwpck_require__(2785) +const { kGuard } = __nccwpck_require__(5861) +const { kEnumerableProperty } = __nccwpck_require__(3983) +const { + makeIterator, + isValidHeaderName, + isValidHeaderValue +} = __nccwpck_require__(2538) +const { webidl } = __nccwpck_require__(1744) +const assert = __nccwpck_require__(9491) + +const kHeadersMap = Symbol('headers map') +const kHeadersSortedMap = Symbol('headers map sorted') + +/** + * @param {number} code + */ +function isHTTPWhiteSpaceCharCode (code) { + return code === 0x00a || code === 0x00d || code === 0x009 || code === 0x020 +} + +/** + * @see https://fetch.spec.whatwg.org/#concept-header-value-normalize + * @param {string} potentialValue + */ +function headerValueNormalize (potentialValue) { + // To normalize a byte sequence potentialValue, remove + // any leading and trailing HTTP whitespace bytes from + // potentialValue. + let i = 0; let j = potentialValue.length + + while (j > i && isHTTPWhiteSpaceCharCode(potentialValue.charCodeAt(j - 1))) --j + while (j > i && isHTTPWhiteSpaceCharCode(potentialValue.charCodeAt(i))) ++i + + return i === 0 && j === potentialValue.length ? potentialValue : potentialValue.substring(i, j) +} + +function fill (headers, object) { + // To fill a Headers object headers with a given object object, run these steps: + + // 1. If object is a sequence, then for each header in object: + // Note: webidl conversion to array has already been done. + if (Array.isArray(object)) { + for (let i = 0; i < object.length; ++i) { + const header = object[i] + // 1. If header does not contain exactly two items, then throw a TypeError. + if (header.length !== 2) { + throw webidl.errors.exception({ + header: 'Headers constructor', + message: `expected name/value pair to be length 2, found ${header.length}.` + }) + } + + // 2. Append (header’s first item, header’s second item) to headers. + appendHeader(headers, header[0], header[1]) + } + } else if (typeof object === 'object' && object !== null) { + // Note: null should throw + + // 2. Otherwise, object is a record, then for each key → value in object, + // append (key, value) to headers + const keys = Object.keys(object) + for (let i = 0; i < keys.length; ++i) { + appendHeader(headers, keys[i], object[keys[i]]) + } + } else { + throw webidl.errors.conversionFailed({ + prefix: 'Headers constructor', + argument: 'Argument 1', + types: ['sequence>', 'record'] + }) + } +} + +/** + * @see https://fetch.spec.whatwg.org/#concept-headers-append + */ +function appendHeader (headers, name, value) { + // 1. Normalize value. + value = headerValueNormalize(value) + + // 2. If name is not a header name or value is not a + // header value, then throw a TypeError. + if (!isValidHeaderName(name)) { + throw webidl.errors.invalidArgument({ + prefix: 'Headers.append', + value: name, + type: 'header name' + }) + } else if (!isValidHeaderValue(value)) { + throw webidl.errors.invalidArgument({ + prefix: 'Headers.append', + value, + type: 'header value' + }) + } + + // 3. If headers’s guard is "immutable", then throw a TypeError. + // 4. Otherwise, if headers’s guard is "request" and name is a + // forbidden header name, return. + // Note: undici does not implement forbidden header names + if (headers[kGuard] === 'immutable') { + throw new TypeError('immutable') + } else if (headers[kGuard] === 'request-no-cors') { + // 5. Otherwise, if headers’s guard is "request-no-cors": + // TODO + } + + // 6. Otherwise, if headers’s guard is "response" and name is a + // forbidden response-header name, return. + + // 7. Append (name, value) to headers’s header list. + return headers[kHeadersList].append(name, value) + + // 8. If headers’s guard is "request-no-cors", then remove + // privileged no-CORS request headers from headers +} + +class HeadersList { + /** @type {[string, string][]|null} */ + cookies = null + + constructor (init) { + if (init instanceof HeadersList) { + this[kHeadersMap] = new Map(init[kHeadersMap]) + this[kHeadersSortedMap] = init[kHeadersSortedMap] + this.cookies = init.cookies === null ? null : [...init.cookies] + } else { + this[kHeadersMap] = new Map(init) + this[kHeadersSortedMap] = null + } + } + + // https://fetch.spec.whatwg.org/#header-list-contains + contains (name) { + // A header list list contains a header name name if list + // contains a header whose name is a byte-case-insensitive + // match for name. + name = name.toLowerCase() + + return this[kHeadersMap].has(name) + } + + clear () { + this[kHeadersMap].clear() + this[kHeadersSortedMap] = null + this.cookies = null + } + + // https://fetch.spec.whatwg.org/#concept-header-list-append + append (name, value) { + this[kHeadersSortedMap] = null + + // 1. If list contains name, then set name to the first such + // header’s name. + const lowercaseName = name.toLowerCase() + const exists = this[kHeadersMap].get(lowercaseName) + + // 2. Append (name, value) to list. + if (exists) { + const delimiter = lowercaseName === 'cookie' ? '; ' : ', ' + this[kHeadersMap].set(lowercaseName, { + name: exists.name, + value: `${exists.value}${delimiter}${value}` + }) + } else { + this[kHeadersMap].set(lowercaseName, { name, value }) + } + + if (lowercaseName === 'set-cookie') { + this.cookies ??= [] + this.cookies.push(value) + } + } + + // https://fetch.spec.whatwg.org/#concept-header-list-set + set (name, value) { + this[kHeadersSortedMap] = null + const lowercaseName = name.toLowerCase() + + if (lowercaseName === 'set-cookie') { + this.cookies = [value] + } + + // 1. If list contains name, then set the value of + // the first such header to value and remove the + // others. + // 2. Otherwise, append header (name, value) to list. + this[kHeadersMap].set(lowercaseName, { name, value }) + } + + // https://fetch.spec.whatwg.org/#concept-header-list-delete + delete (name) { + this[kHeadersSortedMap] = null + + name = name.toLowerCase() + + if (name === 'set-cookie') { + this.cookies = null + } + + this[kHeadersMap].delete(name) + } + + // https://fetch.spec.whatwg.org/#concept-header-list-get + get (name) { + const value = this[kHeadersMap].get(name.toLowerCase()) + + // 1. If list does not contain name, then return null. + // 2. Return the values of all headers in list whose name + // is a byte-case-insensitive match for name, + // separated from each other by 0x2C 0x20, in order. + return value === undefined ? null : value.value + } + + * [Symbol.iterator] () { + // use the lowercased name + for (const [name, { value }] of this[kHeadersMap]) { + yield [name, value] + } + } + + get entries () { + const headers = {} + + if (this[kHeadersMap].size) { + for (const { name, value } of this[kHeadersMap].values()) { + headers[name] = value + } + } + + return headers + } +} + +// https://fetch.spec.whatwg.org/#headers-class +class Headers { + constructor (init = undefined) { + if (init === kConstruct) { + return + } + this[kHeadersList] = new HeadersList() + + // The new Headers(init) constructor steps are: + + // 1. Set this’s guard to "none". + this[kGuard] = 'none' + + // 2. If init is given, then fill this with init. + if (init !== undefined) { + init = webidl.converters.HeadersInit(init) + fill(this, init) + } + } + + // https://fetch.spec.whatwg.org/#dom-headers-append + append (name, value) { + webidl.brandCheck(this, Headers) + + webidl.argumentLengthCheck(arguments, 2, { header: 'Headers.append' }) + + name = webidl.converters.ByteString(name) + value = webidl.converters.ByteString(value) + + return appendHeader(this, name, value) + } + + // https://fetch.spec.whatwg.org/#dom-headers-delete + delete (name) { + webidl.brandCheck(this, Headers) + + webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.delete' }) + + name = webidl.converters.ByteString(name) + + // 1. If name is not a header name, then throw a TypeError. + if (!isValidHeaderName(name)) { + throw webidl.errors.invalidArgument({ + prefix: 'Headers.delete', + value: name, + type: 'header name' + }) + } + + // 2. If this’s guard is "immutable", then throw a TypeError. + // 3. Otherwise, if this’s guard is "request" and name is a + // forbidden header name, return. + // 4. Otherwise, if this’s guard is "request-no-cors", name + // is not a no-CORS-safelisted request-header name, and + // name is not a privileged no-CORS request-header name, + // return. + // 5. Otherwise, if this’s guard is "response" and name is + // a forbidden response-header name, return. + // Note: undici does not implement forbidden header names + if (this[kGuard] === 'immutable') { + throw new TypeError('immutable') + } else if (this[kGuard] === 'request-no-cors') { + // TODO + } + + // 6. If this’s header list does not contain name, then + // return. + if (!this[kHeadersList].contains(name)) { + return + } + + // 7. Delete name from this’s header list. + // 8. If this’s guard is "request-no-cors", then remove + // privileged no-CORS request headers from this. + this[kHeadersList].delete(name) + } + + // https://fetch.spec.whatwg.org/#dom-headers-get + get (name) { + webidl.brandCheck(this, Headers) + + webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.get' }) + + name = webidl.converters.ByteString(name) + + // 1. If name is not a header name, then throw a TypeError. + if (!isValidHeaderName(name)) { + throw webidl.errors.invalidArgument({ + prefix: 'Headers.get', + value: name, + type: 'header name' + }) + } + + // 2. Return the result of getting name from this’s header + // list. + return this[kHeadersList].get(name) + } + + // https://fetch.spec.whatwg.org/#dom-headers-has + has (name) { + webidl.brandCheck(this, Headers) + + webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.has' }) + + name = webidl.converters.ByteString(name) + + // 1. If name is not a header name, then throw a TypeError. + if (!isValidHeaderName(name)) { + throw webidl.errors.invalidArgument({ + prefix: 'Headers.has', + value: name, + type: 'header name' + }) + } + + // 2. Return true if this’s header list contains name; + // otherwise false. + return this[kHeadersList].contains(name) + } + + // https://fetch.spec.whatwg.org/#dom-headers-set + set (name, value) { + webidl.brandCheck(this, Headers) + + webidl.argumentLengthCheck(arguments, 2, { header: 'Headers.set' }) + + name = webidl.converters.ByteString(name) + value = webidl.converters.ByteString(value) + + // 1. Normalize value. + value = headerValueNormalize(value) + + // 2. If name is not a header name or value is not a + // header value, then throw a TypeError. + if (!isValidHeaderName(name)) { + throw webidl.errors.invalidArgument({ + prefix: 'Headers.set', + value: name, + type: 'header name' + }) + } else if (!isValidHeaderValue(value)) { + throw webidl.errors.invalidArgument({ + prefix: 'Headers.set', + value, + type: 'header value' + }) + } + + // 3. If this’s guard is "immutable", then throw a TypeError. + // 4. Otherwise, if this’s guard is "request" and name is a + // forbidden header name, return. + // 5. Otherwise, if this’s guard is "request-no-cors" and + // name/value is not a no-CORS-safelisted request-header, + // return. + // 6. Otherwise, if this’s guard is "response" and name is a + // forbidden response-header name, return. + // Note: undici does not implement forbidden header names + if (this[kGuard] === 'immutable') { + throw new TypeError('immutable') + } else if (this[kGuard] === 'request-no-cors') { + // TODO + } + + // 7. Set (name, value) in this’s header list. + // 8. If this’s guard is "request-no-cors", then remove + // privileged no-CORS request headers from this + this[kHeadersList].set(name, value) + } + + // https://fetch.spec.whatwg.org/#dom-headers-getsetcookie + getSetCookie () { + webidl.brandCheck(this, Headers) + + // 1. If this’s header list does not contain `Set-Cookie`, then return « ». + // 2. Return the values of all headers in this’s header list whose name is + // a byte-case-insensitive match for `Set-Cookie`, in order. + + const list = this[kHeadersList].cookies + + if (list) { + return [...list] + } + + return [] + } + + // https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine + get [kHeadersSortedMap] () { + if (this[kHeadersList][kHeadersSortedMap]) { + return this[kHeadersList][kHeadersSortedMap] + } + + // 1. Let headers be an empty list of headers with the key being the name + // and value the value. + const headers = [] + + // 2. Let names be the result of convert header names to a sorted-lowercase + // set with all the names of the headers in list. + const names = [...this[kHeadersList]].sort((a, b) => a[0] < b[0] ? -1 : 1) + const cookies = this[kHeadersList].cookies + + // 3. For each name of names: + for (let i = 0; i < names.length; ++i) { + const [name, value] = names[i] + // 1. If name is `set-cookie`, then: + if (name === 'set-cookie') { + // 1. Let values be a list of all values of headers in list whose name + // is a byte-case-insensitive match for name, in order. + + // 2. For each value of values: + // 1. Append (name, value) to headers. + for (let j = 0; j < cookies.length; ++j) { + headers.push([name, cookies[j]]) + } + } else { + // 2. Otherwise: + + // 1. Let value be the result of getting name from list. + + // 2. Assert: value is non-null. + assert(value !== null) + + // 3. Append (name, value) to headers. + headers.push([name, value]) + } + } + + this[kHeadersList][kHeadersSortedMap] = headers + + // 4. Return headers. + return headers + } + + keys () { + webidl.brandCheck(this, Headers) + + if (this[kGuard] === 'immutable') { + const value = this[kHeadersSortedMap] + return makeIterator(() => value, 'Headers', + 'key') + } + + return makeIterator( + () => [...this[kHeadersSortedMap].values()], + 'Headers', + 'key' + ) + } + + values () { + webidl.brandCheck(this, Headers) + + if (this[kGuard] === 'immutable') { + const value = this[kHeadersSortedMap] + return makeIterator(() => value, 'Headers', + 'value') + } + + return makeIterator( + () => [...this[kHeadersSortedMap].values()], + 'Headers', + 'value' + ) + } + + entries () { + webidl.brandCheck(this, Headers) + + if (this[kGuard] === 'immutable') { + const value = this[kHeadersSortedMap] + return makeIterator(() => value, 'Headers', + 'key+value') + } + + return makeIterator( + () => [...this[kHeadersSortedMap].values()], + 'Headers', + 'key+value' + ) + } + + /** + * @param {(value: string, key: string, self: Headers) => void} callbackFn + * @param {unknown} thisArg + */ + forEach (callbackFn, thisArg = globalThis) { + webidl.brandCheck(this, Headers) + + webidl.argumentLengthCheck(arguments, 1, { header: 'Headers.forEach' }) + + if (typeof callbackFn !== 'function') { + throw new TypeError( + "Failed to execute 'forEach' on 'Headers': parameter 1 is not of type 'Function'." + ) + } + + for (const [key, value] of this) { + callbackFn.apply(thisArg, [value, key, this]) + } + } + + [Symbol.for('nodejs.util.inspect.custom')] () { + webidl.brandCheck(this, Headers) + + return this[kHeadersList] + } +} + +Headers.prototype[Symbol.iterator] = Headers.prototype.entries + +Object.defineProperties(Headers.prototype, { + append: kEnumerableProperty, + delete: kEnumerableProperty, + get: kEnumerableProperty, + has: kEnumerableProperty, + set: kEnumerableProperty, + getSetCookie: kEnumerableProperty, + keys: kEnumerableProperty, + values: kEnumerableProperty, + entries: kEnumerableProperty, + forEach: kEnumerableProperty, + [Symbol.iterator]: { enumerable: false }, + [Symbol.toStringTag]: { + value: 'Headers', + configurable: true + } +}) + +webidl.converters.HeadersInit = function (V) { + if (webidl.util.Type(V) === 'Object') { + if (V[Symbol.iterator]) { + return webidl.converters['sequence>'](V) + } + + return webidl.converters['record'](V) + } + + throw webidl.errors.conversionFailed({ + prefix: 'Headers constructor', + argument: 'Argument 1', + types: ['sequence>', 'record'] + }) +} + +module.exports = { + fill, + Headers, + HeadersList +} + + +/***/ }), + +/***/ 4881: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; +// https://github.com/Ethan-Arrowood/undici-fetch + + + +const { + Response, + makeNetworkError, + makeAppropriateNetworkError, + filterResponse, + makeResponse +} = __nccwpck_require__(7823) +const { Headers } = __nccwpck_require__(554) +const { Request, makeRequest } = __nccwpck_require__(8359) +const zlib = __nccwpck_require__(9796) +const { + bytesMatch, + makePolicyContainer, + clonePolicyContainer, + requestBadPort, + TAOCheck, + appendRequestOriginHeader, + responseLocationURL, + requestCurrentURL, + setRequestReferrerPolicyOnRedirect, + tryUpgradeRequestToAPotentiallyTrustworthyURL, + createOpaqueTimingInfo, + appendFetchMetadata, + corsCheck, + crossOriginResourcePolicyCheck, + determineRequestsReferrer, + coarsenedSharedCurrentTime, + createDeferredPromise, + isBlobLike, + sameOrigin, + isCancelled, + isAborted, + isErrorLike, + fullyReadBody, + readableStreamClose, + isomorphicEncode, + urlIsLocal, + urlIsHttpHttpsScheme, + urlHasHttpsScheme +} = __nccwpck_require__(2538) +const { kState, kHeaders, kGuard, kRealm } = __nccwpck_require__(5861) +const assert = __nccwpck_require__(9491) +const { safelyExtractBody } = __nccwpck_require__(1472) +const { + redirectStatusSet, + nullBodyStatus, + safeMethodsSet, + requestBodyHeader, + subresourceSet, + DOMException +} = __nccwpck_require__(1037) +const { kHeadersList } = __nccwpck_require__(2785) +const EE = __nccwpck_require__(2361) +const { Readable, pipeline } = __nccwpck_require__(2781) +const { addAbortListener, isErrored, isReadable, nodeMajor, nodeMinor } = __nccwpck_require__(3983) +const { dataURLProcessor, serializeAMimeType } = __nccwpck_require__(685) +const { TransformStream } = __nccwpck_require__(5356) +const { getGlobalDispatcher } = __nccwpck_require__(1892) +const { webidl } = __nccwpck_require__(1744) +const { STATUS_CODES } = __nccwpck_require__(3685) +const GET_OR_HEAD = ['GET', 'HEAD'] + +/** @type {import('buffer').resolveObjectURL} */ +let resolveObjectURL +let ReadableStream = globalThis.ReadableStream + +class Fetch extends EE { + constructor (dispatcher) { + super() + + this.dispatcher = dispatcher + this.connection = null + this.dump = false + this.state = 'ongoing' + // 2 terminated listeners get added per request, + // but only 1 gets removed. If there are 20 redirects, + // 21 listeners will be added. + // See https://github.com/nodejs/undici/issues/1711 + // TODO (fix): Find and fix root cause for leaked listener. + this.setMaxListeners(21) + } + + terminate (reason) { + if (this.state !== 'ongoing') { + return + } + + this.state = 'terminated' + this.connection?.destroy(reason) + this.emit('terminated', reason) + } + + // https://fetch.spec.whatwg.org/#fetch-controller-abort + abort (error) { + if (this.state !== 'ongoing') { + return + } + + // 1. Set controller’s state to "aborted". + this.state = 'aborted' + + // 2. Let fallbackError be an "AbortError" DOMException. + // 3. Set error to fallbackError if it is not given. + if (!error) { + error = new DOMException('The operation was aborted.', 'AbortError') + } + + // 4. Let serializedError be StructuredSerialize(error). + // If that threw an exception, catch it, and let + // serializedError be StructuredSerialize(fallbackError). + + // 5. Set controller’s serialized abort reason to serializedError. + this.serializedAbortReason = error + + this.connection?.destroy(error) + this.emit('terminated', error) + } +} + +// https://fetch.spec.whatwg.org/#fetch-method +function fetch (input, init = {}) { + webidl.argumentLengthCheck(arguments, 1, { header: 'globalThis.fetch' }) + + // 1. Let p be a new promise. + const p = createDeferredPromise() + + // 2. Let requestObject be the result of invoking the initial value of + // Request as constructor with input and init as arguments. If this throws + // an exception, reject p with it and return p. + let requestObject + + try { + requestObject = new Request(input, init) + } catch (e) { + p.reject(e) + return p.promise + } + + // 3. Let request be requestObject’s request. + const request = requestObject[kState] + + // 4. If requestObject’s signal’s aborted flag is set, then: + if (requestObject.signal.aborted) { + // 1. Abort the fetch() call with p, request, null, and + // requestObject’s signal’s abort reason. + abortFetch(p, request, null, requestObject.signal.reason) + + // 2. Return p. + return p.promise + } + + // 5. Let globalObject be request’s client’s global object. + const globalObject = request.client.globalObject + + // 6. If globalObject is a ServiceWorkerGlobalScope object, then set + // request’s service-workers mode to "none". + if (globalObject?.constructor?.name === 'ServiceWorkerGlobalScope') { + request.serviceWorkers = 'none' + } + + // 7. Let responseObject be null. + let responseObject = null + + // 8. Let relevantRealm be this’s relevant Realm. + const relevantRealm = null + + // 9. Let locallyAborted be false. + let locallyAborted = false + + // 10. Let controller be null. + let controller = null + + // 11. Add the following abort steps to requestObject’s signal: + addAbortListener( + requestObject.signal, + () => { + // 1. Set locallyAborted to true. + locallyAborted = true + + // 2. Assert: controller is non-null. + assert(controller != null) + + // 3. Abort controller with requestObject’s signal’s abort reason. + controller.abort(requestObject.signal.reason) + + // 4. Abort the fetch() call with p, request, responseObject, + // and requestObject’s signal’s abort reason. + abortFetch(p, request, responseObject, requestObject.signal.reason) + } + ) + + // 12. Let handleFetchDone given response response be to finalize and + // report timing with response, globalObject, and "fetch". + const handleFetchDone = (response) => + finalizeAndReportTiming(response, 'fetch') + + // 13. Set controller to the result of calling fetch given request, + // with processResponseEndOfBody set to handleFetchDone, and processResponse + // given response being these substeps: + + const processResponse = (response) => { + // 1. If locallyAborted is true, terminate these substeps. + if (locallyAborted) { + return Promise.resolve() + } + + // 2. If response’s aborted flag is set, then: + if (response.aborted) { + // 1. Let deserializedError be the result of deserialize a serialized + // abort reason given controller’s serialized abort reason and + // relevantRealm. + + // 2. Abort the fetch() call with p, request, responseObject, and + // deserializedError. + + abortFetch(p, request, responseObject, controller.serializedAbortReason) + return Promise.resolve() + } + + // 3. If response is a network error, then reject p with a TypeError + // and terminate these substeps. + if (response.type === 'error') { + p.reject( + Object.assign(new TypeError('fetch failed'), { cause: response.error }) + ) + return Promise.resolve() + } + + // 4. Set responseObject to the result of creating a Response object, + // given response, "immutable", and relevantRealm. + responseObject = new Response() + responseObject[kState] = response + responseObject[kRealm] = relevantRealm + responseObject[kHeaders][kHeadersList] = response.headersList + responseObject[kHeaders][kGuard] = 'immutable' + responseObject[kHeaders][kRealm] = relevantRealm + + // 5. Resolve p with responseObject. + p.resolve(responseObject) + } + + controller = fetching({ + request, + processResponseEndOfBody: handleFetchDone, + processResponse, + dispatcher: init.dispatcher ?? getGlobalDispatcher() // undici + }) + + // 14. Return p. + return p.promise +} + +// https://fetch.spec.whatwg.org/#finalize-and-report-timing +function finalizeAndReportTiming (response, initiatorType = 'other') { + // 1. If response is an aborted network error, then return. + if (response.type === 'error' && response.aborted) { + return + } + + // 2. If response’s URL list is null or empty, then return. + if (!response.urlList?.length) { + return + } + + // 3. Let originalURL be response’s URL list[0]. + const originalURL = response.urlList[0] + + // 4. Let timingInfo be response’s timing info. + let timingInfo = response.timingInfo + + // 5. Let cacheState be response’s cache state. + let cacheState = response.cacheState + + // 6. If originalURL’s scheme is not an HTTP(S) scheme, then return. + if (!urlIsHttpHttpsScheme(originalURL)) { + return + } + + // 7. If timingInfo is null, then return. + if (timingInfo === null) { + return + } + + // 8. If response’s timing allow passed flag is not set, then: + if (!response.timingAllowPassed) { + // 1. Set timingInfo to a the result of creating an opaque timing info for timingInfo. + timingInfo = createOpaqueTimingInfo({ + startTime: timingInfo.startTime + }) + + // 2. Set cacheState to the empty string. + cacheState = '' + } + + // 9. Set timingInfo’s end time to the coarsened shared current time + // given global’s relevant settings object’s cross-origin isolated + // capability. + // TODO: given global’s relevant settings object’s cross-origin isolated + // capability? + timingInfo.endTime = coarsenedSharedCurrentTime() + + // 10. Set response’s timing info to timingInfo. + response.timingInfo = timingInfo + + // 11. Mark resource timing for timingInfo, originalURL, initiatorType, + // global, and cacheState. + markResourceTiming( + timingInfo, + originalURL, + initiatorType, + globalThis, + cacheState + ) +} + +// https://w3c.github.io/resource-timing/#dfn-mark-resource-timing +function markResourceTiming (timingInfo, originalURL, initiatorType, globalThis, cacheState) { + if (nodeMajor > 18 || (nodeMajor === 18 && nodeMinor >= 2)) { + performance.markResourceTiming(timingInfo, originalURL.href, initiatorType, globalThis, cacheState) + } +} + +// https://fetch.spec.whatwg.org/#abort-fetch +function abortFetch (p, request, responseObject, error) { + // Note: AbortSignal.reason was added in node v17.2.0 + // which would give us an undefined error to reject with. + // Remove this once node v16 is no longer supported. + if (!error) { + error = new DOMException('The operation was aborted.', 'AbortError') + } + + // 1. Reject promise with error. + p.reject(error) + + // 2. If request’s body is not null and is readable, then cancel request’s + // body with error. + if (request.body != null && isReadable(request.body?.stream)) { + request.body.stream.cancel(error).catch((err) => { + if (err.code === 'ERR_INVALID_STATE') { + // Node bug? + return + } + throw err + }) + } + + // 3. If responseObject is null, then return. + if (responseObject == null) { + return + } + + // 4. Let response be responseObject’s response. + const response = responseObject[kState] + + // 5. If response’s body is not null and is readable, then error response’s + // body with error. + if (response.body != null && isReadable(response.body?.stream)) { + response.body.stream.cancel(error).catch((err) => { + if (err.code === 'ERR_INVALID_STATE') { + // Node bug? + return + } + throw err + }) + } +} + +// https://fetch.spec.whatwg.org/#fetching +function fetching ({ + request, + processRequestBodyChunkLength, + processRequestEndOfBody, + processResponse, + processResponseEndOfBody, + processResponseConsumeBody, + useParallelQueue = false, + dispatcher // undici +}) { + // 1. Let taskDestination be null. + let taskDestination = null + + // 2. Let crossOriginIsolatedCapability be false. + let crossOriginIsolatedCapability = false + + // 3. If request’s client is non-null, then: + if (request.client != null) { + // 1. Set taskDestination to request’s client’s global object. + taskDestination = request.client.globalObject + + // 2. Set crossOriginIsolatedCapability to request’s client’s cross-origin + // isolated capability. + crossOriginIsolatedCapability = + request.client.crossOriginIsolatedCapability + } + + // 4. If useParallelQueue is true, then set taskDestination to the result of + // starting a new parallel queue. + // TODO + + // 5. Let timingInfo be a new fetch timing info whose start time and + // post-redirect start time are the coarsened shared current time given + // crossOriginIsolatedCapability. + const currenTime = coarsenedSharedCurrentTime(crossOriginIsolatedCapability) + const timingInfo = createOpaqueTimingInfo({ + startTime: currenTime + }) + + // 6. Let fetchParams be a new fetch params whose + // request is request, + // timing info is timingInfo, + // process request body chunk length is processRequestBodyChunkLength, + // process request end-of-body is processRequestEndOfBody, + // process response is processResponse, + // process response consume body is processResponseConsumeBody, + // process response end-of-body is processResponseEndOfBody, + // task destination is taskDestination, + // and cross-origin isolated capability is crossOriginIsolatedCapability. + const fetchParams = { + controller: new Fetch(dispatcher), + request, + timingInfo, + processRequestBodyChunkLength, + processRequestEndOfBody, + processResponse, + processResponseConsumeBody, + processResponseEndOfBody, + taskDestination, + crossOriginIsolatedCapability + } + + // 7. If request’s body is a byte sequence, then set request’s body to + // request’s body as a body. + // NOTE: Since fetching is only called from fetch, body should already be + // extracted. + assert(!request.body || request.body.stream) + + // 8. If request’s window is "client", then set request’s window to request’s + // client, if request’s client’s global object is a Window object; otherwise + // "no-window". + if (request.window === 'client') { + // TODO: What if request.client is null? + request.window = + request.client?.globalObject?.constructor?.name === 'Window' + ? request.client + : 'no-window' + } + + // 9. If request’s origin is "client", then set request’s origin to request’s + // client’s origin. + if (request.origin === 'client') { + // TODO: What if request.client is null? + request.origin = request.client?.origin + } + + // 10. If all of the following conditions are true: + // TODO + + // 11. If request’s policy container is "client", then: + if (request.policyContainer === 'client') { + // 1. If request’s client is non-null, then set request’s policy + // container to a clone of request’s client’s policy container. [HTML] + if (request.client != null) { + request.policyContainer = clonePolicyContainer( + request.client.policyContainer + ) + } else { + // 2. Otherwise, set request’s policy container to a new policy + // container. + request.policyContainer = makePolicyContainer() + } + } + + // 12. If request’s header list does not contain `Accept`, then: + if (!request.headersList.contains('accept')) { + // 1. Let value be `*/*`. + const value = '*/*' + + // 2. A user agent should set value to the first matching statement, if + // any, switching on request’s destination: + // "document" + // "frame" + // "iframe" + // `text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8` + // "image" + // `image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5` + // "style" + // `text/css,*/*;q=0.1` + // TODO + + // 3. Append `Accept`/value to request’s header list. + request.headersList.append('accept', value) + } + + // 13. If request’s header list does not contain `Accept-Language`, then + // user agents should append `Accept-Language`/an appropriate value to + // request’s header list. + if (!request.headersList.contains('accept-language')) { + request.headersList.append('accept-language', '*') + } + + // 14. If request’s priority is null, then use request’s initiator and + // destination appropriately in setting request’s priority to a + // user-agent-defined object. + if (request.priority === null) { + // TODO + } + + // 15. If request is a subresource request, then: + if (subresourceSet.has(request.destination)) { + // TODO + } + + // 16. Run main fetch given fetchParams. + mainFetch(fetchParams) + .catch(err => { + fetchParams.controller.terminate(err) + }) + + // 17. Return fetchParam's controller + return fetchParams.controller +} + +// https://fetch.spec.whatwg.org/#concept-main-fetch +async function mainFetch (fetchParams, recursive = false) { + // 1. Let request be fetchParams’s request. + const request = fetchParams.request + + // 2. Let response be null. + let response = null + + // 3. If request’s local-URLs-only flag is set and request’s current URL is + // not local, then set response to a network error. + if (request.localURLsOnly && !urlIsLocal(requestCurrentURL(request))) { + response = makeNetworkError('local URLs only') + } + + // 4. Run report Content Security Policy violations for request. + // TODO + + // 5. Upgrade request to a potentially trustworthy URL, if appropriate. + tryUpgradeRequestToAPotentiallyTrustworthyURL(request) + + // 6. If should request be blocked due to a bad port, should fetching request + // be blocked as mixed content, or should request be blocked by Content + // Security Policy returns blocked, then set response to a network error. + if (requestBadPort(request) === 'blocked') { + response = makeNetworkError('bad port') + } + // TODO: should fetching request be blocked as mixed content? + // TODO: should request be blocked by Content Security Policy? + + // 7. If request’s referrer policy is the empty string, then set request’s + // referrer policy to request’s policy container’s referrer policy. + if (request.referrerPolicy === '') { + request.referrerPolicy = request.policyContainer.referrerPolicy + } + + // 8. If request’s referrer is not "no-referrer", then set request’s + // referrer to the result of invoking determine request’s referrer. + if (request.referrer !== 'no-referrer') { + request.referrer = determineRequestsReferrer(request) + } + + // 9. Set request’s current URL’s scheme to "https" if all of the following + // conditions are true: + // - request’s current URL’s scheme is "http" + // - request’s current URL’s host is a domain + // - Matching request’s current URL’s host per Known HSTS Host Domain Name + // Matching results in either a superdomain match with an asserted + // includeSubDomains directive or a congruent match (with or without an + // asserted includeSubDomains directive). [HSTS] + // TODO + + // 10. If recursive is false, then run the remaining steps in parallel. + // TODO + + // 11. If response is null, then set response to the result of running + // the steps corresponding to the first matching statement: + if (response === null) { + response = await (async () => { + const currentURL = requestCurrentURL(request) + + if ( + // - request’s current URL’s origin is same origin with request’s origin, + // and request’s response tainting is "basic" + (sameOrigin(currentURL, request.url) && request.responseTainting === 'basic') || + // request’s current URL’s scheme is "data" + (currentURL.protocol === 'data:') || + // - request’s mode is "navigate" or "websocket" + (request.mode === 'navigate' || request.mode === 'websocket') + ) { + // 1. Set request’s response tainting to "basic". + request.responseTainting = 'basic' + + // 2. Return the result of running scheme fetch given fetchParams. + return await schemeFetch(fetchParams) + } + + // request’s mode is "same-origin" + if (request.mode === 'same-origin') { + // 1. Return a network error. + return makeNetworkError('request mode cannot be "same-origin"') + } + + // request’s mode is "no-cors" + if (request.mode === 'no-cors') { + // 1. If request’s redirect mode is not "follow", then return a network + // error. + if (request.redirect !== 'follow') { + return makeNetworkError( + 'redirect mode cannot be "follow" for "no-cors" request' + ) + } + + // 2. Set request’s response tainting to "opaque". + request.responseTainting = 'opaque' + + // 3. Return the result of running scheme fetch given fetchParams. + return await schemeFetch(fetchParams) + } + + // request’s current URL’s scheme is not an HTTP(S) scheme + if (!urlIsHttpHttpsScheme(requestCurrentURL(request))) { + // Return a network error. + return makeNetworkError('URL scheme must be a HTTP(S) scheme') + } + + // - request’s use-CORS-preflight flag is set + // - request’s unsafe-request flag is set and either request’s method is + // not a CORS-safelisted method or CORS-unsafe request-header names with + // request’s header list is not empty + // 1. Set request’s response tainting to "cors". + // 2. Let corsWithPreflightResponse be the result of running HTTP fetch + // given fetchParams and true. + // 3. If corsWithPreflightResponse is a network error, then clear cache + // entries using request. + // 4. Return corsWithPreflightResponse. + // TODO + + // Otherwise + // 1. Set request’s response tainting to "cors". + request.responseTainting = 'cors' + + // 2. Return the result of running HTTP fetch given fetchParams. + return await httpFetch(fetchParams) + })() + } + + // 12. If recursive is true, then return response. + if (recursive) { + return response + } + + // 13. If response is not a network error and response is not a filtered + // response, then: + if (response.status !== 0 && !response.internalResponse) { + // If request’s response tainting is "cors", then: + if (request.responseTainting === 'cors') { + // 1. Let headerNames be the result of extracting header list values + // given `Access-Control-Expose-Headers` and response’s header list. + // TODO + // 2. If request’s credentials mode is not "include" and headerNames + // contains `*`, then set response’s CORS-exposed header-name list to + // all unique header names in response’s header list. + // TODO + // 3. Otherwise, if headerNames is not null or failure, then set + // response’s CORS-exposed header-name list to headerNames. + // TODO + } + + // Set response to the following filtered response with response as its + // internal response, depending on request’s response tainting: + if (request.responseTainting === 'basic') { + response = filterResponse(response, 'basic') + } else if (request.responseTainting === 'cors') { + response = filterResponse(response, 'cors') + } else if (request.responseTainting === 'opaque') { + response = filterResponse(response, 'opaque') + } else { + assert(false) + } + } + + // 14. Let internalResponse be response, if response is a network error, + // and response’s internal response otherwise. + let internalResponse = + response.status === 0 ? response : response.internalResponse + + // 15. If internalResponse’s URL list is empty, then set it to a clone of + // request’s URL list. + if (internalResponse.urlList.length === 0) { + internalResponse.urlList.push(...request.urlList) + } + + // 16. If request’s timing allow failed flag is unset, then set + // internalResponse’s timing allow passed flag. + if (!request.timingAllowFailed) { + response.timingAllowPassed = true + } + + // 17. If response is not a network error and any of the following returns + // blocked + // - should internalResponse to request be blocked as mixed content + // - should internalResponse to request be blocked by Content Security Policy + // - should internalResponse to request be blocked due to its MIME type + // - should internalResponse to request be blocked due to nosniff + // TODO + + // 18. If response’s type is "opaque", internalResponse’s status is 206, + // internalResponse’s range-requested flag is set, and request’s header + // list does not contain `Range`, then set response and internalResponse + // to a network error. + if ( + response.type === 'opaque' && + internalResponse.status === 206 && + internalResponse.rangeRequested && + !request.headers.contains('range') + ) { + response = internalResponse = makeNetworkError() + } + + // 19. If response is not a network error and either request’s method is + // `HEAD` or `CONNECT`, or internalResponse’s status is a null body status, + // set internalResponse’s body to null and disregard any enqueuing toward + // it (if any). + if ( + response.status !== 0 && + (request.method === 'HEAD' || + request.method === 'CONNECT' || + nullBodyStatus.includes(internalResponse.status)) + ) { + internalResponse.body = null + fetchParams.controller.dump = true + } + + // 20. If request’s integrity metadata is not the empty string, then: + if (request.integrity) { + // 1. Let processBodyError be this step: run fetch finale given fetchParams + // and a network error. + const processBodyError = (reason) => + fetchFinale(fetchParams, makeNetworkError(reason)) + + // 2. If request’s response tainting is "opaque", or response’s body is null, + // then run processBodyError and abort these steps. + if (request.responseTainting === 'opaque' || response.body == null) { + processBodyError(response.error) + return + } + + // 3. Let processBody given bytes be these steps: + const processBody = (bytes) => { + // 1. If bytes do not match request’s integrity metadata, + // then run processBodyError and abort these steps. [SRI] + if (!bytesMatch(bytes, request.integrity)) { + processBodyError('integrity mismatch') + return + } + + // 2. Set response’s body to bytes as a body. + response.body = safelyExtractBody(bytes)[0] + + // 3. Run fetch finale given fetchParams and response. + fetchFinale(fetchParams, response) + } + + // 4. Fully read response’s body given processBody and processBodyError. + await fullyReadBody(response.body, processBody, processBodyError) + } else { + // 21. Otherwise, run fetch finale given fetchParams and response. + fetchFinale(fetchParams, response) + } +} + +// https://fetch.spec.whatwg.org/#concept-scheme-fetch +// given a fetch params fetchParams +function schemeFetch (fetchParams) { + // Note: since the connection is destroyed on redirect, which sets fetchParams to a + // cancelled state, we do not want this condition to trigger *unless* there have been + // no redirects. See https://github.com/nodejs/undici/issues/1776 + // 1. If fetchParams is canceled, then return the appropriate network error for fetchParams. + if (isCancelled(fetchParams) && fetchParams.request.redirectCount === 0) { + return Promise.resolve(makeAppropriateNetworkError(fetchParams)) + } + + // 2. Let request be fetchParams’s request. + const { request } = fetchParams + + const { protocol: scheme } = requestCurrentURL(request) + + // 3. Switch on request’s current URL’s scheme and run the associated steps: + switch (scheme) { + case 'about:': { + // If request’s current URL’s path is the string "blank", then return a new response + // whose status message is `OK`, header list is « (`Content-Type`, `text/html;charset=utf-8`) », + // and body is the empty byte sequence as a body. + + // Otherwise, return a network error. + return Promise.resolve(makeNetworkError('about scheme is not supported')) + } + case 'blob:': { + if (!resolveObjectURL) { + resolveObjectURL = (__nccwpck_require__(4300).resolveObjectURL) + } + + // 1. Let blobURLEntry be request’s current URL’s blob URL entry. + const blobURLEntry = requestCurrentURL(request) + + // https://github.com/web-platform-tests/wpt/blob/7b0ebaccc62b566a1965396e5be7bb2bc06f841f/FileAPI/url/resources/fetch-tests.js#L52-L56 + // Buffer.resolveObjectURL does not ignore URL queries. + if (blobURLEntry.search.length !== 0) { + return Promise.resolve(makeNetworkError('NetworkError when attempting to fetch resource.')) + } + + const blobURLEntryObject = resolveObjectURL(blobURLEntry.toString()) + + // 2. If request’s method is not `GET`, blobURLEntry is null, or blobURLEntry’s + // object is not a Blob object, then return a network error. + if (request.method !== 'GET' || !isBlobLike(blobURLEntryObject)) { + return Promise.resolve(makeNetworkError('invalid method')) + } + + // 3. Let bodyWithType be the result of safely extracting blobURLEntry’s object. + const bodyWithType = safelyExtractBody(blobURLEntryObject) + + // 4. Let body be bodyWithType’s body. + const body = bodyWithType[0] + + // 5. Let length be body’s length, serialized and isomorphic encoded. + const length = isomorphicEncode(`${body.length}`) + + // 6. Let type be bodyWithType’s type if it is non-null; otherwise the empty byte sequence. + const type = bodyWithType[1] ?? '' + + // 7. Return a new response whose status message is `OK`, header list is + // « (`Content-Length`, length), (`Content-Type`, type) », and body is body. + const response = makeResponse({ + statusText: 'OK', + headersList: [ + ['content-length', { name: 'Content-Length', value: length }], + ['content-type', { name: 'Content-Type', value: type }] + ] + }) + + response.body = body + + return Promise.resolve(response) + } + case 'data:': { + // 1. Let dataURLStruct be the result of running the + // data: URL processor on request’s current URL. + const currentURL = requestCurrentURL(request) + const dataURLStruct = dataURLProcessor(currentURL) + + // 2. If dataURLStruct is failure, then return a + // network error. + if (dataURLStruct === 'failure') { + return Promise.resolve(makeNetworkError('failed to fetch the data URL')) + } + + // 3. Let mimeType be dataURLStruct’s MIME type, serialized. + const mimeType = serializeAMimeType(dataURLStruct.mimeType) + + // 4. Return a response whose status message is `OK`, + // header list is « (`Content-Type`, mimeType) », + // and body is dataURLStruct’s body as a body. + return Promise.resolve(makeResponse({ + statusText: 'OK', + headersList: [ + ['content-type', { name: 'Content-Type', value: mimeType }] + ], + body: safelyExtractBody(dataURLStruct.body)[0] + })) + } + case 'file:': { + // For now, unfortunate as it is, file URLs are left as an exercise for the reader. + // When in doubt, return a network error. + return Promise.resolve(makeNetworkError('not implemented... yet...')) + } + case 'http:': + case 'https:': { + // Return the result of running HTTP fetch given fetchParams. + + return httpFetch(fetchParams) + .catch((err) => makeNetworkError(err)) + } + default: { + return Promise.resolve(makeNetworkError('unknown scheme')) + } + } +} + +// https://fetch.spec.whatwg.org/#finalize-response +function finalizeResponse (fetchParams, response) { + // 1. Set fetchParams’s request’s done flag. + fetchParams.request.done = true + + // 2, If fetchParams’s process response done is not null, then queue a fetch + // task to run fetchParams’s process response done given response, with + // fetchParams’s task destination. + if (fetchParams.processResponseDone != null) { + queueMicrotask(() => fetchParams.processResponseDone(response)) + } +} + +// https://fetch.spec.whatwg.org/#fetch-finale +function fetchFinale (fetchParams, response) { + // 1. If response is a network error, then: + if (response.type === 'error') { + // 1. Set response’s URL list to « fetchParams’s request’s URL list[0] ». + response.urlList = [fetchParams.request.urlList[0]] + + // 2. Set response’s timing info to the result of creating an opaque timing + // info for fetchParams’s timing info. + response.timingInfo = createOpaqueTimingInfo({ + startTime: fetchParams.timingInfo.startTime + }) + } + + // 2. Let processResponseEndOfBody be the following steps: + const processResponseEndOfBody = () => { + // 1. Set fetchParams’s request’s done flag. + fetchParams.request.done = true + + // If fetchParams’s process response end-of-body is not null, + // then queue a fetch task to run fetchParams’s process response + // end-of-body given response with fetchParams’s task destination. + if (fetchParams.processResponseEndOfBody != null) { + queueMicrotask(() => fetchParams.processResponseEndOfBody(response)) + } + } + + // 3. If fetchParams’s process response is non-null, then queue a fetch task + // to run fetchParams’s process response given response, with fetchParams’s + // task destination. + if (fetchParams.processResponse != null) { + queueMicrotask(() => fetchParams.processResponse(response)) + } + + // 4. If response’s body is null, then run processResponseEndOfBody. + if (response.body == null) { + processResponseEndOfBody() + } else { + // 5. Otherwise: + + // 1. Let transformStream be a new a TransformStream. + + // 2. Let identityTransformAlgorithm be an algorithm which, given chunk, + // enqueues chunk in transformStream. + const identityTransformAlgorithm = (chunk, controller) => { + controller.enqueue(chunk) + } + + // 3. Set up transformStream with transformAlgorithm set to identityTransformAlgorithm + // and flushAlgorithm set to processResponseEndOfBody. + const transformStream = new TransformStream({ + start () {}, + transform: identityTransformAlgorithm, + flush: processResponseEndOfBody + }, { + size () { + return 1 + } + }, { + size () { + return 1 + } + }) + + // 4. Set response’s body to the result of piping response’s body through transformStream. + response.body = { stream: response.body.stream.pipeThrough(transformStream) } + } + + // 6. If fetchParams’s process response consume body is non-null, then: + if (fetchParams.processResponseConsumeBody != null) { + // 1. Let processBody given nullOrBytes be this step: run fetchParams’s + // process response consume body given response and nullOrBytes. + const processBody = (nullOrBytes) => fetchParams.processResponseConsumeBody(response, nullOrBytes) + + // 2. Let processBodyError be this step: run fetchParams’s process + // response consume body given response and failure. + const processBodyError = (failure) => fetchParams.processResponseConsumeBody(response, failure) + + // 3. If response’s body is null, then queue a fetch task to run processBody + // given null, with fetchParams’s task destination. + if (response.body == null) { + queueMicrotask(() => processBody(null)) + } else { + // 4. Otherwise, fully read response’s body given processBody, processBodyError, + // and fetchParams’s task destination. + return fullyReadBody(response.body, processBody, processBodyError) + } + return Promise.resolve() + } +} + +// https://fetch.spec.whatwg.org/#http-fetch +async function httpFetch (fetchParams) { + // 1. Let request be fetchParams’s request. + const request = fetchParams.request + + // 2. Let response be null. + let response = null + + // 3. Let actualResponse be null. + let actualResponse = null + + // 4. Let timingInfo be fetchParams’s timing info. + const timingInfo = fetchParams.timingInfo + + // 5. If request’s service-workers mode is "all", then: + if (request.serviceWorkers === 'all') { + // TODO + } + + // 6. If response is null, then: + if (response === null) { + // 1. If makeCORSPreflight is true and one of these conditions is true: + // TODO + + // 2. If request’s redirect mode is "follow", then set request’s + // service-workers mode to "none". + if (request.redirect === 'follow') { + request.serviceWorkers = 'none' + } + + // 3. Set response and actualResponse to the result of running + // HTTP-network-or-cache fetch given fetchParams. + actualResponse = response = await httpNetworkOrCacheFetch(fetchParams) + + // 4. If request’s response tainting is "cors" and a CORS check + // for request and response returns failure, then return a network error. + if ( + request.responseTainting === 'cors' && + corsCheck(request, response) === 'failure' + ) { + return makeNetworkError('cors failure') + } + + // 5. If the TAO check for request and response returns failure, then set + // request’s timing allow failed flag. + if (TAOCheck(request, response) === 'failure') { + request.timingAllowFailed = true + } + } + + // 7. If either request’s response tainting or response’s type + // is "opaque", and the cross-origin resource policy check with + // request’s origin, request’s client, request’s destination, + // and actualResponse returns blocked, then return a network error. + if ( + (request.responseTainting === 'opaque' || response.type === 'opaque') && + crossOriginResourcePolicyCheck( + request.origin, + request.client, + request.destination, + actualResponse + ) === 'blocked' + ) { + return makeNetworkError('blocked') + } + + // 8. If actualResponse’s status is a redirect status, then: + if (redirectStatusSet.has(actualResponse.status)) { + // 1. If actualResponse’s status is not 303, request’s body is not null, + // and the connection uses HTTP/2, then user agents may, and are even + // encouraged to, transmit an RST_STREAM frame. + // See, https://github.com/whatwg/fetch/issues/1288 + if (request.redirect !== 'manual') { + fetchParams.controller.connection.destroy() + } + + // 2. Switch on request’s redirect mode: + if (request.redirect === 'error') { + // Set response to a network error. + response = makeNetworkError('unexpected redirect') + } else if (request.redirect === 'manual') { + // Set response to an opaque-redirect filtered response whose internal + // response is actualResponse. + // NOTE(spec): On the web this would return an `opaqueredirect` response, + // but that doesn't make sense server side. + // See https://github.com/nodejs/undici/issues/1193. + response = actualResponse + } else if (request.redirect === 'follow') { + // Set response to the result of running HTTP-redirect fetch given + // fetchParams and response. + response = await httpRedirectFetch(fetchParams, response) + } else { + assert(false) + } + } + + // 9. Set response’s timing info to timingInfo. + response.timingInfo = timingInfo + + // 10. Return response. + return response +} + +// https://fetch.spec.whatwg.org/#http-redirect-fetch +function httpRedirectFetch (fetchParams, response) { + // 1. Let request be fetchParams’s request. + const request = fetchParams.request + + // 2. Let actualResponse be response, if response is not a filtered response, + // and response’s internal response otherwise. + const actualResponse = response.internalResponse + ? response.internalResponse + : response + + // 3. Let locationURL be actualResponse’s location URL given request’s current + // URL’s fragment. + let locationURL + + try { + locationURL = responseLocationURL( + actualResponse, + requestCurrentURL(request).hash + ) + + // 4. If locationURL is null, then return response. + if (locationURL == null) { + return response + } + } catch (err) { + // 5. If locationURL is failure, then return a network error. + return Promise.resolve(makeNetworkError(err)) + } + + // 6. If locationURL’s scheme is not an HTTP(S) scheme, then return a network + // error. + if (!urlIsHttpHttpsScheme(locationURL)) { + return Promise.resolve(makeNetworkError('URL scheme must be a HTTP(S) scheme')) + } + + // 7. If request’s redirect count is 20, then return a network error. + if (request.redirectCount === 20) { + return Promise.resolve(makeNetworkError('redirect count exceeded')) + } + + // 8. Increase request’s redirect count by 1. + request.redirectCount += 1 + + // 9. If request’s mode is "cors", locationURL includes credentials, and + // request’s origin is not same origin with locationURL’s origin, then return + // a network error. + if ( + request.mode === 'cors' && + (locationURL.username || locationURL.password) && + !sameOrigin(request, locationURL) + ) { + return Promise.resolve(makeNetworkError('cross origin not allowed for request mode "cors"')) + } + + // 10. If request’s response tainting is "cors" and locationURL includes + // credentials, then return a network error. + if ( + request.responseTainting === 'cors' && + (locationURL.username || locationURL.password) + ) { + return Promise.resolve(makeNetworkError( + 'URL cannot contain credentials for request mode "cors"' + )) + } + + // 11. If actualResponse’s status is not 303, request’s body is non-null, + // and request’s body’s source is null, then return a network error. + if ( + actualResponse.status !== 303 && + request.body != null && + request.body.source == null + ) { + return Promise.resolve(makeNetworkError()) + } + + // 12. If one of the following is true + // - actualResponse’s status is 301 or 302 and request’s method is `POST` + // - actualResponse’s status is 303 and request’s method is not `GET` or `HEAD` + if ( + ([301, 302].includes(actualResponse.status) && request.method === 'POST') || + (actualResponse.status === 303 && + !GET_OR_HEAD.includes(request.method)) + ) { + // then: + // 1. Set request’s method to `GET` and request’s body to null. + request.method = 'GET' + request.body = null + + // 2. For each headerName of request-body-header name, delete headerName from + // request’s header list. + for (const headerName of requestBodyHeader) { + request.headersList.delete(headerName) + } + } + + // 13. If request’s current URL’s origin is not same origin with locationURL’s + // origin, then for each headerName of CORS non-wildcard request-header name, + // delete headerName from request’s header list. + if (!sameOrigin(requestCurrentURL(request), locationURL)) { + // https://fetch.spec.whatwg.org/#cors-non-wildcard-request-header-name + request.headersList.delete('authorization') + + // "Cookie" and "Host" are forbidden request-headers, which undici doesn't implement. + request.headersList.delete('cookie') + request.headersList.delete('host') + } + + // 14. If request’s body is non-null, then set request’s body to the first return + // value of safely extracting request’s body’s source. + if (request.body != null) { + assert(request.body.source != null) + request.body = safelyExtractBody(request.body.source)[0] + } + + // 15. Let timingInfo be fetchParams’s timing info. + const timingInfo = fetchParams.timingInfo + + // 16. Set timingInfo’s redirect end time and post-redirect start time to the + // coarsened shared current time given fetchParams’s cross-origin isolated + // capability. + timingInfo.redirectEndTime = timingInfo.postRedirectStartTime = + coarsenedSharedCurrentTime(fetchParams.crossOriginIsolatedCapability) + + // 17. If timingInfo’s redirect start time is 0, then set timingInfo’s + // redirect start time to timingInfo’s start time. + if (timingInfo.redirectStartTime === 0) { + timingInfo.redirectStartTime = timingInfo.startTime + } + + // 18. Append locationURL to request’s URL list. + request.urlList.push(locationURL) + + // 19. Invoke set request’s referrer policy on redirect on request and + // actualResponse. + setRequestReferrerPolicyOnRedirect(request, actualResponse) + + // 20. Return the result of running main fetch given fetchParams and true. + return mainFetch(fetchParams, true) +} + +// https://fetch.spec.whatwg.org/#http-network-or-cache-fetch +async function httpNetworkOrCacheFetch ( + fetchParams, + isAuthenticationFetch = false, + isNewConnectionFetch = false +) { + // 1. Let request be fetchParams’s request. + const request = fetchParams.request + + // 2. Let httpFetchParams be null. + let httpFetchParams = null + + // 3. Let httpRequest be null. + let httpRequest = null + + // 4. Let response be null. + let response = null + + // 5. Let storedResponse be null. + // TODO: cache + + // 6. Let httpCache be null. + const httpCache = null + + // 7. Let the revalidatingFlag be unset. + const revalidatingFlag = false + + // 8. Run these steps, but abort when the ongoing fetch is terminated: + + // 1. If request’s window is "no-window" and request’s redirect mode is + // "error", then set httpFetchParams to fetchParams and httpRequest to + // request. + if (request.window === 'no-window' && request.redirect === 'error') { + httpFetchParams = fetchParams + httpRequest = request + } else { + // Otherwise: + + // 1. Set httpRequest to a clone of request. + httpRequest = makeRequest(request) + + // 2. Set httpFetchParams to a copy of fetchParams. + httpFetchParams = { ...fetchParams } + + // 3. Set httpFetchParams’s request to httpRequest. + httpFetchParams.request = httpRequest + } + + // 3. Let includeCredentials be true if one of + const includeCredentials = + request.credentials === 'include' || + (request.credentials === 'same-origin' && + request.responseTainting === 'basic') + + // 4. Let contentLength be httpRequest’s body’s length, if httpRequest’s + // body is non-null; otherwise null. + const contentLength = httpRequest.body ? httpRequest.body.length : null + + // 5. Let contentLengthHeaderValue be null. + let contentLengthHeaderValue = null + + // 6. If httpRequest’s body is null and httpRequest’s method is `POST` or + // `PUT`, then set contentLengthHeaderValue to `0`. + if ( + httpRequest.body == null && + ['POST', 'PUT'].includes(httpRequest.method) + ) { + contentLengthHeaderValue = '0' + } + + // 7. If contentLength is non-null, then set contentLengthHeaderValue to + // contentLength, serialized and isomorphic encoded. + if (contentLength != null) { + contentLengthHeaderValue = isomorphicEncode(`${contentLength}`) + } + + // 8. If contentLengthHeaderValue is non-null, then append + // `Content-Length`/contentLengthHeaderValue to httpRequest’s header + // list. + if (contentLengthHeaderValue != null) { + httpRequest.headersList.append('content-length', contentLengthHeaderValue) + } + + // 9. If contentLengthHeaderValue is non-null, then append (`Content-Length`, + // contentLengthHeaderValue) to httpRequest’s header list. + + // 10. If contentLength is non-null and httpRequest’s keepalive is true, + // then: + if (contentLength != null && httpRequest.keepalive) { + // NOTE: keepalive is a noop outside of browser context. + } + + // 11. If httpRequest’s referrer is a URL, then append + // `Referer`/httpRequest’s referrer, serialized and isomorphic encoded, + // to httpRequest’s header list. + if (httpRequest.referrer instanceof URL) { + httpRequest.headersList.append('referer', isomorphicEncode(httpRequest.referrer.href)) + } + + // 12. Append a request `Origin` header for httpRequest. + appendRequestOriginHeader(httpRequest) + + // 13. Append the Fetch metadata headers for httpRequest. [FETCH-METADATA] + appendFetchMetadata(httpRequest) + + // 14. If httpRequest’s header list does not contain `User-Agent`, then + // user agents should append `User-Agent`/default `User-Agent` value to + // httpRequest’s header list. + if (!httpRequest.headersList.contains('user-agent')) { + httpRequest.headersList.append('user-agent', typeof esbuildDetection === 'undefined' ? 'undici' : 'node') + } + + // 15. If httpRequest’s cache mode is "default" and httpRequest’s header + // list contains `If-Modified-Since`, `If-None-Match`, + // `If-Unmodified-Since`, `If-Match`, or `If-Range`, then set + // httpRequest’s cache mode to "no-store". + if ( + httpRequest.cache === 'default' && + (httpRequest.headersList.contains('if-modified-since') || + httpRequest.headersList.contains('if-none-match') || + httpRequest.headersList.contains('if-unmodified-since') || + httpRequest.headersList.contains('if-match') || + httpRequest.headersList.contains('if-range')) + ) { + httpRequest.cache = 'no-store' + } + + // 16. If httpRequest’s cache mode is "no-cache", httpRequest’s prevent + // no-cache cache-control header modification flag is unset, and + // httpRequest’s header list does not contain `Cache-Control`, then append + // `Cache-Control`/`max-age=0` to httpRequest’s header list. + if ( + httpRequest.cache === 'no-cache' && + !httpRequest.preventNoCacheCacheControlHeaderModification && + !httpRequest.headersList.contains('cache-control') + ) { + httpRequest.headersList.append('cache-control', 'max-age=0') + } + + // 17. If httpRequest’s cache mode is "no-store" or "reload", then: + if (httpRequest.cache === 'no-store' || httpRequest.cache === 'reload') { + // 1. If httpRequest’s header list does not contain `Pragma`, then append + // `Pragma`/`no-cache` to httpRequest’s header list. + if (!httpRequest.headersList.contains('pragma')) { + httpRequest.headersList.append('pragma', 'no-cache') + } + + // 2. If httpRequest’s header list does not contain `Cache-Control`, + // then append `Cache-Control`/`no-cache` to httpRequest’s header list. + if (!httpRequest.headersList.contains('cache-control')) { + httpRequest.headersList.append('cache-control', 'no-cache') + } + } + + // 18. If httpRequest’s header list contains `Range`, then append + // `Accept-Encoding`/`identity` to httpRequest’s header list. + if (httpRequest.headersList.contains('range')) { + httpRequest.headersList.append('accept-encoding', 'identity') + } + + // 19. Modify httpRequest’s header list per HTTP. Do not append a given + // header if httpRequest’s header list contains that header’s name. + // TODO: https://github.com/whatwg/fetch/issues/1285#issuecomment-896560129 + if (!httpRequest.headersList.contains('accept-encoding')) { + if (urlHasHttpsScheme(requestCurrentURL(httpRequest))) { + httpRequest.headersList.append('accept-encoding', 'br, gzip, deflate') + } else { + httpRequest.headersList.append('accept-encoding', 'gzip, deflate') + } + } + + httpRequest.headersList.delete('host') + + // 20. If includeCredentials is true, then: + if (includeCredentials) { + // 1. If the user agent is not configured to block cookies for httpRequest + // (see section 7 of [COOKIES]), then: + // TODO: credentials + // 2. If httpRequest’s header list does not contain `Authorization`, then: + // TODO: credentials + } + + // 21. If there’s a proxy-authentication entry, use it as appropriate. + // TODO: proxy-authentication + + // 22. Set httpCache to the result of determining the HTTP cache + // partition, given httpRequest. + // TODO: cache + + // 23. If httpCache is null, then set httpRequest’s cache mode to + // "no-store". + if (httpCache == null) { + httpRequest.cache = 'no-store' + } + + // 24. If httpRequest’s cache mode is neither "no-store" nor "reload", + // then: + if (httpRequest.mode !== 'no-store' && httpRequest.mode !== 'reload') { + // TODO: cache + } + + // 9. If aborted, then return the appropriate network error for fetchParams. + // TODO + + // 10. If response is null, then: + if (response == null) { + // 1. If httpRequest’s cache mode is "only-if-cached", then return a + // network error. + if (httpRequest.mode === 'only-if-cached') { + return makeNetworkError('only if cached') + } + + // 2. Let forwardResponse be the result of running HTTP-network fetch + // given httpFetchParams, includeCredentials, and isNewConnectionFetch. + const forwardResponse = await httpNetworkFetch( + httpFetchParams, + includeCredentials, + isNewConnectionFetch + ) + + // 3. If httpRequest’s method is unsafe and forwardResponse’s status is + // in the range 200 to 399, inclusive, invalidate appropriate stored + // responses in httpCache, as per the "Invalidation" chapter of HTTP + // Caching, and set storedResponse to null. [HTTP-CACHING] + if ( + !safeMethodsSet.has(httpRequest.method) && + forwardResponse.status >= 200 && + forwardResponse.status <= 399 + ) { + // TODO: cache + } + + // 4. If the revalidatingFlag is set and forwardResponse’s status is 304, + // then: + if (revalidatingFlag && forwardResponse.status === 304) { + // TODO: cache + } + + // 5. If response is null, then: + if (response == null) { + // 1. Set response to forwardResponse. + response = forwardResponse + + // 2. Store httpRequest and forwardResponse in httpCache, as per the + // "Storing Responses in Caches" chapter of HTTP Caching. [HTTP-CACHING] + // TODO: cache + } + } + + // 11. Set response’s URL list to a clone of httpRequest’s URL list. + response.urlList = [...httpRequest.urlList] + + // 12. If httpRequest’s header list contains `Range`, then set response’s + // range-requested flag. + if (httpRequest.headersList.contains('range')) { + response.rangeRequested = true + } + + // 13. Set response’s request-includes-credentials to includeCredentials. + response.requestIncludesCredentials = includeCredentials + + // 14. If response’s status is 401, httpRequest’s response tainting is not + // "cors", includeCredentials is true, and request’s window is an environment + // settings object, then: + // TODO + + // 15. If response’s status is 407, then: + if (response.status === 407) { + // 1. If request’s window is "no-window", then return a network error. + if (request.window === 'no-window') { + return makeNetworkError() + } + + // 2. ??? + + // 3. If fetchParams is canceled, then return the appropriate network error for fetchParams. + if (isCancelled(fetchParams)) { + return makeAppropriateNetworkError(fetchParams) + } + + // 4. Prompt the end user as appropriate in request’s window and store + // the result as a proxy-authentication entry. [HTTP-AUTH] + // TODO: Invoke some kind of callback? + + // 5. Set response to the result of running HTTP-network-or-cache fetch given + // fetchParams. + // TODO + return makeNetworkError('proxy authentication required') + } + + // 16. If all of the following are true + if ( + // response’s status is 421 + response.status === 421 && + // isNewConnectionFetch is false + !isNewConnectionFetch && + // request’s body is null, or request’s body is non-null and request’s body’s source is non-null + (request.body == null || request.body.source != null) + ) { + // then: + + // 1. If fetchParams is canceled, then return the appropriate network error for fetchParams. + if (isCancelled(fetchParams)) { + return makeAppropriateNetworkError(fetchParams) + } + + // 2. Set response to the result of running HTTP-network-or-cache + // fetch given fetchParams, isAuthenticationFetch, and true. + + // TODO (spec): The spec doesn't specify this but we need to cancel + // the active response before we can start a new one. + // https://github.com/whatwg/fetch/issues/1293 + fetchParams.controller.connection.destroy() + + response = await httpNetworkOrCacheFetch( + fetchParams, + isAuthenticationFetch, + true + ) + } + + // 17. If isAuthenticationFetch is true, then create an authentication entry + if (isAuthenticationFetch) { + // TODO + } + + // 18. Return response. + return response +} + +// https://fetch.spec.whatwg.org/#http-network-fetch +async function httpNetworkFetch ( + fetchParams, + includeCredentials = false, + forceNewConnection = false +) { + assert(!fetchParams.controller.connection || fetchParams.controller.connection.destroyed) + + fetchParams.controller.connection = { + abort: null, + destroyed: false, + destroy (err) { + if (!this.destroyed) { + this.destroyed = true + this.abort?.(err ?? new DOMException('The operation was aborted.', 'AbortError')) + } + } + } + + // 1. Let request be fetchParams’s request. + const request = fetchParams.request + + // 2. Let response be null. + let response = null + + // 3. Let timingInfo be fetchParams’s timing info. + const timingInfo = fetchParams.timingInfo + + // 4. Let httpCache be the result of determining the HTTP cache partition, + // given request. + // TODO: cache + const httpCache = null + + // 5. If httpCache is null, then set request’s cache mode to "no-store". + if (httpCache == null) { + request.cache = 'no-store' + } + + // 6. Let networkPartitionKey be the result of determining the network + // partition key given request. + // TODO + + // 7. Let newConnection be "yes" if forceNewConnection is true; otherwise + // "no". + const newConnection = forceNewConnection ? 'yes' : 'no' // eslint-disable-line no-unused-vars + + // 8. Switch on request’s mode: + if (request.mode === 'websocket') { + // Let connection be the result of obtaining a WebSocket connection, + // given request’s current URL. + // TODO + } else { + // Let connection be the result of obtaining a connection, given + // networkPartitionKey, request’s current URL’s origin, + // includeCredentials, and forceNewConnection. + // TODO + } + + // 9. Run these steps, but abort when the ongoing fetch is terminated: + + // 1. If connection is failure, then return a network error. + + // 2. Set timingInfo’s final connection timing info to the result of + // calling clamp and coarsen connection timing info with connection’s + // timing info, timingInfo’s post-redirect start time, and fetchParams’s + // cross-origin isolated capability. + + // 3. If connection is not an HTTP/2 connection, request’s body is non-null, + // and request’s body’s source is null, then append (`Transfer-Encoding`, + // `chunked`) to request’s header list. + + // 4. Set timingInfo’s final network-request start time to the coarsened + // shared current time given fetchParams’s cross-origin isolated + // capability. + + // 5. Set response to the result of making an HTTP request over connection + // using request with the following caveats: + + // - Follow the relevant requirements from HTTP. [HTTP] [HTTP-SEMANTICS] + // [HTTP-COND] [HTTP-CACHING] [HTTP-AUTH] + + // - If request’s body is non-null, and request’s body’s source is null, + // then the user agent may have a buffer of up to 64 kibibytes and store + // a part of request’s body in that buffer. If the user agent reads from + // request’s body beyond that buffer’s size and the user agent needs to + // resend request, then instead return a network error. + + // - Set timingInfo’s final network-response start time to the coarsened + // shared current time given fetchParams’s cross-origin isolated capability, + // immediately after the user agent’s HTTP parser receives the first byte + // of the response (e.g., frame header bytes for HTTP/2 or response status + // line for HTTP/1.x). + + // - Wait until all the headers are transmitted. + + // - Any responses whose status is in the range 100 to 199, inclusive, + // and is not 101, are to be ignored, except for the purposes of setting + // timingInfo’s final network-response start time above. + + // - If request’s header list contains `Transfer-Encoding`/`chunked` and + // response is transferred via HTTP/1.0 or older, then return a network + // error. + + // - If the HTTP request results in a TLS client certificate dialog, then: + + // 1. If request’s window is an environment settings object, make the + // dialog available in request’s window. + + // 2. Otherwise, return a network error. + + // To transmit request’s body body, run these steps: + let requestBody = null + // 1. If body is null and fetchParams’s process request end-of-body is + // non-null, then queue a fetch task given fetchParams’s process request + // end-of-body and fetchParams’s task destination. + if (request.body == null && fetchParams.processRequestEndOfBody) { + queueMicrotask(() => fetchParams.processRequestEndOfBody()) + } else if (request.body != null) { + // 2. Otherwise, if body is non-null: + + // 1. Let processBodyChunk given bytes be these steps: + const processBodyChunk = async function * (bytes) { + // 1. If the ongoing fetch is terminated, then abort these steps. + if (isCancelled(fetchParams)) { + return + } + + // 2. Run this step in parallel: transmit bytes. + yield bytes + + // 3. If fetchParams’s process request body is non-null, then run + // fetchParams’s process request body given bytes’s length. + fetchParams.processRequestBodyChunkLength?.(bytes.byteLength) + } + + // 2. Let processEndOfBody be these steps: + const processEndOfBody = () => { + // 1. If fetchParams is canceled, then abort these steps. + if (isCancelled(fetchParams)) { + return + } + + // 2. If fetchParams’s process request end-of-body is non-null, + // then run fetchParams’s process request end-of-body. + if (fetchParams.processRequestEndOfBody) { + fetchParams.processRequestEndOfBody() + } + } + + // 3. Let processBodyError given e be these steps: + const processBodyError = (e) => { + // 1. If fetchParams is canceled, then abort these steps. + if (isCancelled(fetchParams)) { + return + } + + // 2. If e is an "AbortError" DOMException, then abort fetchParams’s controller. + if (e.name === 'AbortError') { + fetchParams.controller.abort() + } else { + fetchParams.controller.terminate(e) + } + } + + // 4. Incrementally read request’s body given processBodyChunk, processEndOfBody, + // processBodyError, and fetchParams’s task destination. + requestBody = (async function * () { + try { + for await (const bytes of request.body.stream) { + yield * processBodyChunk(bytes) + } + processEndOfBody() + } catch (err) { + processBodyError(err) + } + })() + } + + try { + // socket is only provided for websockets + const { body, status, statusText, headersList, socket } = await dispatch({ body: requestBody }) + + if (socket) { + response = makeResponse({ status, statusText, headersList, socket }) + } else { + const iterator = body[Symbol.asyncIterator]() + fetchParams.controller.next = () => iterator.next() + + response = makeResponse({ status, statusText, headersList }) + } + } catch (err) { + // 10. If aborted, then: + if (err.name === 'AbortError') { + // 1. If connection uses HTTP/2, then transmit an RST_STREAM frame. + fetchParams.controller.connection.destroy() + + // 2. Return the appropriate network error for fetchParams. + return makeAppropriateNetworkError(fetchParams, err) + } + + return makeNetworkError(err) + } + + // 11. Let pullAlgorithm be an action that resumes the ongoing fetch + // if it is suspended. + const pullAlgorithm = () => { + fetchParams.controller.resume() + } + + // 12. Let cancelAlgorithm be an algorithm that aborts fetchParams’s + // controller with reason, given reason. + const cancelAlgorithm = (reason) => { + fetchParams.controller.abort(reason) + } + + // 13. Let highWaterMark be a non-negative, non-NaN number, chosen by + // the user agent. + // TODO + + // 14. Let sizeAlgorithm be an algorithm that accepts a chunk object + // and returns a non-negative, non-NaN, non-infinite number, chosen by the user agent. + // TODO + + // 15. Let stream be a new ReadableStream. + // 16. Set up stream with pullAlgorithm set to pullAlgorithm, + // cancelAlgorithm set to cancelAlgorithm, highWaterMark set to + // highWaterMark, and sizeAlgorithm set to sizeAlgorithm. + if (!ReadableStream) { + ReadableStream = (__nccwpck_require__(5356).ReadableStream) + } + + const stream = new ReadableStream( + { + async start (controller) { + fetchParams.controller.controller = controller + }, + async pull (controller) { + await pullAlgorithm(controller) + }, + async cancel (reason) { + await cancelAlgorithm(reason) + } + }, + { + highWaterMark: 0, + size () { + return 1 + } + } + ) + + // 17. Run these steps, but abort when the ongoing fetch is terminated: + + // 1. Set response’s body to a new body whose stream is stream. + response.body = { stream } + + // 2. If response is not a network error and request’s cache mode is + // not "no-store", then update response in httpCache for request. + // TODO + + // 3. If includeCredentials is true and the user agent is not configured + // to block cookies for request (see section 7 of [COOKIES]), then run the + // "set-cookie-string" parsing algorithm (see section 5.2 of [COOKIES]) on + // the value of each header whose name is a byte-case-insensitive match for + // `Set-Cookie` in response’s header list, if any, and request’s current URL. + // TODO + + // 18. If aborted, then: + // TODO + + // 19. Run these steps in parallel: + + // 1. Run these steps, but abort when fetchParams is canceled: + fetchParams.controller.on('terminated', onAborted) + fetchParams.controller.resume = async () => { + // 1. While true + while (true) { + // 1-3. See onData... + + // 4. Set bytes to the result of handling content codings given + // codings and bytes. + let bytes + let isFailure + try { + const { done, value } = await fetchParams.controller.next() + + if (isAborted(fetchParams)) { + break + } + + bytes = done ? undefined : value + } catch (err) { + if (fetchParams.controller.ended && !timingInfo.encodedBodySize) { + // zlib doesn't like empty streams. + bytes = undefined + } else { + bytes = err + + // err may be propagated from the result of calling readablestream.cancel, + // which might not be an error. https://github.com/nodejs/undici/issues/2009 + isFailure = true + } + } + + if (bytes === undefined) { + // 2. Otherwise, if the bytes transmission for response’s message + // body is done normally and stream is readable, then close + // stream, finalize response for fetchParams and response, and + // abort these in-parallel steps. + readableStreamClose(fetchParams.controller.controller) + + finalizeResponse(fetchParams, response) + + return + } + + // 5. Increase timingInfo’s decoded body size by bytes’s length. + timingInfo.decodedBodySize += bytes?.byteLength ?? 0 + + // 6. If bytes is failure, then terminate fetchParams’s controller. + if (isFailure) { + fetchParams.controller.terminate(bytes) + return + } + + // 7. Enqueue a Uint8Array wrapping an ArrayBuffer containing bytes + // into stream. + fetchParams.controller.controller.enqueue(new Uint8Array(bytes)) + + // 8. If stream is errored, then terminate the ongoing fetch. + if (isErrored(stream)) { + fetchParams.controller.terminate() + return + } + + // 9. If stream doesn’t need more data ask the user agent to suspend + // the ongoing fetch. + if (!fetchParams.controller.controller.desiredSize) { + return + } + } + } + + // 2. If aborted, then: + function onAborted (reason) { + // 2. If fetchParams is aborted, then: + if (isAborted(fetchParams)) { + // 1. Set response’s aborted flag. + response.aborted = true + + // 2. If stream is readable, then error stream with the result of + // deserialize a serialized abort reason given fetchParams’s + // controller’s serialized abort reason and an + // implementation-defined realm. + if (isReadable(stream)) { + fetchParams.controller.controller.error( + fetchParams.controller.serializedAbortReason + ) + } + } else { + // 3. Otherwise, if stream is readable, error stream with a TypeError. + if (isReadable(stream)) { + fetchParams.controller.controller.error(new TypeError('terminated', { + cause: isErrorLike(reason) ? reason : undefined + })) + } + } + + // 4. If connection uses HTTP/2, then transmit an RST_STREAM frame. + // 5. Otherwise, the user agent should close connection unless it would be bad for performance to do so. + fetchParams.controller.connection.destroy() + } + + // 20. Return response. + return response + + async function dispatch ({ body }) { + const url = requestCurrentURL(request) + /** @type {import('../..').Agent} */ + const agent = fetchParams.controller.dispatcher + + return new Promise((resolve, reject) => agent.dispatch( + { + path: url.pathname + url.search, + origin: url.origin, + method: request.method, + body: fetchParams.controller.dispatcher.isMockActive ? request.body && (request.body.source || request.body.stream) : body, + headers: request.headersList.entries, + maxRedirections: 0, + upgrade: request.mode === 'websocket' ? 'websocket' : undefined + }, + { + body: null, + abort: null, + + onConnect (abort) { + // TODO (fix): Do we need connection here? + const { connection } = fetchParams.controller + + if (connection.destroyed) { + abort(new DOMException('The operation was aborted.', 'AbortError')) + } else { + fetchParams.controller.on('terminated', abort) + this.abort = connection.abort = abort + } + }, + + onHeaders (status, headersList, resume, statusText) { + if (status < 200) { + return + } + + let codings = [] + let location = '' + + const headers = new Headers() + + // For H2, the headers are a plain JS object + // We distinguish between them and iterate accordingly + if (Array.isArray(headersList)) { + for (let n = 0; n < headersList.length; n += 2) { + const key = headersList[n + 0].toString('latin1') + const val = headersList[n + 1].toString('latin1') + if (key.toLowerCase() === 'content-encoding') { + // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1 + // "All content-coding values are case-insensitive..." + codings = val.toLowerCase().split(',').map((x) => x.trim()) + } else if (key.toLowerCase() === 'location') { + location = val + } + + headers[kHeadersList].append(key, val) + } + } else { + const keys = Object.keys(headersList) + for (const key of keys) { + const val = headersList[key] + if (key.toLowerCase() === 'content-encoding') { + // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1 + // "All content-coding values are case-insensitive..." + codings = val.toLowerCase().split(',').map((x) => x.trim()).reverse() + } else if (key.toLowerCase() === 'location') { + location = val + } + + headers[kHeadersList].append(key, val) + } + } + + this.body = new Readable({ read: resume }) + + const decoders = [] + + const willFollow = request.redirect === 'follow' && + location && + redirectStatusSet.has(status) + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding + if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status) && !willFollow) { + for (const coding of codings) { + // https://www.rfc-editor.org/rfc/rfc9112.html#section-7.2 + if (coding === 'x-gzip' || coding === 'gzip') { + decoders.push(zlib.createGunzip({ + // Be less strict when decoding compressed responses, since sometimes + // servers send slightly invalid responses that are still accepted + // by common browsers. + // Always using Z_SYNC_FLUSH is what cURL does. + flush: zlib.constants.Z_SYNC_FLUSH, + finishFlush: zlib.constants.Z_SYNC_FLUSH + })) + } else if (coding === 'deflate') { + decoders.push(zlib.createInflate()) + } else if (coding === 'br') { + decoders.push(zlib.createBrotliDecompress()) + } else { + decoders.length = 0 + break + } + } + } + + resolve({ + status, + statusText, + headersList: headers[kHeadersList], + body: decoders.length + ? pipeline(this.body, ...decoders, () => { }) + : this.body.on('error', () => {}) + }) + + return true + }, + + onData (chunk) { + if (fetchParams.controller.dump) { + return + } + + // 1. If one or more bytes have been transmitted from response’s + // message body, then: + + // 1. Let bytes be the transmitted bytes. + const bytes = chunk + + // 2. Let codings be the result of extracting header list values + // given `Content-Encoding` and response’s header list. + // See pullAlgorithm. + + // 3. Increase timingInfo’s encoded body size by bytes’s length. + timingInfo.encodedBodySize += bytes.byteLength + + // 4. See pullAlgorithm... + + return this.body.push(bytes) + }, + + onComplete () { + if (this.abort) { + fetchParams.controller.off('terminated', this.abort) + } + + fetchParams.controller.ended = true + + this.body.push(null) + }, + + onError (error) { + if (this.abort) { + fetchParams.controller.off('terminated', this.abort) + } + + this.body?.destroy(error) + + fetchParams.controller.terminate(error) + + reject(error) + }, + + onUpgrade (status, headersList, socket) { + if (status !== 101) { + return + } + + const headers = new Headers() + + for (let n = 0; n < headersList.length; n += 2) { + const key = headersList[n + 0].toString('latin1') + const val = headersList[n + 1].toString('latin1') + + headers[kHeadersList].append(key, val) + } + + resolve({ + status, + statusText: STATUS_CODES[status], + headersList: headers[kHeadersList], + socket + }) + + return true + } + } + )) + } +} + +module.exports = { + fetch, + Fetch, + fetching, + finalizeAndReportTiming +} + + +/***/ }), + +/***/ 8359: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; +/* globals AbortController */ + + + +const { extractBody, mixinBody, cloneBody } = __nccwpck_require__(1472) +const { Headers, fill: fillHeaders, HeadersList } = __nccwpck_require__(554) +const { FinalizationRegistry } = __nccwpck_require__(6436)() +const util = __nccwpck_require__(3983) +const { + isValidHTTPToken, + sameOrigin, + normalizeMethod, + makePolicyContainer, + normalizeMethodRecord +} = __nccwpck_require__(2538) +const { + forbiddenMethodsSet, + corsSafeListedMethodsSet, + referrerPolicy, + requestRedirect, + requestMode, + requestCredentials, + requestCache, + requestDuplex +} = __nccwpck_require__(1037) +const { kEnumerableProperty } = util +const { kHeaders, kSignal, kState, kGuard, kRealm } = __nccwpck_require__(5861) +const { webidl } = __nccwpck_require__(1744) +const { getGlobalOrigin } = __nccwpck_require__(1246) +const { URLSerializer } = __nccwpck_require__(685) +const { kHeadersList, kConstruct } = __nccwpck_require__(2785) +const assert = __nccwpck_require__(9491) +const { getMaxListeners, setMaxListeners, getEventListeners, defaultMaxListeners } = __nccwpck_require__(2361) + +let TransformStream = globalThis.TransformStream + +const kAbortController = Symbol('abortController') + +const requestFinalizer = new FinalizationRegistry(({ signal, abort }) => { + signal.removeEventListener('abort', abort) +}) + +// https://fetch.spec.whatwg.org/#request-class +class Request { + // https://fetch.spec.whatwg.org/#dom-request + constructor (input, init = {}) { + if (input === kConstruct) { + return + } + + webidl.argumentLengthCheck(arguments, 1, { header: 'Request constructor' }) + + input = webidl.converters.RequestInfo(input) + init = webidl.converters.RequestInit(init) + + // https://html.spec.whatwg.org/multipage/webappapis.html#environment-settings-object + this[kRealm] = { + settingsObject: { + baseUrl: getGlobalOrigin(), + get origin () { + return this.baseUrl?.origin + }, + policyContainer: makePolicyContainer() + } + } + + // 1. Let request be null. + let request = null + + // 2. Let fallbackMode be null. + let fallbackMode = null + + // 3. Let baseURL be this’s relevant settings object’s API base URL. + const baseUrl = this[kRealm].settingsObject.baseUrl + + // 4. Let signal be null. + let signal = null + + // 5. If input is a string, then: + if (typeof input === 'string') { + // 1. Let parsedURL be the result of parsing input with baseURL. + // 2. If parsedURL is failure, then throw a TypeError. + let parsedURL + try { + parsedURL = new URL(input, baseUrl) + } catch (err) { + throw new TypeError('Failed to parse URL from ' + input, { cause: err }) + } + + // 3. If parsedURL includes credentials, then throw a TypeError. + if (parsedURL.username || parsedURL.password) { + throw new TypeError( + 'Request cannot be constructed from a URL that includes credentials: ' + + input + ) + } + + // 4. Set request to a new request whose URL is parsedURL. + request = makeRequest({ urlList: [parsedURL] }) + + // 5. Set fallbackMode to "cors". + fallbackMode = 'cors' + } else { + // 6. Otherwise: + + // 7. Assert: input is a Request object. + assert(input instanceof Request) + + // 8. Set request to input’s request. + request = input[kState] + + // 9. Set signal to input’s signal. + signal = input[kSignal] + } + + // 7. Let origin be this’s relevant settings object’s origin. + const origin = this[kRealm].settingsObject.origin + + // 8. Let window be "client". + let window = 'client' + + // 9. If request’s window is an environment settings object and its origin + // is same origin with origin, then set window to request’s window. + if ( + request.window?.constructor?.name === 'EnvironmentSettingsObject' && + sameOrigin(request.window, origin) + ) { + window = request.window + } + + // 10. If init["window"] exists and is non-null, then throw a TypeError. + if (init.window != null) { + throw new TypeError(`'window' option '${window}' must be null`) + } + + // 11. If init["window"] exists, then set window to "no-window". + if ('window' in init) { + window = 'no-window' + } + + // 12. Set request to a new request with the following properties: + request = makeRequest({ + // URL request’s URL. + // undici implementation note: this is set as the first item in request's urlList in makeRequest + // method request’s method. + method: request.method, + // header list A copy of request’s header list. + // undici implementation note: headersList is cloned in makeRequest + headersList: request.headersList, + // unsafe-request flag Set. + unsafeRequest: request.unsafeRequest, + // client This’s relevant settings object. + client: this[kRealm].settingsObject, + // window window. + window, + // priority request’s priority. + priority: request.priority, + // origin request’s origin. The propagation of the origin is only significant for navigation requests + // being handled by a service worker. In this scenario a request can have an origin that is different + // from the current client. + origin: request.origin, + // referrer request’s referrer. + referrer: request.referrer, + // referrer policy request’s referrer policy. + referrerPolicy: request.referrerPolicy, + // mode request’s mode. + mode: request.mode, + // credentials mode request’s credentials mode. + credentials: request.credentials, + // cache mode request’s cache mode. + cache: request.cache, + // redirect mode request’s redirect mode. + redirect: request.redirect, + // integrity metadata request’s integrity metadata. + integrity: request.integrity, + // keepalive request’s keepalive. + keepalive: request.keepalive, + // reload-navigation flag request’s reload-navigation flag. + reloadNavigation: request.reloadNavigation, + // history-navigation flag request’s history-navigation flag. + historyNavigation: request.historyNavigation, + // URL list A clone of request’s URL list. + urlList: [...request.urlList] + }) + + const initHasKey = Object.keys(init).length !== 0 + + // 13. If init is not empty, then: + if (initHasKey) { + // 1. If request’s mode is "navigate", then set it to "same-origin". + if (request.mode === 'navigate') { + request.mode = 'same-origin' + } + + // 2. Unset request’s reload-navigation flag. + request.reloadNavigation = false + + // 3. Unset request’s history-navigation flag. + request.historyNavigation = false + + // 4. Set request’s origin to "client". + request.origin = 'client' + + // 5. Set request’s referrer to "client" + request.referrer = 'client' + + // 6. Set request’s referrer policy to the empty string. + request.referrerPolicy = '' + + // 7. Set request’s URL to request’s current URL. + request.url = request.urlList[request.urlList.length - 1] + + // 8. Set request’s URL list to « request’s URL ». + request.urlList = [request.url] + } + + // 14. If init["referrer"] exists, then: + if (init.referrer !== undefined) { + // 1. Let referrer be init["referrer"]. + const referrer = init.referrer + + // 2. If referrer is the empty string, then set request’s referrer to "no-referrer". + if (referrer === '') { + request.referrer = 'no-referrer' + } else { + // 1. Let parsedReferrer be the result of parsing referrer with + // baseURL. + // 2. If parsedReferrer is failure, then throw a TypeError. + let parsedReferrer + try { + parsedReferrer = new URL(referrer, baseUrl) + } catch (err) { + throw new TypeError(`Referrer "${referrer}" is not a valid URL.`, { cause: err }) + } + + // 3. If one of the following is true + // - parsedReferrer’s scheme is "about" and path is the string "client" + // - parsedReferrer’s origin is not same origin with origin + // then set request’s referrer to "client". + if ( + (parsedReferrer.protocol === 'about:' && parsedReferrer.hostname === 'client') || + (origin && !sameOrigin(parsedReferrer, this[kRealm].settingsObject.baseUrl)) + ) { + request.referrer = 'client' + } else { + // 4. Otherwise, set request’s referrer to parsedReferrer. + request.referrer = parsedReferrer + } + } + } + + // 15. If init["referrerPolicy"] exists, then set request’s referrer policy + // to it. + if (init.referrerPolicy !== undefined) { + request.referrerPolicy = init.referrerPolicy + } + + // 16. Let mode be init["mode"] if it exists, and fallbackMode otherwise. + let mode + if (init.mode !== undefined) { + mode = init.mode + } else { + mode = fallbackMode + } + + // 17. If mode is "navigate", then throw a TypeError. + if (mode === 'navigate') { + throw webidl.errors.exception({ + header: 'Request constructor', + message: 'invalid request mode navigate.' + }) + } + + // 18. If mode is non-null, set request’s mode to mode. + if (mode != null) { + request.mode = mode + } + + // 19. If init["credentials"] exists, then set request’s credentials mode + // to it. + if (init.credentials !== undefined) { + request.credentials = init.credentials + } + + // 18. If init["cache"] exists, then set request’s cache mode to it. + if (init.cache !== undefined) { + request.cache = init.cache + } + + // 21. If request’s cache mode is "only-if-cached" and request’s mode is + // not "same-origin", then throw a TypeError. + if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { + throw new TypeError( + "'only-if-cached' can be set only with 'same-origin' mode" + ) + } + + // 22. If init["redirect"] exists, then set request’s redirect mode to it. + if (init.redirect !== undefined) { + request.redirect = init.redirect + } + + // 23. If init["integrity"] exists, then set request’s integrity metadata to it. + if (init.integrity != null) { + request.integrity = String(init.integrity) + } + + // 24. If init["keepalive"] exists, then set request’s keepalive to it. + if (init.keepalive !== undefined) { + request.keepalive = Boolean(init.keepalive) + } + + // 25. If init["method"] exists, then: + if (init.method !== undefined) { + // 1. Let method be init["method"]. + let method = init.method + + // 2. If method is not a method or method is a forbidden method, then + // throw a TypeError. + if (!isValidHTTPToken(method)) { + throw new TypeError(`'${method}' is not a valid HTTP method.`) + } + + if (forbiddenMethodsSet.has(method.toUpperCase())) { + throw new TypeError(`'${method}' HTTP method is unsupported.`) + } + + // 3. Normalize method. + method = normalizeMethodRecord[method] ?? normalizeMethod(method) + + // 4. Set request’s method to method. + request.method = method + } + + // 26. If init["signal"] exists, then set signal to it. + if (init.signal !== undefined) { + signal = init.signal + } + + // 27. Set this’s request to request. + this[kState] = request + + // 28. Set this’s signal to a new AbortSignal object with this’s relevant + // Realm. + // TODO: could this be simplified with AbortSignal.any + // (https://dom.spec.whatwg.org/#dom-abortsignal-any) + const ac = new AbortController() + this[kSignal] = ac.signal + this[kSignal][kRealm] = this[kRealm] + + // 29. If signal is not null, then make this’s signal follow signal. + if (signal != null) { + if ( + !signal || + typeof signal.aborted !== 'boolean' || + typeof signal.addEventListener !== 'function' + ) { + throw new TypeError( + "Failed to construct 'Request': member signal is not of type AbortSignal." + ) + } + + if (signal.aborted) { + ac.abort(signal.reason) + } else { + // Keep a strong ref to ac while request object + // is alive. This is needed to prevent AbortController + // from being prematurely garbage collected. + // See, https://github.com/nodejs/undici/issues/1926. + this[kAbortController] = ac + + const acRef = new WeakRef(ac) + const abort = function () { + const ac = acRef.deref() + if (ac !== undefined) { + ac.abort(this.reason) + } + } + + // Third-party AbortControllers may not work with these. + // See, https://github.com/nodejs/undici/pull/1910#issuecomment-1464495619. + try { + // If the max amount of listeners is equal to the default, increase it + // This is only available in node >= v19.9.0 + if (typeof getMaxListeners === 'function' && getMaxListeners(signal) === defaultMaxListeners) { + setMaxListeners(100, signal) + } else if (getEventListeners(signal, 'abort').length >= defaultMaxListeners) { + setMaxListeners(100, signal) + } + } catch {} + + util.addAbortListener(signal, abort) + requestFinalizer.register(ac, { signal, abort }) + } + } + + // 30. Set this’s headers to a new Headers object with this’s relevant + // Realm, whose header list is request’s header list and guard is + // "request". + this[kHeaders] = new Headers(kConstruct) + this[kHeaders][kHeadersList] = request.headersList + this[kHeaders][kGuard] = 'request' + this[kHeaders][kRealm] = this[kRealm] + + // 31. If this’s request’s mode is "no-cors", then: + if (mode === 'no-cors') { + // 1. If this’s request’s method is not a CORS-safelisted method, + // then throw a TypeError. + if (!corsSafeListedMethodsSet.has(request.method)) { + throw new TypeError( + `'${request.method} is unsupported in no-cors mode.` + ) + } + + // 2. Set this’s headers’s guard to "request-no-cors". + this[kHeaders][kGuard] = 'request-no-cors' + } + + // 32. If init is not empty, then: + if (initHasKey) { + /** @type {HeadersList} */ + const headersList = this[kHeaders][kHeadersList] + // 1. Let headers be a copy of this’s headers and its associated header + // list. + // 2. If init["headers"] exists, then set headers to init["headers"]. + const headers = init.headers !== undefined ? init.headers : new HeadersList(headersList) + + // 3. Empty this’s headers’s header list. + headersList.clear() + + // 4. If headers is a Headers object, then for each header in its header + // list, append header’s name/header’s value to this’s headers. + if (headers instanceof HeadersList) { + for (const [key, val] of headers) { + headersList.append(key, val) + } + // Note: Copy the `set-cookie` meta-data. + headersList.cookies = headers.cookies + } else { + // 5. Otherwise, fill this’s headers with headers. + fillHeaders(this[kHeaders], headers) + } + } + + // 33. Let inputBody be input’s request’s body if input is a Request + // object; otherwise null. + const inputBody = input instanceof Request ? input[kState].body : null + + // 34. If either init["body"] exists and is non-null or inputBody is + // non-null, and request’s method is `GET` or `HEAD`, then throw a + // TypeError. + if ( + (init.body != null || inputBody != null) && + (request.method === 'GET' || request.method === 'HEAD') + ) { + throw new TypeError('Request with GET/HEAD method cannot have body.') + } + + // 35. Let initBody be null. + let initBody = null + + // 36. If init["body"] exists and is non-null, then: + if (init.body != null) { + // 1. Let Content-Type be null. + // 2. Set initBody and Content-Type to the result of extracting + // init["body"], with keepalive set to request’s keepalive. + const [extractedBody, contentType] = extractBody( + init.body, + request.keepalive + ) + initBody = extractedBody + + // 3, If Content-Type is non-null and this’s headers’s header list does + // not contain `Content-Type`, then append `Content-Type`/Content-Type to + // this’s headers. + if (contentType && !this[kHeaders][kHeadersList].contains('content-type')) { + this[kHeaders].append('content-type', contentType) + } + } + + // 37. Let inputOrInitBody be initBody if it is non-null; otherwise + // inputBody. + const inputOrInitBody = initBody ?? inputBody + + // 38. If inputOrInitBody is non-null and inputOrInitBody’s source is + // null, then: + if (inputOrInitBody != null && inputOrInitBody.source == null) { + // 1. If initBody is non-null and init["duplex"] does not exist, + // then throw a TypeError. + if (initBody != null && init.duplex == null) { + throw new TypeError('RequestInit: duplex option is required when sending a body.') + } + + // 2. If this’s request’s mode is neither "same-origin" nor "cors", + // then throw a TypeError. + if (request.mode !== 'same-origin' && request.mode !== 'cors') { + throw new TypeError( + 'If request is made from ReadableStream, mode should be "same-origin" or "cors"' + ) + } + + // 3. Set this’s request’s use-CORS-preflight flag. + request.useCORSPreflightFlag = true + } + + // 39. Let finalBody be inputOrInitBody. + let finalBody = inputOrInitBody + + // 40. If initBody is null and inputBody is non-null, then: + if (initBody == null && inputBody != null) { + // 1. If input is unusable, then throw a TypeError. + if (util.isDisturbed(inputBody.stream) || inputBody.stream.locked) { + throw new TypeError( + 'Cannot construct a Request with a Request object that has already been used.' + ) + } + + // 2. Set finalBody to the result of creating a proxy for inputBody. + if (!TransformStream) { + TransformStream = (__nccwpck_require__(5356).TransformStream) + } + + // https://streams.spec.whatwg.org/#readablestream-create-a-proxy + const identityTransform = new TransformStream() + inputBody.stream.pipeThrough(identityTransform) + finalBody = { + source: inputBody.source, + length: inputBody.length, + stream: identityTransform.readable + } + } + + // 41. Set this’s request’s body to finalBody. + this[kState].body = finalBody + } + + // Returns request’s HTTP method, which is "GET" by default. + get method () { + webidl.brandCheck(this, Request) + + // The method getter steps are to return this’s request’s method. + return this[kState].method + } + + // Returns the URL of request as a string. + get url () { + webidl.brandCheck(this, Request) + + // The url getter steps are to return this’s request’s URL, serialized. + return URLSerializer(this[kState].url) + } + + // Returns a Headers object consisting of the headers associated with request. + // Note that headers added in the network layer by the user agent will not + // be accounted for in this object, e.g., the "Host" header. + get headers () { + webidl.brandCheck(this, Request) + + // The headers getter steps are to return this’s headers. + return this[kHeaders] + } + + // Returns the kind of resource requested by request, e.g., "document" + // or "script". + get destination () { + webidl.brandCheck(this, Request) + + // The destination getter are to return this’s request’s destination. + return this[kState].destination + } + + // Returns the referrer of request. Its value can be a same-origin URL if + // explicitly set in init, the empty string to indicate no referrer, and + // "about:client" when defaulting to the global’s default. This is used + // during fetching to determine the value of the `Referer` header of the + // request being made. + get referrer () { + webidl.brandCheck(this, Request) + + // 1. If this’s request’s referrer is "no-referrer", then return the + // empty string. + if (this[kState].referrer === 'no-referrer') { + return '' + } + + // 2. If this’s request’s referrer is "client", then return + // "about:client". + if (this[kState].referrer === 'client') { + return 'about:client' + } + + // Return this’s request’s referrer, serialized. + return this[kState].referrer.toString() + } + + // Returns the referrer policy associated with request. + // This is used during fetching to compute the value of the request’s + // referrer. + get referrerPolicy () { + webidl.brandCheck(this, Request) + + // The referrerPolicy getter steps are to return this’s request’s referrer policy. + return this[kState].referrerPolicy + } + + // Returns the mode associated with request, which is a string indicating + // whether the request will use CORS, or will be restricted to same-origin + // URLs. + get mode () { + webidl.brandCheck(this, Request) + + // The mode getter steps are to return this’s request’s mode. + return this[kState].mode + } + + // Returns the credentials mode associated with request, + // which is a string indicating whether credentials will be sent with the + // request always, never, or only when sent to a same-origin URL. + get credentials () { + // The credentials getter steps are to return this’s request’s credentials mode. + return this[kState].credentials + } + + // Returns the cache mode associated with request, + // which is a string indicating how the request will + // interact with the browser’s cache when fetching. + get cache () { + webidl.brandCheck(this, Request) + + // The cache getter steps are to return this’s request’s cache mode. + return this[kState].cache + } + + // Returns the redirect mode associated with request, + // which is a string indicating how redirects for the + // request will be handled during fetching. A request + // will follow redirects by default. + get redirect () { + webidl.brandCheck(this, Request) + + // The redirect getter steps are to return this’s request’s redirect mode. + return this[kState].redirect + } + + // Returns request’s subresource integrity metadata, which is a + // cryptographic hash of the resource being fetched. Its value + // consists of multiple hashes separated by whitespace. [SRI] + get integrity () { + webidl.brandCheck(this, Request) + + // The integrity getter steps are to return this’s request’s integrity + // metadata. + return this[kState].integrity + } + + // Returns a boolean indicating whether or not request can outlive the + // global in which it was created. + get keepalive () { + webidl.brandCheck(this, Request) + + // The keepalive getter steps are to return this’s request’s keepalive. + return this[kState].keepalive + } + + // Returns a boolean indicating whether or not request is for a reload + // navigation. + get isReloadNavigation () { + webidl.brandCheck(this, Request) + + // The isReloadNavigation getter steps are to return true if this’s + // request’s reload-navigation flag is set; otherwise false. + return this[kState].reloadNavigation + } + + // Returns a boolean indicating whether or not request is for a history + // navigation (a.k.a. back-foward navigation). + get isHistoryNavigation () { + webidl.brandCheck(this, Request) + + // The isHistoryNavigation getter steps are to return true if this’s request’s + // history-navigation flag is set; otherwise false. + return this[kState].historyNavigation + } + + // Returns the signal associated with request, which is an AbortSignal + // object indicating whether or not request has been aborted, and its + // abort event handler. + get signal () { + webidl.brandCheck(this, Request) + + // The signal getter steps are to return this’s signal. + return this[kSignal] + } + + get body () { + webidl.brandCheck(this, Request) + + return this[kState].body ? this[kState].body.stream : null + } + + get bodyUsed () { + webidl.brandCheck(this, Request) + + return !!this[kState].body && util.isDisturbed(this[kState].body.stream) + } + + get duplex () { + webidl.brandCheck(this, Request) + + return 'half' + } + + // Returns a clone of request. + clone () { + webidl.brandCheck(this, Request) + + // 1. If this is unusable, then throw a TypeError. + if (this.bodyUsed || this.body?.locked) { + throw new TypeError('unusable') + } + + // 2. Let clonedRequest be the result of cloning this’s request. + const clonedRequest = cloneRequest(this[kState]) + + // 3. Let clonedRequestObject be the result of creating a Request object, + // given clonedRequest, this’s headers’s guard, and this’s relevant Realm. + const clonedRequestObject = new Request(kConstruct) + clonedRequestObject[kState] = clonedRequest + clonedRequestObject[kRealm] = this[kRealm] + clonedRequestObject[kHeaders] = new Headers(kConstruct) + clonedRequestObject[kHeaders][kHeadersList] = clonedRequest.headersList + clonedRequestObject[kHeaders][kGuard] = this[kHeaders][kGuard] + clonedRequestObject[kHeaders][kRealm] = this[kHeaders][kRealm] + + // 4. Make clonedRequestObject’s signal follow this’s signal. + const ac = new AbortController() + if (this.signal.aborted) { + ac.abort(this.signal.reason) + } else { + util.addAbortListener( + this.signal, + () => { + ac.abort(this.signal.reason) + } + ) + } + clonedRequestObject[kSignal] = ac.signal + + // 4. Return clonedRequestObject. + return clonedRequestObject + } +} + +mixinBody(Request) + +function makeRequest (init) { + // https://fetch.spec.whatwg.org/#requests + const request = { + method: 'GET', + localURLsOnly: false, + unsafeRequest: false, + body: null, + client: null, + reservedClient: null, + replacesClientId: '', + window: 'client', + keepalive: false, + serviceWorkers: 'all', + initiator: '', + destination: '', + priority: null, + origin: 'client', + policyContainer: 'client', + referrer: 'client', + referrerPolicy: '', + mode: 'no-cors', + useCORSPreflightFlag: false, + credentials: 'same-origin', + useCredentials: false, + cache: 'default', + redirect: 'follow', + integrity: '', + cryptoGraphicsNonceMetadata: '', + parserMetadata: '', + reloadNavigation: false, + historyNavigation: false, + userActivation: false, + taintedOrigin: false, + redirectCount: 0, + responseTainting: 'basic', + preventNoCacheCacheControlHeaderModification: false, + done: false, + timingAllowFailed: false, + ...init, + headersList: init.headersList + ? new HeadersList(init.headersList) + : new HeadersList() + } + request.url = request.urlList[0] + return request +} + +// https://fetch.spec.whatwg.org/#concept-request-clone +function cloneRequest (request) { + // To clone a request request, run these steps: + + // 1. Let newRequest be a copy of request, except for its body. + const newRequest = makeRequest({ ...request, body: null }) + + // 2. If request’s body is non-null, set newRequest’s body to the + // result of cloning request’s body. + if (request.body != null) { + newRequest.body = cloneBody(request.body) + } + + // 3. Return newRequest. + return newRequest +} + +Object.defineProperties(Request.prototype, { + method: kEnumerableProperty, + url: kEnumerableProperty, + headers: kEnumerableProperty, + redirect: kEnumerableProperty, + clone: kEnumerableProperty, + signal: kEnumerableProperty, + duplex: kEnumerableProperty, + destination: kEnumerableProperty, + body: kEnumerableProperty, + bodyUsed: kEnumerableProperty, + isHistoryNavigation: kEnumerableProperty, + isReloadNavigation: kEnumerableProperty, + keepalive: kEnumerableProperty, + integrity: kEnumerableProperty, + cache: kEnumerableProperty, + credentials: kEnumerableProperty, + attribute: kEnumerableProperty, + referrerPolicy: kEnumerableProperty, + referrer: kEnumerableProperty, + mode: kEnumerableProperty, + [Symbol.toStringTag]: { + value: 'Request', + configurable: true + } +}) + +webidl.converters.Request = webidl.interfaceConverter( + Request +) + +// https://fetch.spec.whatwg.org/#requestinfo +webidl.converters.RequestInfo = function (V) { + if (typeof V === 'string') { + return webidl.converters.USVString(V) + } + + if (V instanceof Request) { + return webidl.converters.Request(V) + } + + return webidl.converters.USVString(V) +} + +webidl.converters.AbortSignal = webidl.interfaceConverter( + AbortSignal +) + +// https://fetch.spec.whatwg.org/#requestinit +webidl.converters.RequestInit = webidl.dictionaryConverter([ + { + key: 'method', + converter: webidl.converters.ByteString + }, + { + key: 'headers', + converter: webidl.converters.HeadersInit + }, + { + key: 'body', + converter: webidl.nullableConverter( + webidl.converters.BodyInit + ) + }, + { + key: 'referrer', + converter: webidl.converters.USVString + }, + { + key: 'referrerPolicy', + converter: webidl.converters.DOMString, + // https://w3c.github.io/webappsec-referrer-policy/#referrer-policy + allowedValues: referrerPolicy + }, + { + key: 'mode', + converter: webidl.converters.DOMString, + // https://fetch.spec.whatwg.org/#concept-request-mode + allowedValues: requestMode + }, + { + key: 'credentials', + converter: webidl.converters.DOMString, + // https://fetch.spec.whatwg.org/#requestcredentials + allowedValues: requestCredentials + }, + { + key: 'cache', + converter: webidl.converters.DOMString, + // https://fetch.spec.whatwg.org/#requestcache + allowedValues: requestCache + }, + { + key: 'redirect', + converter: webidl.converters.DOMString, + // https://fetch.spec.whatwg.org/#requestredirect + allowedValues: requestRedirect + }, + { + key: 'integrity', + converter: webidl.converters.DOMString + }, + { + key: 'keepalive', + converter: webidl.converters.boolean + }, + { + key: 'signal', + converter: webidl.nullableConverter( + (signal) => webidl.converters.AbortSignal( + signal, + { strict: false } + ) + ) + }, + { + key: 'window', + converter: webidl.converters.any + }, + { + key: 'duplex', + converter: webidl.converters.DOMString, + allowedValues: requestDuplex + } +]) + +module.exports = { Request, makeRequest } + + +/***/ }), + +/***/ 7823: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { Headers, HeadersList, fill } = __nccwpck_require__(554) +const { extractBody, cloneBody, mixinBody } = __nccwpck_require__(1472) +const util = __nccwpck_require__(3983) +const { kEnumerableProperty } = util +const { + isValidReasonPhrase, + isCancelled, + isAborted, + isBlobLike, + serializeJavascriptValueToJSONString, + isErrorLike, + isomorphicEncode +} = __nccwpck_require__(2538) +const { + redirectStatusSet, + nullBodyStatus, + DOMException +} = __nccwpck_require__(1037) +const { kState, kHeaders, kGuard, kRealm } = __nccwpck_require__(5861) +const { webidl } = __nccwpck_require__(1744) +const { FormData } = __nccwpck_require__(2015) +const { getGlobalOrigin } = __nccwpck_require__(1246) +const { URLSerializer } = __nccwpck_require__(685) +const { kHeadersList, kConstruct } = __nccwpck_require__(2785) +const assert = __nccwpck_require__(9491) +const { types } = __nccwpck_require__(3837) + +const ReadableStream = globalThis.ReadableStream || (__nccwpck_require__(5356).ReadableStream) +const textEncoder = new TextEncoder('utf-8') + +// https://fetch.spec.whatwg.org/#response-class +class Response { + // Creates network error Response. + static error () { + // TODO + const relevantRealm = { settingsObject: {} } + + // The static error() method steps are to return the result of creating a + // Response object, given a new network error, "immutable", and this’s + // relevant Realm. + const responseObject = new Response() + responseObject[kState] = makeNetworkError() + responseObject[kRealm] = relevantRealm + responseObject[kHeaders][kHeadersList] = responseObject[kState].headersList + responseObject[kHeaders][kGuard] = 'immutable' + responseObject[kHeaders][kRealm] = relevantRealm + return responseObject + } + + // https://fetch.spec.whatwg.org/#dom-response-json + static json (data, init = {}) { + webidl.argumentLengthCheck(arguments, 1, { header: 'Response.json' }) + + if (init !== null) { + init = webidl.converters.ResponseInit(init) + } + + // 1. Let bytes the result of running serialize a JavaScript value to JSON bytes on data. + const bytes = textEncoder.encode( + serializeJavascriptValueToJSONString(data) + ) + + // 2. Let body be the result of extracting bytes. + const body = extractBody(bytes) + + // 3. Let responseObject be the result of creating a Response object, given a new response, + // "response", and this’s relevant Realm. + const relevantRealm = { settingsObject: {} } + const responseObject = new Response() + responseObject[kRealm] = relevantRealm + responseObject[kHeaders][kGuard] = 'response' + responseObject[kHeaders][kRealm] = relevantRealm + + // 4. Perform initialize a response given responseObject, init, and (body, "application/json"). + initializeResponse(responseObject, init, { body: body[0], type: 'application/json' }) + + // 5. Return responseObject. + return responseObject + } + + // Creates a redirect Response that redirects to url with status status. + static redirect (url, status = 302) { + const relevantRealm = { settingsObject: {} } + + webidl.argumentLengthCheck(arguments, 1, { header: 'Response.redirect' }) + + url = webidl.converters.USVString(url) + status = webidl.converters['unsigned short'](status) + + // 1. Let parsedURL be the result of parsing url with current settings + // object’s API base URL. + // 2. If parsedURL is failure, then throw a TypeError. + // TODO: base-URL? + let parsedURL + try { + parsedURL = new URL(url, getGlobalOrigin()) + } catch (err) { + throw Object.assign(new TypeError('Failed to parse URL from ' + url), { + cause: err + }) + } + + // 3. If status is not a redirect status, then throw a RangeError. + if (!redirectStatusSet.has(status)) { + throw new RangeError('Invalid status code ' + status) + } + + // 4. Let responseObject be the result of creating a Response object, + // given a new response, "immutable", and this’s relevant Realm. + const responseObject = new Response() + responseObject[kRealm] = relevantRealm + responseObject[kHeaders][kGuard] = 'immutable' + responseObject[kHeaders][kRealm] = relevantRealm + + // 5. Set responseObject’s response’s status to status. + responseObject[kState].status = status + + // 6. Let value be parsedURL, serialized and isomorphic encoded. + const value = isomorphicEncode(URLSerializer(parsedURL)) + + // 7. Append `Location`/value to responseObject’s response’s header list. + responseObject[kState].headersList.append('location', value) + + // 8. Return responseObject. + return responseObject + } + + // https://fetch.spec.whatwg.org/#dom-response + constructor (body = null, init = {}) { + if (body !== null) { + body = webidl.converters.BodyInit(body) + } + + init = webidl.converters.ResponseInit(init) + + // TODO + this[kRealm] = { settingsObject: {} } + + // 1. Set this’s response to a new response. + this[kState] = makeResponse({}) + + // 2. Set this’s headers to a new Headers object with this’s relevant + // Realm, whose header list is this’s response’s header list and guard + // is "response". + this[kHeaders] = new Headers(kConstruct) + this[kHeaders][kGuard] = 'response' + this[kHeaders][kHeadersList] = this[kState].headersList + this[kHeaders][kRealm] = this[kRealm] + + // 3. Let bodyWithType be null. + let bodyWithType = null + + // 4. If body is non-null, then set bodyWithType to the result of extracting body. + if (body != null) { + const [extractedBody, type] = extractBody(body) + bodyWithType = { body: extractedBody, type } + } + + // 5. Perform initialize a response given this, init, and bodyWithType. + initializeResponse(this, init, bodyWithType) + } + + // Returns response’s type, e.g., "cors". + get type () { + webidl.brandCheck(this, Response) + + // The type getter steps are to return this’s response’s type. + return this[kState].type + } + + // Returns response’s URL, if it has one; otherwise the empty string. + get url () { + webidl.brandCheck(this, Response) + + const urlList = this[kState].urlList + + // The url getter steps are to return the empty string if this’s + // response’s URL is null; otherwise this’s response’s URL, + // serialized with exclude fragment set to true. + const url = urlList[urlList.length - 1] ?? null + + if (url === null) { + return '' + } + + return URLSerializer(url, true) + } + + // Returns whether response was obtained through a redirect. + get redirected () { + webidl.brandCheck(this, Response) + + // The redirected getter steps are to return true if this’s response’s URL + // list has more than one item; otherwise false. + return this[kState].urlList.length > 1 + } + + // Returns response’s status. + get status () { + webidl.brandCheck(this, Response) + + // The status getter steps are to return this’s response’s status. + return this[kState].status + } + + // Returns whether response’s status is an ok status. + get ok () { + webidl.brandCheck(this, Response) + + // The ok getter steps are to return true if this’s response’s status is an + // ok status; otherwise false. + return this[kState].status >= 200 && this[kState].status <= 299 + } + + // Returns response’s status message. + get statusText () { + webidl.brandCheck(this, Response) + + // The statusText getter steps are to return this’s response’s status + // message. + return this[kState].statusText + } + + // Returns response’s headers as Headers. + get headers () { + webidl.brandCheck(this, Response) + + // The headers getter steps are to return this’s headers. + return this[kHeaders] + } + + get body () { + webidl.brandCheck(this, Response) + + return this[kState].body ? this[kState].body.stream : null + } + + get bodyUsed () { + webidl.brandCheck(this, Response) + + return !!this[kState].body && util.isDisturbed(this[kState].body.stream) + } + + // Returns a clone of response. + clone () { + webidl.brandCheck(this, Response) + + // 1. If this is unusable, then throw a TypeError. + if (this.bodyUsed || (this.body && this.body.locked)) { + throw webidl.errors.exception({ + header: 'Response.clone', + message: 'Body has already been consumed.' + }) + } + + // 2. Let clonedResponse be the result of cloning this’s response. + const clonedResponse = cloneResponse(this[kState]) + + // 3. Return the result of creating a Response object, given + // clonedResponse, this’s headers’s guard, and this’s relevant Realm. + const clonedResponseObject = new Response() + clonedResponseObject[kState] = clonedResponse + clonedResponseObject[kRealm] = this[kRealm] + clonedResponseObject[kHeaders][kHeadersList] = clonedResponse.headersList + clonedResponseObject[kHeaders][kGuard] = this[kHeaders][kGuard] + clonedResponseObject[kHeaders][kRealm] = this[kHeaders][kRealm] + + return clonedResponseObject + } +} + +mixinBody(Response) + +Object.defineProperties(Response.prototype, { + type: kEnumerableProperty, + url: kEnumerableProperty, + status: kEnumerableProperty, + ok: kEnumerableProperty, + redirected: kEnumerableProperty, + statusText: kEnumerableProperty, + headers: kEnumerableProperty, + clone: kEnumerableProperty, + body: kEnumerableProperty, + bodyUsed: kEnumerableProperty, + [Symbol.toStringTag]: { + value: 'Response', + configurable: true + } +}) + +Object.defineProperties(Response, { + json: kEnumerableProperty, + redirect: kEnumerableProperty, + error: kEnumerableProperty +}) + +// https://fetch.spec.whatwg.org/#concept-response-clone +function cloneResponse (response) { + // To clone a response response, run these steps: + + // 1. If response is a filtered response, then return a new identical + // filtered response whose internal response is a clone of response’s + // internal response. + if (response.internalResponse) { + return filterResponse( + cloneResponse(response.internalResponse), + response.type + ) + } + + // 2. Let newResponse be a copy of response, except for its body. + const newResponse = makeResponse({ ...response, body: null }) + + // 3. If response’s body is non-null, then set newResponse’s body to the + // result of cloning response’s body. + if (response.body != null) { + newResponse.body = cloneBody(response.body) + } + + // 4. Return newResponse. + return newResponse +} + +function makeResponse (init) { + return { + aborted: false, + rangeRequested: false, + timingAllowPassed: false, + requestIncludesCredentials: false, + type: 'default', + status: 200, + timingInfo: null, + cacheState: '', + statusText: '', + ...init, + headersList: init.headersList + ? new HeadersList(init.headersList) + : new HeadersList(), + urlList: init.urlList ? [...init.urlList] : [] + } +} + +function makeNetworkError (reason) { + const isError = isErrorLike(reason) + return makeResponse({ + type: 'error', + status: 0, + error: isError + ? reason + : new Error(reason ? String(reason) : reason), + aborted: reason && reason.name === 'AbortError' + }) +} + +function makeFilteredResponse (response, state) { + state = { + internalResponse: response, + ...state + } + + return new Proxy(response, { + get (target, p) { + return p in state ? state[p] : target[p] + }, + set (target, p, value) { + assert(!(p in state)) + target[p] = value + return true + } + }) +} + +// https://fetch.spec.whatwg.org/#concept-filtered-response +function filterResponse (response, type) { + // Set response to the following filtered response with response as its + // internal response, depending on request’s response tainting: + if (type === 'basic') { + // A basic filtered response is a filtered response whose type is "basic" + // and header list excludes any headers in internal response’s header list + // whose name is a forbidden response-header name. + + // Note: undici does not implement forbidden response-header names + return makeFilteredResponse(response, { + type: 'basic', + headersList: response.headersList + }) + } else if (type === 'cors') { + // A CORS filtered response is a filtered response whose type is "cors" + // and header list excludes any headers in internal response’s header + // list whose name is not a CORS-safelisted response-header name, given + // internal response’s CORS-exposed header-name list. + + // Note: undici does not implement CORS-safelisted response-header names + return makeFilteredResponse(response, { + type: 'cors', + headersList: response.headersList + }) + } else if (type === 'opaque') { + // An opaque filtered response is a filtered response whose type is + // "opaque", URL list is the empty list, status is 0, status message + // is the empty byte sequence, header list is empty, and body is null. + + return makeFilteredResponse(response, { + type: 'opaque', + urlList: Object.freeze([]), + status: 0, + statusText: '', + body: null + }) + } else if (type === 'opaqueredirect') { + // An opaque-redirect filtered response is a filtered response whose type + // is "opaqueredirect", status is 0, status message is the empty byte + // sequence, header list is empty, and body is null. + + return makeFilteredResponse(response, { + type: 'opaqueredirect', + status: 0, + statusText: '', + headersList: [], + body: null + }) + } else { + assert(false) + } +} + +// https://fetch.spec.whatwg.org/#appropriate-network-error +function makeAppropriateNetworkError (fetchParams, err = null) { + // 1. Assert: fetchParams is canceled. + assert(isCancelled(fetchParams)) + + // 2. Return an aborted network error if fetchParams is aborted; + // otherwise return a network error. + return isAborted(fetchParams) + ? makeNetworkError(Object.assign(new DOMException('The operation was aborted.', 'AbortError'), { cause: err })) + : makeNetworkError(Object.assign(new DOMException('Request was cancelled.'), { cause: err })) +} + +// https://whatpr.org/fetch/1392.html#initialize-a-response +function initializeResponse (response, init, body) { + // 1. If init["status"] is not in the range 200 to 599, inclusive, then + // throw a RangeError. + if (init.status !== null && (init.status < 200 || init.status > 599)) { + throw new RangeError('init["status"] must be in the range of 200 to 599, inclusive.') + } + + // 2. If init["statusText"] does not match the reason-phrase token production, + // then throw a TypeError. + if ('statusText' in init && init.statusText != null) { + // See, https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2: + // reason-phrase = *( HTAB / SP / VCHAR / obs-text ) + if (!isValidReasonPhrase(String(init.statusText))) { + throw new TypeError('Invalid statusText') + } + } + + // 3. Set response’s response’s status to init["status"]. + if ('status' in init && init.status != null) { + response[kState].status = init.status + } + + // 4. Set response’s response’s status message to init["statusText"]. + if ('statusText' in init && init.statusText != null) { + response[kState].statusText = init.statusText + } + + // 5. If init["headers"] exists, then fill response’s headers with init["headers"]. + if ('headers' in init && init.headers != null) { + fill(response[kHeaders], init.headers) + } + + // 6. If body was given, then: + if (body) { + // 1. If response's status is a null body status, then throw a TypeError. + if (nullBodyStatus.includes(response.status)) { + throw webidl.errors.exception({ + header: 'Response constructor', + message: 'Invalid response status code ' + response.status + }) + } + + // 2. Set response's body to body's body. + response[kState].body = body.body + + // 3. If body's type is non-null and response's header list does not contain + // `Content-Type`, then append (`Content-Type`, body's type) to response's header list. + if (body.type != null && !response[kState].headersList.contains('Content-Type')) { + response[kState].headersList.append('content-type', body.type) + } + } +} + +webidl.converters.ReadableStream = webidl.interfaceConverter( + ReadableStream +) + +webidl.converters.FormData = webidl.interfaceConverter( + FormData +) + +webidl.converters.URLSearchParams = webidl.interfaceConverter( + URLSearchParams +) + +// https://fetch.spec.whatwg.org/#typedefdef-xmlhttprequestbodyinit +webidl.converters.XMLHttpRequestBodyInit = function (V) { + if (typeof V === 'string') { + return webidl.converters.USVString(V) + } + + if (isBlobLike(V)) { + return webidl.converters.Blob(V, { strict: false }) + } + + if (types.isArrayBuffer(V) || types.isTypedArray(V) || types.isDataView(V)) { + return webidl.converters.BufferSource(V) + } + + if (util.isFormDataLike(V)) { + return webidl.converters.FormData(V, { strict: false }) + } + + if (V instanceof URLSearchParams) { + return webidl.converters.URLSearchParams(V) + } + + return webidl.converters.DOMString(V) +} + +// https://fetch.spec.whatwg.org/#bodyinit +webidl.converters.BodyInit = function (V) { + if (V instanceof ReadableStream) { + return webidl.converters.ReadableStream(V) + } + + // Note: the spec doesn't include async iterables, + // this is an undici extension. + if (V?.[Symbol.asyncIterator]) { + return V + } + + return webidl.converters.XMLHttpRequestBodyInit(V) +} + +webidl.converters.ResponseInit = webidl.dictionaryConverter([ + { + key: 'status', + converter: webidl.converters['unsigned short'], + defaultValue: 200 + }, + { + key: 'statusText', + converter: webidl.converters.ByteString, + defaultValue: '' + }, + { + key: 'headers', + converter: webidl.converters.HeadersInit + } +]) + +module.exports = { + makeNetworkError, + makeResponse, + makeAppropriateNetworkError, + filterResponse, + Response, + cloneResponse +} + + +/***/ }), + +/***/ 5861: +/***/ ((module) => { + +"use strict"; + + +module.exports = { + kUrl: Symbol('url'), + kHeaders: Symbol('headers'), + kSignal: Symbol('signal'), + kState: Symbol('state'), + kGuard: Symbol('guard'), + kRealm: Symbol('realm') +} + + +/***/ }), + +/***/ 2538: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { redirectStatusSet, referrerPolicySet: referrerPolicyTokens, badPortsSet } = __nccwpck_require__(1037) +const { getGlobalOrigin } = __nccwpck_require__(1246) +const { performance } = __nccwpck_require__(4074) +const { isBlobLike, toUSVString, ReadableStreamFrom } = __nccwpck_require__(3983) +const assert = __nccwpck_require__(9491) +const { isUint8Array } = __nccwpck_require__(9830) + +// https://nodejs.org/api/crypto.html#determining-if-crypto-support-is-unavailable +/** @type {import('crypto')|undefined} */ +let crypto + +try { + crypto = __nccwpck_require__(6113) +} catch { + +} + +function responseURL (response) { + // https://fetch.spec.whatwg.org/#responses + // A response has an associated URL. It is a pointer to the last URL + // in response’s URL list and null if response’s URL list is empty. + const urlList = response.urlList + const length = urlList.length + return length === 0 ? null : urlList[length - 1].toString() +} + +// https://fetch.spec.whatwg.org/#concept-response-location-url +function responseLocationURL (response, requestFragment) { + // 1. If response’s status is not a redirect status, then return null. + if (!redirectStatusSet.has(response.status)) { + return null + } + + // 2. Let location be the result of extracting header list values given + // `Location` and response’s header list. + let location = response.headersList.get('location') + + // 3. If location is a header value, then set location to the result of + // parsing location with response’s URL. + if (location !== null && isValidHeaderValue(location)) { + location = new URL(location, responseURL(response)) + } + + // 4. If location is a URL whose fragment is null, then set location’s + // fragment to requestFragment. + if (location && !location.hash) { + location.hash = requestFragment + } + + // 5. Return location. + return location +} + +/** @returns {URL} */ +function requestCurrentURL (request) { + return request.urlList[request.urlList.length - 1] +} + +function requestBadPort (request) { + // 1. Let url be request’s current URL. + const url = requestCurrentURL(request) + + // 2. If url’s scheme is an HTTP(S) scheme and url’s port is a bad port, + // then return blocked. + if (urlIsHttpHttpsScheme(url) && badPortsSet.has(url.port)) { + return 'blocked' + } + + // 3. Return allowed. + return 'allowed' +} + +function isErrorLike (object) { + return object instanceof Error || ( + object?.constructor?.name === 'Error' || + object?.constructor?.name === 'DOMException' + ) +} + +// Check whether |statusText| is a ByteString and +// matches the Reason-Phrase token production. +// RFC 2616: https://tools.ietf.org/html/rfc2616 +// RFC 7230: https://tools.ietf.org/html/rfc7230 +// "reason-phrase = *( HTAB / SP / VCHAR / obs-text )" +// https://github.com/chromium/chromium/blob/94.0.4604.1/third_party/blink/renderer/core/fetch/response.cc#L116 +function isValidReasonPhrase (statusText) { + for (let i = 0; i < statusText.length; ++i) { + const c = statusText.charCodeAt(i) + if ( + !( + ( + c === 0x09 || // HTAB + (c >= 0x20 && c <= 0x7e) || // SP / VCHAR + (c >= 0x80 && c <= 0xff) + ) // obs-text + ) + ) { + return false + } + } + return true +} + +/** + * @see https://tools.ietf.org/html/rfc7230#section-3.2.6 + * @param {number} c + */ +function isTokenCharCode (c) { + switch (c) { + case 0x22: + case 0x28: + case 0x29: + case 0x2c: + case 0x2f: + case 0x3a: + case 0x3b: + case 0x3c: + case 0x3d: + case 0x3e: + case 0x3f: + case 0x40: + case 0x5b: + case 0x5c: + case 0x5d: + case 0x7b: + case 0x7d: + // DQUOTE and "(),/:;<=>?@[\]{}" + return false + default: + // VCHAR %x21-7E + return c >= 0x21 && c <= 0x7e + } +} + +/** + * @param {string} characters + */ +function isValidHTTPToken (characters) { + if (characters.length === 0) { + return false + } + for (let i = 0; i < characters.length; ++i) { + if (!isTokenCharCode(characters.charCodeAt(i))) { + return false + } + } + return true +} + +/** + * @see https://fetch.spec.whatwg.org/#header-name + * @param {string} potentialValue + */ +function isValidHeaderName (potentialValue) { + return isValidHTTPToken(potentialValue) +} + +/** + * @see https://fetch.spec.whatwg.org/#header-value + * @param {string} potentialValue + */ +function isValidHeaderValue (potentialValue) { + // - Has no leading or trailing HTTP tab or space bytes. + // - Contains no 0x00 (NUL) or HTTP newline bytes. + if ( + potentialValue.startsWith('\t') || + potentialValue.startsWith(' ') || + potentialValue.endsWith('\t') || + potentialValue.endsWith(' ') + ) { + return false + } + + if ( + potentialValue.includes('\0') || + potentialValue.includes('\r') || + potentialValue.includes('\n') + ) { + return false + } + + return true +} + +// https://w3c.github.io/webappsec-referrer-policy/#set-requests-referrer-policy-on-redirect +function setRequestReferrerPolicyOnRedirect (request, actualResponse) { + // Given a request request and a response actualResponse, this algorithm + // updates request’s referrer policy according to the Referrer-Policy + // header (if any) in actualResponse. + + // 1. Let policy be the result of executing § 8.1 Parse a referrer policy + // from a Referrer-Policy header on actualResponse. + + // 8.1 Parse a referrer policy from a Referrer-Policy header + // 1. Let policy-tokens be the result of extracting header list values given `Referrer-Policy` and response’s header list. + const { headersList } = actualResponse + // 2. Let policy be the empty string. + // 3. For each token in policy-tokens, if token is a referrer policy and token is not the empty string, then set policy to token. + // 4. Return policy. + const policyHeader = (headersList.get('referrer-policy') ?? '').split(',') + + // Note: As the referrer-policy can contain multiple policies + // separated by comma, we need to loop through all of them + // and pick the first valid one. + // Ref: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy#specify_a_fallback_policy + let policy = '' + if (policyHeader.length > 0) { + // The right-most policy takes precedence. + // The left-most policy is the fallback. + for (let i = policyHeader.length; i !== 0; i--) { + const token = policyHeader[i - 1].trim() + if (referrerPolicyTokens.has(token)) { + policy = token + break + } + } + } + + // 2. If policy is not the empty string, then set request’s referrer policy to policy. + if (policy !== '') { + request.referrerPolicy = policy + } +} + +// https://fetch.spec.whatwg.org/#cross-origin-resource-policy-check +function crossOriginResourcePolicyCheck () { + // TODO + return 'allowed' +} + +// https://fetch.spec.whatwg.org/#concept-cors-check +function corsCheck () { + // TODO + return 'success' +} + +// https://fetch.spec.whatwg.org/#concept-tao-check +function TAOCheck () { + // TODO + return 'success' +} + +function appendFetchMetadata (httpRequest) { + // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-dest-header + // TODO + + // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-mode-header + + // 1. Assert: r’s url is a potentially trustworthy URL. + // TODO + + // 2. Let header be a Structured Header whose value is a token. + let header = null + + // 3. Set header’s value to r’s mode. + header = httpRequest.mode + + // 4. Set a structured field value `Sec-Fetch-Mode`/header in r’s header list. + httpRequest.headersList.set('sec-fetch-mode', header) + + // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-site-header + // TODO + + // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-user-header + // TODO +} + +// https://fetch.spec.whatwg.org/#append-a-request-origin-header +function appendRequestOriginHeader (request) { + // 1. Let serializedOrigin be the result of byte-serializing a request origin with request. + let serializedOrigin = request.origin + + // 2. If request’s response tainting is "cors" or request’s mode is "websocket", then append (`Origin`, serializedOrigin) to request’s header list. + if (request.responseTainting === 'cors' || request.mode === 'websocket') { + if (serializedOrigin) { + request.headersList.append('origin', serializedOrigin) + } + + // 3. Otherwise, if request’s method is neither `GET` nor `HEAD`, then: + } else if (request.method !== 'GET' && request.method !== 'HEAD') { + // 1. Switch on request’s referrer policy: + switch (request.referrerPolicy) { + case 'no-referrer': + // Set serializedOrigin to `null`. + serializedOrigin = null + break + case 'no-referrer-when-downgrade': + case 'strict-origin': + case 'strict-origin-when-cross-origin': + // If request’s origin is a tuple origin, its scheme is "https", and request’s current URL’s scheme is not "https", then set serializedOrigin to `null`. + if (request.origin && urlHasHttpsScheme(request.origin) && !urlHasHttpsScheme(requestCurrentURL(request))) { + serializedOrigin = null + } + break + case 'same-origin': + // If request’s origin is not same origin with request’s current URL’s origin, then set serializedOrigin to `null`. + if (!sameOrigin(request, requestCurrentURL(request))) { + serializedOrigin = null + } + break + default: + // Do nothing. + } + + if (serializedOrigin) { + // 2. Append (`Origin`, serializedOrigin) to request’s header list. + request.headersList.append('origin', serializedOrigin) + } + } +} + +function coarsenedSharedCurrentTime (crossOriginIsolatedCapability) { + // TODO + return performance.now() +} + +// https://fetch.spec.whatwg.org/#create-an-opaque-timing-info +function createOpaqueTimingInfo (timingInfo) { + return { + startTime: timingInfo.startTime ?? 0, + redirectStartTime: 0, + redirectEndTime: 0, + postRedirectStartTime: timingInfo.startTime ?? 0, + finalServiceWorkerStartTime: 0, + finalNetworkResponseStartTime: 0, + finalNetworkRequestStartTime: 0, + endTime: 0, + encodedBodySize: 0, + decodedBodySize: 0, + finalConnectionTimingInfo: null + } +} + +// https://html.spec.whatwg.org/multipage/origin.html#policy-container +function makePolicyContainer () { + // Note: the fetch spec doesn't make use of embedder policy or CSP list + return { + referrerPolicy: 'strict-origin-when-cross-origin' + } +} + +// https://html.spec.whatwg.org/multipage/origin.html#clone-a-policy-container +function clonePolicyContainer (policyContainer) { + return { + referrerPolicy: policyContainer.referrerPolicy + } +} + +// https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer +function determineRequestsReferrer (request) { + // 1. Let policy be request's referrer policy. + const policy = request.referrerPolicy + + // Note: policy cannot (shouldn't) be null or an empty string. + assert(policy) + + // 2. Let environment be request’s client. + + let referrerSource = null + + // 3. Switch on request’s referrer: + if (request.referrer === 'client') { + // Note: node isn't a browser and doesn't implement document/iframes, + // so we bypass this step and replace it with our own. + + const globalOrigin = getGlobalOrigin() + + if (!globalOrigin || globalOrigin.origin === 'null') { + return 'no-referrer' + } + + // note: we need to clone it as it's mutated + referrerSource = new URL(globalOrigin) + } else if (request.referrer instanceof URL) { + // Let referrerSource be request’s referrer. + referrerSource = request.referrer + } + + // 4. Let request’s referrerURL be the result of stripping referrerSource for + // use as a referrer. + let referrerURL = stripURLForReferrer(referrerSource) + + // 5. Let referrerOrigin be the result of stripping referrerSource for use as + // a referrer, with the origin-only flag set to true. + const referrerOrigin = stripURLForReferrer(referrerSource, true) + + // 6. If the result of serializing referrerURL is a string whose length is + // greater than 4096, set referrerURL to referrerOrigin. + if (referrerURL.toString().length > 4096) { + referrerURL = referrerOrigin + } + + const areSameOrigin = sameOrigin(request, referrerURL) + const isNonPotentiallyTrustWorthy = isURLPotentiallyTrustworthy(referrerURL) && + !isURLPotentiallyTrustworthy(request.url) + + // 8. Execute the switch statements corresponding to the value of policy: + switch (policy) { + case 'origin': return referrerOrigin != null ? referrerOrigin : stripURLForReferrer(referrerSource, true) + case 'unsafe-url': return referrerURL + case 'same-origin': + return areSameOrigin ? referrerOrigin : 'no-referrer' + case 'origin-when-cross-origin': + return areSameOrigin ? referrerURL : referrerOrigin + case 'strict-origin-when-cross-origin': { + const currentURL = requestCurrentURL(request) + + // 1. If the origin of referrerURL and the origin of request’s current + // URL are the same, then return referrerURL. + if (sameOrigin(referrerURL, currentURL)) { + return referrerURL + } + + // 2. If referrerURL is a potentially trustworthy URL and request’s + // current URL is not a potentially trustworthy URL, then return no + // referrer. + if (isURLPotentiallyTrustworthy(referrerURL) && !isURLPotentiallyTrustworthy(currentURL)) { + return 'no-referrer' + } + + // 3. Return referrerOrigin. + return referrerOrigin + } + case 'strict-origin': // eslint-disable-line + /** + * 1. If referrerURL is a potentially trustworthy URL and + * request’s current URL is not a potentially trustworthy URL, + * then return no referrer. + * 2. Return referrerOrigin + */ + case 'no-referrer-when-downgrade': // eslint-disable-line + /** + * 1. If referrerURL is a potentially trustworthy URL and + * request’s current URL is not a potentially trustworthy URL, + * then return no referrer. + * 2. Return referrerOrigin + */ + + default: // eslint-disable-line + return isNonPotentiallyTrustWorthy ? 'no-referrer' : referrerOrigin + } +} + +/** + * @see https://w3c.github.io/webappsec-referrer-policy/#strip-url + * @param {URL} url + * @param {boolean|undefined} originOnly + */ +function stripURLForReferrer (url, originOnly) { + // 1. Assert: url is a URL. + assert(url instanceof URL) + + // 2. If url’s scheme is a local scheme, then return no referrer. + if (url.protocol === 'file:' || url.protocol === 'about:' || url.protocol === 'blank:') { + return 'no-referrer' + } + + // 3. Set url’s username to the empty string. + url.username = '' + + // 4. Set url’s password to the empty string. + url.password = '' + + // 5. Set url’s fragment to null. + url.hash = '' + + // 6. If the origin-only flag is true, then: + if (originOnly) { + // 1. Set url’s path to « the empty string ». + url.pathname = '' + + // 2. Set url’s query to null. + url.search = '' + } + + // 7. Return url. + return url +} + +function isURLPotentiallyTrustworthy (url) { + if (!(url instanceof URL)) { + return false + } + + // If child of about, return true + if (url.href === 'about:blank' || url.href === 'about:srcdoc') { + return true + } + + // If scheme is data, return true + if (url.protocol === 'data:') return true + + // If file, return true + if (url.protocol === 'file:') return true + + return isOriginPotentiallyTrustworthy(url.origin) + + function isOriginPotentiallyTrustworthy (origin) { + // If origin is explicitly null, return false + if (origin == null || origin === 'null') return false + + const originAsURL = new URL(origin) + + // If secure, return true + if (originAsURL.protocol === 'https:' || originAsURL.protocol === 'wss:') { + return true + } + + // If localhost or variants, return true + if (/^127(?:\.[0-9]+){0,2}\.[0-9]+$|^\[(?:0*:)*?:?0*1\]$/.test(originAsURL.hostname) || + (originAsURL.hostname === 'localhost' || originAsURL.hostname.includes('localhost.')) || + (originAsURL.hostname.endsWith('.localhost'))) { + return true + } + + // If any other, return false + return false + } +} + +/** + * @see https://w3c.github.io/webappsec-subresource-integrity/#does-response-match-metadatalist + * @param {Uint8Array} bytes + * @param {string} metadataList + */ +function bytesMatch (bytes, metadataList) { + // If node is not built with OpenSSL support, we cannot check + // a request's integrity, so allow it by default (the spec will + // allow requests if an invalid hash is given, as precedence). + /* istanbul ignore if: only if node is built with --without-ssl */ + if (crypto === undefined) { + return true + } + + // 1. Let parsedMetadata be the result of parsing metadataList. + const parsedMetadata = parseMetadata(metadataList) + + // 2. If parsedMetadata is no metadata, return true. + if (parsedMetadata === 'no metadata') { + return true + } + + // 3. If parsedMetadata is the empty set, return true. + if (parsedMetadata.length === 0) { + return true + } + + // 4. Let metadata be the result of getting the strongest + // metadata from parsedMetadata. + const list = parsedMetadata.sort((c, d) => d.algo.localeCompare(c.algo)) + // get the strongest algorithm + const strongest = list[0].algo + // get all entries that use the strongest algorithm; ignore weaker + const metadata = list.filter((item) => item.algo === strongest) + + // 5. For each item in metadata: + for (const item of metadata) { + // 1. Let algorithm be the alg component of item. + const algorithm = item.algo + + // 2. Let expectedValue be the val component of item. + let expectedValue = item.hash + + // See https://github.com/web-platform-tests/wpt/commit/e4c5cc7a5e48093220528dfdd1c4012dc3837a0e + // "be liberal with padding". This is annoying, and it's not even in the spec. + + if (expectedValue.endsWith('==')) { + expectedValue = expectedValue.slice(0, -2) + } + + // 3. Let actualValue be the result of applying algorithm to bytes. + let actualValue = crypto.createHash(algorithm).update(bytes).digest('base64') + + if (actualValue.endsWith('==')) { + actualValue = actualValue.slice(0, -2) + } + + // 4. If actualValue is a case-sensitive match for expectedValue, + // return true. + if (actualValue === expectedValue) { + return true + } + + let actualBase64URL = crypto.createHash(algorithm).update(bytes).digest('base64url') + + if (actualBase64URL.endsWith('==')) { + actualBase64URL = actualBase64URL.slice(0, -2) + } + + if (actualBase64URL === expectedValue) { + return true + } + } + + // 6. Return false. + return false +} + +// https://w3c.github.io/webappsec-subresource-integrity/#grammardef-hash-with-options +// https://www.w3.org/TR/CSP2/#source-list-syntax +// https://www.rfc-editor.org/rfc/rfc5234#appendix-B.1 +const parseHashWithOptions = /((?sha256|sha384|sha512)-(?[A-z0-9+/]{1}.*={0,2}))( +[\x21-\x7e]?)?/i + +/** + * @see https://w3c.github.io/webappsec-subresource-integrity/#parse-metadata + * @param {string} metadata + */ +function parseMetadata (metadata) { + // 1. Let result be the empty set. + /** @type {{ algo: string, hash: string }[]} */ + const result = [] + + // 2. Let empty be equal to true. + let empty = true + + const supportedHashes = crypto.getHashes() + + // 3. For each token returned by splitting metadata on spaces: + for (const token of metadata.split(' ')) { + // 1. Set empty to false. + empty = false + + // 2. Parse token as a hash-with-options. + const parsedToken = parseHashWithOptions.exec(token) + + // 3. If token does not parse, continue to the next token. + if (parsedToken === null || parsedToken.groups === undefined) { + // Note: Chromium blocks the request at this point, but Firefox + // gives a warning that an invalid integrity was given. The + // correct behavior is to ignore these, and subsequently not + // check the integrity of the resource. + continue + } + + // 4. Let algorithm be the hash-algo component of token. + const algorithm = parsedToken.groups.algo + + // 5. If algorithm is a hash function recognized by the user + // agent, add the parsed token to result. + if (supportedHashes.includes(algorithm.toLowerCase())) { + result.push(parsedToken.groups) + } + } + + // 4. Return no metadata if empty is true, otherwise return result. + if (empty === true) { + return 'no metadata' + } + + return result +} + +// https://w3c.github.io/webappsec-upgrade-insecure-requests/#upgrade-request +function tryUpgradeRequestToAPotentiallyTrustworthyURL (request) { + // TODO +} + +/** + * @link {https://html.spec.whatwg.org/multipage/origin.html#same-origin} + * @param {URL} A + * @param {URL} B + */ +function sameOrigin (A, B) { + // 1. If A and B are the same opaque origin, then return true. + if (A.origin === B.origin && A.origin === 'null') { + return true + } + + // 2. If A and B are both tuple origins and their schemes, + // hosts, and port are identical, then return true. + if (A.protocol === B.protocol && A.hostname === B.hostname && A.port === B.port) { + return true + } + + // 3. Return false. + return false +} + +function createDeferredPromise () { + let res + let rej + const promise = new Promise((resolve, reject) => { + res = resolve + rej = reject + }) + + return { promise, resolve: res, reject: rej } +} + +function isAborted (fetchParams) { + return fetchParams.controller.state === 'aborted' +} + +function isCancelled (fetchParams) { + return fetchParams.controller.state === 'aborted' || + fetchParams.controller.state === 'terminated' +} + +const normalizeMethodRecord = { + delete: 'DELETE', + DELETE: 'DELETE', + get: 'GET', + GET: 'GET', + head: 'HEAD', + HEAD: 'HEAD', + options: 'OPTIONS', + OPTIONS: 'OPTIONS', + post: 'POST', + POST: 'POST', + put: 'PUT', + PUT: 'PUT' +} + +// Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`. +Object.setPrototypeOf(normalizeMethodRecord, null) + +/** + * @see https://fetch.spec.whatwg.org/#concept-method-normalize + * @param {string} method + */ +function normalizeMethod (method) { + return normalizeMethodRecord[method.toLowerCase()] ?? method +} + +// https://infra.spec.whatwg.org/#serialize-a-javascript-value-to-a-json-string +function serializeJavascriptValueToJSONString (value) { + // 1. Let result be ? Call(%JSON.stringify%, undefined, « value »). + const result = JSON.stringify(value) + + // 2. If result is undefined, then throw a TypeError. + if (result === undefined) { + throw new TypeError('Value is not JSON serializable') + } + + // 3. Assert: result is a string. + assert(typeof result === 'string') + + // 4. Return result. + return result +} + +// https://tc39.es/ecma262/#sec-%25iteratorprototype%25-object +const esIteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]())) + +/** + * @see https://webidl.spec.whatwg.org/#dfn-iterator-prototype-object + * @param {() => unknown[]} iterator + * @param {string} name name of the instance + * @param {'key'|'value'|'key+value'} kind + */ +function makeIterator (iterator, name, kind) { + const object = { + index: 0, + kind, + target: iterator + } + + const i = { + next () { + // 1. Let interface be the interface for which the iterator prototype object exists. + + // 2. Let thisValue be the this value. + + // 3. Let object be ? ToObject(thisValue). + + // 4. If object is a platform object, then perform a security + // check, passing: + + // 5. If object is not a default iterator object for interface, + // then throw a TypeError. + if (Object.getPrototypeOf(this) !== i) { + throw new TypeError( + `'next' called on an object that does not implement interface ${name} Iterator.` + ) + } + + // 6. Let index be object’s index. + // 7. Let kind be object’s kind. + // 8. Let values be object’s target's value pairs to iterate over. + const { index, kind, target } = object + const values = target() + + // 9. Let len be the length of values. + const len = values.length + + // 10. If index is greater than or equal to len, then return + // CreateIterResultObject(undefined, true). + if (index >= len) { + return { value: undefined, done: true } + } + + // 11. Let pair be the entry in values at index index. + const pair = values[index] + + // 12. Set object’s index to index + 1. + object.index = index + 1 + + // 13. Return the iterator result for pair and kind. + return iteratorResult(pair, kind) + }, + // The class string of an iterator prototype object for a given interface is the + // result of concatenating the identifier of the interface and the string " Iterator". + [Symbol.toStringTag]: `${name} Iterator` + } + + // The [[Prototype]] internal slot of an iterator prototype object must be %IteratorPrototype%. + Object.setPrototypeOf(i, esIteratorPrototype) + // esIteratorPrototype needs to be the prototype of i + // which is the prototype of an empty object. Yes, it's confusing. + return Object.setPrototypeOf({}, i) +} + +// https://webidl.spec.whatwg.org/#iterator-result +function iteratorResult (pair, kind) { + let result + + // 1. Let result be a value determined by the value of kind: + switch (kind) { + case 'key': { + // 1. Let idlKey be pair’s key. + // 2. Let key be the result of converting idlKey to an + // ECMAScript value. + // 3. result is key. + result = pair[0] + break + } + case 'value': { + // 1. Let idlValue be pair’s value. + // 2. Let value be the result of converting idlValue to + // an ECMAScript value. + // 3. result is value. + result = pair[1] + break + } + case 'key+value': { + // 1. Let idlKey be pair’s key. + // 2. Let idlValue be pair’s value. + // 3. Let key be the result of converting idlKey to an + // ECMAScript value. + // 4. Let value be the result of converting idlValue to + // an ECMAScript value. + // 5. Let array be ! ArrayCreate(2). + // 6. Call ! CreateDataProperty(array, "0", key). + // 7. Call ! CreateDataProperty(array, "1", value). + // 8. result is array. + result = pair + break + } + } + + // 2. Return CreateIterResultObject(result, false). + return { value: result, done: false } +} + +/** + * @see https://fetch.spec.whatwg.org/#body-fully-read + */ +async function fullyReadBody (body, processBody, processBodyError) { + // 1. If taskDestination is null, then set taskDestination to + // the result of starting a new parallel queue. + + // 2. Let successSteps given a byte sequence bytes be to queue a + // fetch task to run processBody given bytes, with taskDestination. + const successSteps = processBody + + // 3. Let errorSteps be to queue a fetch task to run processBodyError, + // with taskDestination. + const errorSteps = processBodyError + + // 4. Let reader be the result of getting a reader for body’s stream. + // If that threw an exception, then run errorSteps with that + // exception and return. + let reader + + try { + reader = body.stream.getReader() + } catch (e) { + errorSteps(e) + return + } + + // 5. Read all bytes from reader, given successSteps and errorSteps. + try { + const result = await readAllBytes(reader) + successSteps(result) + } catch (e) { + errorSteps(e) + } +} + +/** @type {ReadableStream} */ +let ReadableStream = globalThis.ReadableStream + +function isReadableStreamLike (stream) { + if (!ReadableStream) { + ReadableStream = (__nccwpck_require__(5356).ReadableStream) + } + + return stream instanceof ReadableStream || ( + stream[Symbol.toStringTag] === 'ReadableStream' && + typeof stream.tee === 'function' + ) +} + +const MAXIMUM_ARGUMENT_LENGTH = 65535 + +/** + * @see https://infra.spec.whatwg.org/#isomorphic-decode + * @param {number[]|Uint8Array} input + */ +function isomorphicDecode (input) { + // 1. To isomorphic decode a byte sequence input, return a string whose code point + // length is equal to input’s length and whose code points have the same values + // as the values of input’s bytes, in the same order. + + if (input.length < MAXIMUM_ARGUMENT_LENGTH) { + return String.fromCharCode(...input) + } + + return input.reduce((previous, current) => previous + String.fromCharCode(current), '') +} + +/** + * @param {ReadableStreamController} controller + */ +function readableStreamClose (controller) { + try { + controller.close() + } catch (err) { + // TODO: add comment explaining why this error occurs. + if (!err.message.includes('Controller is already closed')) { + throw err + } + } +} + +/** + * @see https://infra.spec.whatwg.org/#isomorphic-encode + * @param {string} input + */ +function isomorphicEncode (input) { + // 1. Assert: input contains no code points greater than U+00FF. + for (let i = 0; i < input.length; i++) { + assert(input.charCodeAt(i) <= 0xFF) + } + + // 2. Return a byte sequence whose length is equal to input’s code + // point length and whose bytes have the same values as the + // values of input’s code points, in the same order + return input +} + +/** + * @see https://streams.spec.whatwg.org/#readablestreamdefaultreader-read-all-bytes + * @see https://streams.spec.whatwg.org/#read-loop + * @param {ReadableStreamDefaultReader} reader + */ +async function readAllBytes (reader) { + const bytes = [] + let byteLength = 0 + + while (true) { + const { done, value: chunk } = await reader.read() + + if (done) { + // 1. Call successSteps with bytes. + return Buffer.concat(bytes, byteLength) + } + + // 1. If chunk is not a Uint8Array object, call failureSteps + // with a TypeError and abort these steps. + if (!isUint8Array(chunk)) { + throw new TypeError('Received non-Uint8Array chunk') + } + + // 2. Append the bytes represented by chunk to bytes. + bytes.push(chunk) + byteLength += chunk.length + + // 3. Read-loop given reader, bytes, successSteps, and failureSteps. + } +} + +/** + * @see https://fetch.spec.whatwg.org/#is-local + * @param {URL} url + */ +function urlIsLocal (url) { + assert('protocol' in url) // ensure it's a url object + + const protocol = url.protocol + + return protocol === 'about:' || protocol === 'blob:' || protocol === 'data:' +} + +/** + * @param {string|URL} url + */ +function urlHasHttpsScheme (url) { + if (typeof url === 'string') { + return url.startsWith('https:') + } + + return url.protocol === 'https:' +} + +/** + * @see https://fetch.spec.whatwg.org/#http-scheme + * @param {URL} url + */ +function urlIsHttpHttpsScheme (url) { + assert('protocol' in url) // ensure it's a url object + + const protocol = url.protocol + + return protocol === 'http:' || protocol === 'https:' +} + +/** + * Fetch supports node >= 16.8.0, but Object.hasOwn was added in v16.9.0. + */ +const hasOwn = Object.hasOwn || ((dict, key) => Object.prototype.hasOwnProperty.call(dict, key)) + +module.exports = { + isAborted, + isCancelled, + createDeferredPromise, + ReadableStreamFrom, + toUSVString, + tryUpgradeRequestToAPotentiallyTrustworthyURL, + coarsenedSharedCurrentTime, + determineRequestsReferrer, + makePolicyContainer, + clonePolicyContainer, + appendFetchMetadata, + appendRequestOriginHeader, + TAOCheck, + corsCheck, + crossOriginResourcePolicyCheck, + createOpaqueTimingInfo, + setRequestReferrerPolicyOnRedirect, + isValidHTTPToken, + requestBadPort, + requestCurrentURL, + responseURL, + responseLocationURL, + isBlobLike, + isURLPotentiallyTrustworthy, + isValidReasonPhrase, + sameOrigin, + normalizeMethod, + serializeJavascriptValueToJSONString, + makeIterator, + isValidHeaderName, + isValidHeaderValue, + hasOwn, + isErrorLike, + fullyReadBody, + bytesMatch, + isReadableStreamLike, + readableStreamClose, + isomorphicEncode, + isomorphicDecode, + urlIsLocal, + urlHasHttpsScheme, + urlIsHttpHttpsScheme, + readAllBytes, + normalizeMethodRecord +} + + +/***/ }), + +/***/ 1744: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { types } = __nccwpck_require__(3837) +const { hasOwn, toUSVString } = __nccwpck_require__(2538) + +/** @type {import('../../types/webidl').Webidl} */ +const webidl = {} +webidl.converters = {} +webidl.util = {} +webidl.errors = {} + +webidl.errors.exception = function (message) { + return new TypeError(`${message.header}: ${message.message}`) +} + +webidl.errors.conversionFailed = function (context) { + const plural = context.types.length === 1 ? '' : ' one of' + const message = + `${context.argument} could not be converted to` + + `${plural}: ${context.types.join(', ')}.` + + return webidl.errors.exception({ + header: context.prefix, + message + }) +} + +webidl.errors.invalidArgument = function (context) { + return webidl.errors.exception({ + header: context.prefix, + message: `"${context.value}" is an invalid ${context.type}.` + }) +} + +// https://webidl.spec.whatwg.org/#implements +webidl.brandCheck = function (V, I, opts = undefined) { + if (opts?.strict !== false && !(V instanceof I)) { + throw new TypeError('Illegal invocation') + } else { + return V?.[Symbol.toStringTag] === I.prototype[Symbol.toStringTag] + } +} + +webidl.argumentLengthCheck = function ({ length }, min, ctx) { + if (length < min) { + throw webidl.errors.exception({ + message: `${min} argument${min !== 1 ? 's' : ''} required, ` + + `but${length ? ' only' : ''} ${length} found.`, + ...ctx + }) + } +} + +webidl.illegalConstructor = function () { + throw webidl.errors.exception({ + header: 'TypeError', + message: 'Illegal constructor' + }) +} + +// https://tc39.es/ecma262/#sec-ecmascript-data-types-and-values +webidl.util.Type = function (V) { + switch (typeof V) { + case 'undefined': return 'Undefined' + case 'boolean': return 'Boolean' + case 'string': return 'String' + case 'symbol': return 'Symbol' + case 'number': return 'Number' + case 'bigint': return 'BigInt' + case 'function': + case 'object': { + if (V === null) { + return 'Null' + } + + return 'Object' + } + } +} + +// https://webidl.spec.whatwg.org/#abstract-opdef-converttoint +webidl.util.ConvertToInt = function (V, bitLength, signedness, opts = {}) { + let upperBound + let lowerBound + + // 1. If bitLength is 64, then: + if (bitLength === 64) { + // 1. Let upperBound be 2^53 − 1. + upperBound = Math.pow(2, 53) - 1 + + // 2. If signedness is "unsigned", then let lowerBound be 0. + if (signedness === 'unsigned') { + lowerBound = 0 + } else { + // 3. Otherwise let lowerBound be −2^53 + 1. + lowerBound = Math.pow(-2, 53) + 1 + } + } else if (signedness === 'unsigned') { + // 2. Otherwise, if signedness is "unsigned", then: + + // 1. Let lowerBound be 0. + lowerBound = 0 + + // 2. Let upperBound be 2^bitLength − 1. + upperBound = Math.pow(2, bitLength) - 1 + } else { + // 3. Otherwise: + + // 1. Let lowerBound be -2^bitLength − 1. + lowerBound = Math.pow(-2, bitLength) - 1 + + // 2. Let upperBound be 2^bitLength − 1 − 1. + upperBound = Math.pow(2, bitLength - 1) - 1 + } + + // 4. Let x be ? ToNumber(V). + let x = Number(V) + + // 5. If x is −0, then set x to +0. + if (x === 0) { + x = 0 + } + + // 6. If the conversion is to an IDL type associated + // with the [EnforceRange] extended attribute, then: + if (opts.enforceRange === true) { + // 1. If x is NaN, +∞, or −∞, then throw a TypeError. + if ( + Number.isNaN(x) || + x === Number.POSITIVE_INFINITY || + x === Number.NEGATIVE_INFINITY + ) { + throw webidl.errors.exception({ + header: 'Integer conversion', + message: `Could not convert ${V} to an integer.` + }) + } + + // 2. Set x to IntegerPart(x). + x = webidl.util.IntegerPart(x) + + // 3. If x < lowerBound or x > upperBound, then + // throw a TypeError. + if (x < lowerBound || x > upperBound) { + throw webidl.errors.exception({ + header: 'Integer conversion', + message: `Value must be between ${lowerBound}-${upperBound}, got ${x}.` + }) + } + + // 4. Return x. + return x + } + + // 7. If x is not NaN and the conversion is to an IDL + // type associated with the [Clamp] extended + // attribute, then: + if (!Number.isNaN(x) && opts.clamp === true) { + // 1. Set x to min(max(x, lowerBound), upperBound). + x = Math.min(Math.max(x, lowerBound), upperBound) + + // 2. Round x to the nearest integer, choosing the + // even integer if it lies halfway between two, + // and choosing +0 rather than −0. + if (Math.floor(x) % 2 === 0) { + x = Math.floor(x) + } else { + x = Math.ceil(x) + } + + // 3. Return x. + return x + } + + // 8. If x is NaN, +0, +∞, or −∞, then return +0. + if ( + Number.isNaN(x) || + (x === 0 && Object.is(0, x)) || + x === Number.POSITIVE_INFINITY || + x === Number.NEGATIVE_INFINITY + ) { + return 0 + } + + // 9. Set x to IntegerPart(x). + x = webidl.util.IntegerPart(x) + + // 10. Set x to x modulo 2^bitLength. + x = x % Math.pow(2, bitLength) + + // 11. If signedness is "signed" and x ≥ 2^bitLength − 1, + // then return x − 2^bitLength. + if (signedness === 'signed' && x >= Math.pow(2, bitLength) - 1) { + return x - Math.pow(2, bitLength) + } + + // 12. Otherwise, return x. + return x +} + +// https://webidl.spec.whatwg.org/#abstract-opdef-integerpart +webidl.util.IntegerPart = function (n) { + // 1. Let r be floor(abs(n)). + const r = Math.floor(Math.abs(n)) + + // 2. If n < 0, then return -1 × r. + if (n < 0) { + return -1 * r + } + + // 3. Otherwise, return r. + return r +} + +// https://webidl.spec.whatwg.org/#es-sequence +webidl.sequenceConverter = function (converter) { + return (V) => { + // 1. If Type(V) is not Object, throw a TypeError. + if (webidl.util.Type(V) !== 'Object') { + throw webidl.errors.exception({ + header: 'Sequence', + message: `Value of type ${webidl.util.Type(V)} is not an Object.` + }) + } + + // 2. Let method be ? GetMethod(V, @@iterator). + /** @type {Generator} */ + const method = V?.[Symbol.iterator]?.() + const seq = [] + + // 3. If method is undefined, throw a TypeError. + if ( + method === undefined || + typeof method.next !== 'function' + ) { + throw webidl.errors.exception({ + header: 'Sequence', + message: 'Object is not an iterator.' + }) + } + + // https://webidl.spec.whatwg.org/#create-sequence-from-iterable + while (true) { + const { done, value } = method.next() + + if (done) { + break + } + + seq.push(converter(value)) + } + + return seq + } +} + +// https://webidl.spec.whatwg.org/#es-to-record +webidl.recordConverter = function (keyConverter, valueConverter) { + return (O) => { + // 1. If Type(O) is not Object, throw a TypeError. + if (webidl.util.Type(O) !== 'Object') { + throw webidl.errors.exception({ + header: 'Record', + message: `Value of type ${webidl.util.Type(O)} is not an Object.` + }) + } + + // 2. Let result be a new empty instance of record. + const result = {} + + if (!types.isProxy(O)) { + // Object.keys only returns enumerable properties + const keys = Object.keys(O) + + for (const key of keys) { + // 1. Let typedKey be key converted to an IDL value of type K. + const typedKey = keyConverter(key) + + // 2. Let value be ? Get(O, key). + // 3. Let typedValue be value converted to an IDL value of type V. + const typedValue = valueConverter(O[key]) + + // 4. Set result[typedKey] to typedValue. + result[typedKey] = typedValue + } + + // 5. Return result. + return result + } + + // 3. Let keys be ? O.[[OwnPropertyKeys]](). + const keys = Reflect.ownKeys(O) + + // 4. For each key of keys. + for (const key of keys) { + // 1. Let desc be ? O.[[GetOwnProperty]](key). + const desc = Reflect.getOwnPropertyDescriptor(O, key) + + // 2. If desc is not undefined and desc.[[Enumerable]] is true: + if (desc?.enumerable) { + // 1. Let typedKey be key converted to an IDL value of type K. + const typedKey = keyConverter(key) + + // 2. Let value be ? Get(O, key). + // 3. Let typedValue be value converted to an IDL value of type V. + const typedValue = valueConverter(O[key]) + + // 4. Set result[typedKey] to typedValue. + result[typedKey] = typedValue + } + } + + // 5. Return result. + return result + } +} + +webidl.interfaceConverter = function (i) { + return (V, opts = {}) => { + if (opts.strict !== false && !(V instanceof i)) { + throw webidl.errors.exception({ + header: i.name, + message: `Expected ${V} to be an instance of ${i.name}.` + }) + } + + return V + } +} + +webidl.dictionaryConverter = function (converters) { + return (dictionary) => { + const type = webidl.util.Type(dictionary) + const dict = {} + + if (type === 'Null' || type === 'Undefined') { + return dict + } else if (type !== 'Object') { + throw webidl.errors.exception({ + header: 'Dictionary', + message: `Expected ${dictionary} to be one of: Null, Undefined, Object.` + }) + } + + for (const options of converters) { + const { key, defaultValue, required, converter } = options + + if (required === true) { + if (!hasOwn(dictionary, key)) { + throw webidl.errors.exception({ + header: 'Dictionary', + message: `Missing required key "${key}".` + }) + } + } + + let value = dictionary[key] + const hasDefault = hasOwn(options, 'defaultValue') + + // Only use defaultValue if value is undefined and + // a defaultValue options was provided. + if (hasDefault && value !== null) { + value = value ?? defaultValue + } + + // A key can be optional and have no default value. + // When this happens, do not perform a conversion, + // and do not assign the key a value. + if (required || hasDefault || value !== undefined) { + value = converter(value) + + if ( + options.allowedValues && + !options.allowedValues.includes(value) + ) { + throw webidl.errors.exception({ + header: 'Dictionary', + message: `${value} is not an accepted type. Expected one of ${options.allowedValues.join(', ')}.` + }) + } + + dict[key] = value + } + } + + return dict + } +} + +webidl.nullableConverter = function (converter) { + return (V) => { + if (V === null) { + return V + } + + return converter(V) + } +} + +// https://webidl.spec.whatwg.org/#es-DOMString +webidl.converters.DOMString = function (V, opts = {}) { + // 1. If V is null and the conversion is to an IDL type + // associated with the [LegacyNullToEmptyString] + // extended attribute, then return the DOMString value + // that represents the empty string. + if (V === null && opts.legacyNullToEmptyString) { + return '' + } + + // 2. Let x be ? ToString(V). + if (typeof V === 'symbol') { + throw new TypeError('Could not convert argument of type symbol to string.') + } + + // 3. Return the IDL DOMString value that represents the + // same sequence of code units as the one the + // ECMAScript String value x represents. + return String(V) +} + +// https://webidl.spec.whatwg.org/#es-ByteString +webidl.converters.ByteString = function (V) { + // 1. Let x be ? ToString(V). + // Note: DOMString converter perform ? ToString(V) + const x = webidl.converters.DOMString(V) + + // 2. If the value of any element of x is greater than + // 255, then throw a TypeError. + for (let index = 0; index < x.length; index++) { + if (x.charCodeAt(index) > 255) { + throw new TypeError( + 'Cannot convert argument to a ByteString because the character at ' + + `index ${index} has a value of ${x.charCodeAt(index)} which is greater than 255.` + ) + } + } + + // 3. Return an IDL ByteString value whose length is the + // length of x, and where the value of each element is + // the value of the corresponding element of x. + return x +} + +// https://webidl.spec.whatwg.org/#es-USVString +webidl.converters.USVString = toUSVString + +// https://webidl.spec.whatwg.org/#es-boolean +webidl.converters.boolean = function (V) { + // 1. Let x be the result of computing ToBoolean(V). + const x = Boolean(V) + + // 2. Return the IDL boolean value that is the one that represents + // the same truth value as the ECMAScript Boolean value x. + return x +} + +// https://webidl.spec.whatwg.org/#es-any +webidl.converters.any = function (V) { + return V +} + +// https://webidl.spec.whatwg.org/#es-long-long +webidl.converters['long long'] = function (V) { + // 1. Let x be ? ConvertToInt(V, 64, "signed"). + const x = webidl.util.ConvertToInt(V, 64, 'signed') + + // 2. Return the IDL long long value that represents + // the same numeric value as x. + return x +} + +// https://webidl.spec.whatwg.org/#es-unsigned-long-long +webidl.converters['unsigned long long'] = function (V) { + // 1. Let x be ? ConvertToInt(V, 64, "unsigned"). + const x = webidl.util.ConvertToInt(V, 64, 'unsigned') + + // 2. Return the IDL unsigned long long value that + // represents the same numeric value as x. + return x +} + +// https://webidl.spec.whatwg.org/#es-unsigned-long +webidl.converters['unsigned long'] = function (V) { + // 1. Let x be ? ConvertToInt(V, 32, "unsigned"). + const x = webidl.util.ConvertToInt(V, 32, 'unsigned') + + // 2. Return the IDL unsigned long value that + // represents the same numeric value as x. + return x +} + +// https://webidl.spec.whatwg.org/#es-unsigned-short +webidl.converters['unsigned short'] = function (V, opts) { + // 1. Let x be ? ConvertToInt(V, 16, "unsigned"). + const x = webidl.util.ConvertToInt(V, 16, 'unsigned', opts) + + // 2. Return the IDL unsigned short value that represents + // the same numeric value as x. + return x +} + +// https://webidl.spec.whatwg.org/#idl-ArrayBuffer +webidl.converters.ArrayBuffer = function (V, opts = {}) { + // 1. If Type(V) is not Object, or V does not have an + // [[ArrayBufferData]] internal slot, then throw a + // TypeError. + // see: https://tc39.es/ecma262/#sec-properties-of-the-arraybuffer-instances + // see: https://tc39.es/ecma262/#sec-properties-of-the-sharedarraybuffer-instances + if ( + webidl.util.Type(V) !== 'Object' || + !types.isAnyArrayBuffer(V) + ) { + throw webidl.errors.conversionFailed({ + prefix: `${V}`, + argument: `${V}`, + types: ['ArrayBuffer'] + }) + } + + // 2. If the conversion is not to an IDL type associated + // with the [AllowShared] extended attribute, and + // IsSharedArrayBuffer(V) is true, then throw a + // TypeError. + if (opts.allowShared === false && types.isSharedArrayBuffer(V)) { + throw webidl.errors.exception({ + header: 'ArrayBuffer', + message: 'SharedArrayBuffer is not allowed.' + }) + } + + // 3. If the conversion is not to an IDL type associated + // with the [AllowResizable] extended attribute, and + // IsResizableArrayBuffer(V) is true, then throw a + // TypeError. + // Note: resizable ArrayBuffers are currently a proposal. + + // 4. Return the IDL ArrayBuffer value that is a + // reference to the same object as V. + return V +} + +webidl.converters.TypedArray = function (V, T, opts = {}) { + // 1. Let T be the IDL type V is being converted to. + + // 2. If Type(V) is not Object, or V does not have a + // [[TypedArrayName]] internal slot with a value + // equal to T’s name, then throw a TypeError. + if ( + webidl.util.Type(V) !== 'Object' || + !types.isTypedArray(V) || + V.constructor.name !== T.name + ) { + throw webidl.errors.conversionFailed({ + prefix: `${T.name}`, + argument: `${V}`, + types: [T.name] + }) + } + + // 3. If the conversion is not to an IDL type associated + // with the [AllowShared] extended attribute, and + // IsSharedArrayBuffer(V.[[ViewedArrayBuffer]]) is + // true, then throw a TypeError. + if (opts.allowShared === false && types.isSharedArrayBuffer(V.buffer)) { + throw webidl.errors.exception({ + header: 'ArrayBuffer', + message: 'SharedArrayBuffer is not allowed.' + }) + } + + // 4. If the conversion is not to an IDL type associated + // with the [AllowResizable] extended attribute, and + // IsResizableArrayBuffer(V.[[ViewedArrayBuffer]]) is + // true, then throw a TypeError. + // Note: resizable array buffers are currently a proposal + + // 5. Return the IDL value of type T that is a reference + // to the same object as V. + return V +} + +webidl.converters.DataView = function (V, opts = {}) { + // 1. If Type(V) is not Object, or V does not have a + // [[DataView]] internal slot, then throw a TypeError. + if (webidl.util.Type(V) !== 'Object' || !types.isDataView(V)) { + throw webidl.errors.exception({ + header: 'DataView', + message: 'Object is not a DataView.' + }) + } + + // 2. If the conversion is not to an IDL type associated + // with the [AllowShared] extended attribute, and + // IsSharedArrayBuffer(V.[[ViewedArrayBuffer]]) is true, + // then throw a TypeError. + if (opts.allowShared === false && types.isSharedArrayBuffer(V.buffer)) { + throw webidl.errors.exception({ + header: 'ArrayBuffer', + message: 'SharedArrayBuffer is not allowed.' + }) + } + + // 3. If the conversion is not to an IDL type associated + // with the [AllowResizable] extended attribute, and + // IsResizableArrayBuffer(V.[[ViewedArrayBuffer]]) is + // true, then throw a TypeError. + // Note: resizable ArrayBuffers are currently a proposal + + // 4. Return the IDL DataView value that is a reference + // to the same object as V. + return V +} + +// https://webidl.spec.whatwg.org/#BufferSource +webidl.converters.BufferSource = function (V, opts = {}) { + if (types.isAnyArrayBuffer(V)) { + return webidl.converters.ArrayBuffer(V, opts) + } + + if (types.isTypedArray(V)) { + return webidl.converters.TypedArray(V, V.constructor) + } + + if (types.isDataView(V)) { + return webidl.converters.DataView(V, opts) + } + + throw new TypeError(`Could not convert ${V} to a BufferSource.`) +} + +webidl.converters['sequence'] = webidl.sequenceConverter( + webidl.converters.ByteString +) + +webidl.converters['sequence>'] = webidl.sequenceConverter( + webidl.converters['sequence'] +) + +webidl.converters['record'] = webidl.recordConverter( + webidl.converters.ByteString, + webidl.converters.ByteString +) + +module.exports = { + webidl +} + + +/***/ }), + +/***/ 4854: +/***/ ((module) => { + +"use strict"; + + +/** + * @see https://encoding.spec.whatwg.org/#concept-encoding-get + * @param {string|undefined} label + */ +function getEncoding (label) { + if (!label) { + return 'failure' + } + + // 1. Remove any leading and trailing ASCII whitespace from label. + // 2. If label is an ASCII case-insensitive match for any of the + // labels listed in the table below, then return the + // corresponding encoding; otherwise return failure. + switch (label.trim().toLowerCase()) { + case 'unicode-1-1-utf-8': + case 'unicode11utf8': + case 'unicode20utf8': + case 'utf-8': + case 'utf8': + case 'x-unicode20utf8': + return 'UTF-8' + case '866': + case 'cp866': + case 'csibm866': + case 'ibm866': + return 'IBM866' + case 'csisolatin2': + case 'iso-8859-2': + case 'iso-ir-101': + case 'iso8859-2': + case 'iso88592': + case 'iso_8859-2': + case 'iso_8859-2:1987': + case 'l2': + case 'latin2': + return 'ISO-8859-2' + case 'csisolatin3': + case 'iso-8859-3': + case 'iso-ir-109': + case 'iso8859-3': + case 'iso88593': + case 'iso_8859-3': + case 'iso_8859-3:1988': + case 'l3': + case 'latin3': + return 'ISO-8859-3' + case 'csisolatin4': + case 'iso-8859-4': + case 'iso-ir-110': + case 'iso8859-4': + case 'iso88594': + case 'iso_8859-4': + case 'iso_8859-4:1988': + case 'l4': + case 'latin4': + return 'ISO-8859-4' + case 'csisolatincyrillic': + case 'cyrillic': + case 'iso-8859-5': + case 'iso-ir-144': + case 'iso8859-5': + case 'iso88595': + case 'iso_8859-5': + case 'iso_8859-5:1988': + return 'ISO-8859-5' + case 'arabic': + case 'asmo-708': + case 'csiso88596e': + case 'csiso88596i': + case 'csisolatinarabic': + case 'ecma-114': + case 'iso-8859-6': + case 'iso-8859-6-e': + case 'iso-8859-6-i': + case 'iso-ir-127': + case 'iso8859-6': + case 'iso88596': + case 'iso_8859-6': + case 'iso_8859-6:1987': + return 'ISO-8859-6' + case 'csisolatingreek': + case 'ecma-118': + case 'elot_928': + case 'greek': + case 'greek8': + case 'iso-8859-7': + case 'iso-ir-126': + case 'iso8859-7': + case 'iso88597': + case 'iso_8859-7': + case 'iso_8859-7:1987': + case 'sun_eu_greek': + return 'ISO-8859-7' + case 'csiso88598e': + case 'csisolatinhebrew': + case 'hebrew': + case 'iso-8859-8': + case 'iso-8859-8-e': + case 'iso-ir-138': + case 'iso8859-8': + case 'iso88598': + case 'iso_8859-8': + case 'iso_8859-8:1988': + case 'visual': + return 'ISO-8859-8' + case 'csiso88598i': + case 'iso-8859-8-i': + case 'logical': + return 'ISO-8859-8-I' + case 'csisolatin6': + case 'iso-8859-10': + case 'iso-ir-157': + case 'iso8859-10': + case 'iso885910': + case 'l6': + case 'latin6': + return 'ISO-8859-10' + case 'iso-8859-13': + case 'iso8859-13': + case 'iso885913': + return 'ISO-8859-13' + case 'iso-8859-14': + case 'iso8859-14': + case 'iso885914': + return 'ISO-8859-14' + case 'csisolatin9': + case 'iso-8859-15': + case 'iso8859-15': + case 'iso885915': + case 'iso_8859-15': + case 'l9': + return 'ISO-8859-15' + case 'iso-8859-16': + return 'ISO-8859-16' + case 'cskoi8r': + case 'koi': + case 'koi8': + case 'koi8-r': + case 'koi8_r': + return 'KOI8-R' + case 'koi8-ru': + case 'koi8-u': + return 'KOI8-U' + case 'csmacintosh': + case 'mac': + case 'macintosh': + case 'x-mac-roman': + return 'macintosh' + case 'iso-8859-11': + case 'iso8859-11': + case 'iso885911': + case 'tis-620': + case 'windows-874': + return 'windows-874' + case 'cp1250': + case 'windows-1250': + case 'x-cp1250': + return 'windows-1250' + case 'cp1251': + case 'windows-1251': + case 'x-cp1251': + return 'windows-1251' + case 'ansi_x3.4-1968': + case 'ascii': + case 'cp1252': + case 'cp819': + case 'csisolatin1': + case 'ibm819': + case 'iso-8859-1': + case 'iso-ir-100': + case 'iso8859-1': + case 'iso88591': + case 'iso_8859-1': + case 'iso_8859-1:1987': + case 'l1': + case 'latin1': + case 'us-ascii': + case 'windows-1252': + case 'x-cp1252': + return 'windows-1252' + case 'cp1253': + case 'windows-1253': + case 'x-cp1253': + return 'windows-1253' + case 'cp1254': + case 'csisolatin5': + case 'iso-8859-9': + case 'iso-ir-148': + case 'iso8859-9': + case 'iso88599': + case 'iso_8859-9': + case 'iso_8859-9:1989': + case 'l5': + case 'latin5': + case 'windows-1254': + case 'x-cp1254': + return 'windows-1254' + case 'cp1255': + case 'windows-1255': + case 'x-cp1255': + return 'windows-1255' + case 'cp1256': + case 'windows-1256': + case 'x-cp1256': + return 'windows-1256' + case 'cp1257': + case 'windows-1257': + case 'x-cp1257': + return 'windows-1257' + case 'cp1258': + case 'windows-1258': + case 'x-cp1258': + return 'windows-1258' + case 'x-mac-cyrillic': + case 'x-mac-ukrainian': + return 'x-mac-cyrillic' + case 'chinese': + case 'csgb2312': + case 'csiso58gb231280': + case 'gb2312': + case 'gb_2312': + case 'gb_2312-80': + case 'gbk': + case 'iso-ir-58': + case 'x-gbk': + return 'GBK' + case 'gb18030': + return 'gb18030' + case 'big5': + case 'big5-hkscs': + case 'cn-big5': + case 'csbig5': + case 'x-x-big5': + return 'Big5' + case 'cseucpkdfmtjapanese': + case 'euc-jp': + case 'x-euc-jp': + return 'EUC-JP' + case 'csiso2022jp': + case 'iso-2022-jp': + return 'ISO-2022-JP' + case 'csshiftjis': + case 'ms932': + case 'ms_kanji': + case 'shift-jis': + case 'shift_jis': + case 'sjis': + case 'windows-31j': + case 'x-sjis': + return 'Shift_JIS' + case 'cseuckr': + case 'csksc56011987': + case 'euc-kr': + case 'iso-ir-149': + case 'korean': + case 'ks_c_5601-1987': + case 'ks_c_5601-1989': + case 'ksc5601': + case 'ksc_5601': + case 'windows-949': + return 'EUC-KR' + case 'csiso2022kr': + case 'hz-gb-2312': + case 'iso-2022-cn': + case 'iso-2022-cn-ext': + case 'iso-2022-kr': + case 'replacement': + return 'replacement' + case 'unicodefffe': + case 'utf-16be': + return 'UTF-16BE' + case 'csunicode': + case 'iso-10646-ucs-2': + case 'ucs-2': + case 'unicode': + case 'unicodefeff': + case 'utf-16': + case 'utf-16le': + return 'UTF-16LE' + case 'x-user-defined': + return 'x-user-defined' + default: return 'failure' + } +} + +module.exports = { + getEncoding +} + + +/***/ }), + +/***/ 1446: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { + staticPropertyDescriptors, + readOperation, + fireAProgressEvent +} = __nccwpck_require__(7530) +const { + kState, + kError, + kResult, + kEvents, + kAborted +} = __nccwpck_require__(9054) +const { webidl } = __nccwpck_require__(1744) +const { kEnumerableProperty } = __nccwpck_require__(3983) + +class FileReader extends EventTarget { + constructor () { + super() + + this[kState] = 'empty' + this[kResult] = null + this[kError] = null + this[kEvents] = { + loadend: null, + error: null, + abort: null, + load: null, + progress: null, + loadstart: null + } + } + + /** + * @see https://w3c.github.io/FileAPI/#dfn-readAsArrayBuffer + * @param {import('buffer').Blob} blob + */ + readAsArrayBuffer (blob) { + webidl.brandCheck(this, FileReader) + + webidl.argumentLengthCheck(arguments, 1, { header: 'FileReader.readAsArrayBuffer' }) + + blob = webidl.converters.Blob(blob, { strict: false }) + + // The readAsArrayBuffer(blob) method, when invoked, + // must initiate a read operation for blob with ArrayBuffer. + readOperation(this, blob, 'ArrayBuffer') + } + + /** + * @see https://w3c.github.io/FileAPI/#readAsBinaryString + * @param {import('buffer').Blob} blob + */ + readAsBinaryString (blob) { + webidl.brandCheck(this, FileReader) + + webidl.argumentLengthCheck(arguments, 1, { header: 'FileReader.readAsBinaryString' }) + + blob = webidl.converters.Blob(blob, { strict: false }) + + // The readAsBinaryString(blob) method, when invoked, + // must initiate a read operation for blob with BinaryString. + readOperation(this, blob, 'BinaryString') + } + + /** + * @see https://w3c.github.io/FileAPI/#readAsDataText + * @param {import('buffer').Blob} blob + * @param {string?} encoding + */ + readAsText (blob, encoding = undefined) { + webidl.brandCheck(this, FileReader) + + webidl.argumentLengthCheck(arguments, 1, { header: 'FileReader.readAsText' }) + + blob = webidl.converters.Blob(blob, { strict: false }) + + if (encoding !== undefined) { + encoding = webidl.converters.DOMString(encoding) + } + + // The readAsText(blob, encoding) method, when invoked, + // must initiate a read operation for blob with Text and encoding. + readOperation(this, blob, 'Text', encoding) + } + + /** + * @see https://w3c.github.io/FileAPI/#dfn-readAsDataURL + * @param {import('buffer').Blob} blob + */ + readAsDataURL (blob) { + webidl.brandCheck(this, FileReader) + + webidl.argumentLengthCheck(arguments, 1, { header: 'FileReader.readAsDataURL' }) + + blob = webidl.converters.Blob(blob, { strict: false }) + + // The readAsDataURL(blob) method, when invoked, must + // initiate a read operation for blob with DataURL. + readOperation(this, blob, 'DataURL') + } + + /** + * @see https://w3c.github.io/FileAPI/#dfn-abort + */ + abort () { + // 1. If this's state is "empty" or if this's state is + // "done" set this's result to null and terminate + // this algorithm. + if (this[kState] === 'empty' || this[kState] === 'done') { + this[kResult] = null + return + } + + // 2. If this's state is "loading" set this's state to + // "done" and set this's result to null. + if (this[kState] === 'loading') { + this[kState] = 'done' + this[kResult] = null + } + + // 3. If there are any tasks from this on the file reading + // task source in an affiliated task queue, then remove + // those tasks from that task queue. + this[kAborted] = true + + // 4. Terminate the algorithm for the read method being processed. + // TODO + + // 5. Fire a progress event called abort at this. + fireAProgressEvent('abort', this) + + // 6. If this's state is not "loading", fire a progress + // event called loadend at this. + if (this[kState] !== 'loading') { + fireAProgressEvent('loadend', this) + } + } + + /** + * @see https://w3c.github.io/FileAPI/#dom-filereader-readystate + */ + get readyState () { + webidl.brandCheck(this, FileReader) + + switch (this[kState]) { + case 'empty': return this.EMPTY + case 'loading': return this.LOADING + case 'done': return this.DONE + } + } + + /** + * @see https://w3c.github.io/FileAPI/#dom-filereader-result + */ + get result () { + webidl.brandCheck(this, FileReader) + + // The result attribute’s getter, when invoked, must return + // this's result. + return this[kResult] + } + + /** + * @see https://w3c.github.io/FileAPI/#dom-filereader-error + */ + get error () { + webidl.brandCheck(this, FileReader) + + // The error attribute’s getter, when invoked, must return + // this's error. + return this[kError] + } + + get onloadend () { + webidl.brandCheck(this, FileReader) + + return this[kEvents].loadend + } + + set onloadend (fn) { + webidl.brandCheck(this, FileReader) + + if (this[kEvents].loadend) { + this.removeEventListener('loadend', this[kEvents].loadend) + } + + if (typeof fn === 'function') { + this[kEvents].loadend = fn + this.addEventListener('loadend', fn) + } else { + this[kEvents].loadend = null + } + } + + get onerror () { + webidl.brandCheck(this, FileReader) + + return this[kEvents].error + } + + set onerror (fn) { + webidl.brandCheck(this, FileReader) + + if (this[kEvents].error) { + this.removeEventListener('error', this[kEvents].error) + } + + if (typeof fn === 'function') { + this[kEvents].error = fn + this.addEventListener('error', fn) + } else { + this[kEvents].error = null + } + } + + get onloadstart () { + webidl.brandCheck(this, FileReader) + + return this[kEvents].loadstart + } + + set onloadstart (fn) { + webidl.brandCheck(this, FileReader) + + if (this[kEvents].loadstart) { + this.removeEventListener('loadstart', this[kEvents].loadstart) + } + + if (typeof fn === 'function') { + this[kEvents].loadstart = fn + this.addEventListener('loadstart', fn) + } else { + this[kEvents].loadstart = null + } + } + + get onprogress () { + webidl.brandCheck(this, FileReader) + + return this[kEvents].progress + } + + set onprogress (fn) { + webidl.brandCheck(this, FileReader) + + if (this[kEvents].progress) { + this.removeEventListener('progress', this[kEvents].progress) + } + + if (typeof fn === 'function') { + this[kEvents].progress = fn + this.addEventListener('progress', fn) + } else { + this[kEvents].progress = null + } + } + + get onload () { + webidl.brandCheck(this, FileReader) + + return this[kEvents].load + } + + set onload (fn) { + webidl.brandCheck(this, FileReader) + + if (this[kEvents].load) { + this.removeEventListener('load', this[kEvents].load) + } + + if (typeof fn === 'function') { + this[kEvents].load = fn + this.addEventListener('load', fn) + } else { + this[kEvents].load = null + } + } + + get onabort () { + webidl.brandCheck(this, FileReader) + + return this[kEvents].abort + } + + set onabort (fn) { + webidl.brandCheck(this, FileReader) + + if (this[kEvents].abort) { + this.removeEventListener('abort', this[kEvents].abort) + } + + if (typeof fn === 'function') { + this[kEvents].abort = fn + this.addEventListener('abort', fn) + } else { + this[kEvents].abort = null + } + } +} + +// https://w3c.github.io/FileAPI/#dom-filereader-empty +FileReader.EMPTY = FileReader.prototype.EMPTY = 0 +// https://w3c.github.io/FileAPI/#dom-filereader-loading +FileReader.LOADING = FileReader.prototype.LOADING = 1 +// https://w3c.github.io/FileAPI/#dom-filereader-done +FileReader.DONE = FileReader.prototype.DONE = 2 + +Object.defineProperties(FileReader.prototype, { + EMPTY: staticPropertyDescriptors, + LOADING: staticPropertyDescriptors, + DONE: staticPropertyDescriptors, + readAsArrayBuffer: kEnumerableProperty, + readAsBinaryString: kEnumerableProperty, + readAsText: kEnumerableProperty, + readAsDataURL: kEnumerableProperty, + abort: kEnumerableProperty, + readyState: kEnumerableProperty, + result: kEnumerableProperty, + error: kEnumerableProperty, + onloadstart: kEnumerableProperty, + onprogress: kEnumerableProperty, + onload: kEnumerableProperty, + onabort: kEnumerableProperty, + onerror: kEnumerableProperty, + onloadend: kEnumerableProperty, + [Symbol.toStringTag]: { + value: 'FileReader', + writable: false, + enumerable: false, + configurable: true + } +}) + +Object.defineProperties(FileReader, { + EMPTY: staticPropertyDescriptors, + LOADING: staticPropertyDescriptors, + DONE: staticPropertyDescriptors +}) + +module.exports = { + FileReader +} + + +/***/ }), + +/***/ 5504: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { webidl } = __nccwpck_require__(1744) + +const kState = Symbol('ProgressEvent state') + +/** + * @see https://xhr.spec.whatwg.org/#progressevent + */ +class ProgressEvent extends Event { + constructor (type, eventInitDict = {}) { + type = webidl.converters.DOMString(type) + eventInitDict = webidl.converters.ProgressEventInit(eventInitDict ?? {}) + + super(type, eventInitDict) + + this[kState] = { + lengthComputable: eventInitDict.lengthComputable, + loaded: eventInitDict.loaded, + total: eventInitDict.total + } + } + + get lengthComputable () { + webidl.brandCheck(this, ProgressEvent) + + return this[kState].lengthComputable + } + + get loaded () { + webidl.brandCheck(this, ProgressEvent) + + return this[kState].loaded + } + + get total () { + webidl.brandCheck(this, ProgressEvent) + + return this[kState].total + } +} + +webidl.converters.ProgressEventInit = webidl.dictionaryConverter([ + { + key: 'lengthComputable', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'loaded', + converter: webidl.converters['unsigned long long'], + defaultValue: 0 + }, + { + key: 'total', + converter: webidl.converters['unsigned long long'], + defaultValue: 0 + }, + { + key: 'bubbles', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'cancelable', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'composed', + converter: webidl.converters.boolean, + defaultValue: false + } +]) + +module.exports = { + ProgressEvent +} + + +/***/ }), + +/***/ 9054: +/***/ ((module) => { + +"use strict"; + + +module.exports = { + kState: Symbol('FileReader state'), + kResult: Symbol('FileReader result'), + kError: Symbol('FileReader error'), + kLastProgressEventFired: Symbol('FileReader last progress event fired timestamp'), + kEvents: Symbol('FileReader events'), + kAborted: Symbol('FileReader aborted') +} + + +/***/ }), + +/***/ 7530: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { + kState, + kError, + kResult, + kAborted, + kLastProgressEventFired +} = __nccwpck_require__(9054) +const { ProgressEvent } = __nccwpck_require__(5504) +const { getEncoding } = __nccwpck_require__(4854) +const { DOMException } = __nccwpck_require__(1037) +const { serializeAMimeType, parseMIMEType } = __nccwpck_require__(685) +const { types } = __nccwpck_require__(3837) +const { StringDecoder } = __nccwpck_require__(1576) +const { btoa } = __nccwpck_require__(4300) + +/** @type {PropertyDescriptor} */ +const staticPropertyDescriptors = { + enumerable: true, + writable: false, + configurable: false +} + +/** + * @see https://w3c.github.io/FileAPI/#readOperation + * @param {import('./filereader').FileReader} fr + * @param {import('buffer').Blob} blob + * @param {string} type + * @param {string?} encodingName + */ +function readOperation (fr, blob, type, encodingName) { + // 1. If fr’s state is "loading", throw an InvalidStateError + // DOMException. + if (fr[kState] === 'loading') { + throw new DOMException('Invalid state', 'InvalidStateError') + } + + // 2. Set fr’s state to "loading". + fr[kState] = 'loading' + + // 3. Set fr’s result to null. + fr[kResult] = null + + // 4. Set fr’s error to null. + fr[kError] = null + + // 5. Let stream be the result of calling get stream on blob. + /** @type {import('stream/web').ReadableStream} */ + const stream = blob.stream() + + // 6. Let reader be the result of getting a reader from stream. + const reader = stream.getReader() + + // 7. Let bytes be an empty byte sequence. + /** @type {Uint8Array[]} */ + const bytes = [] + + // 8. Let chunkPromise be the result of reading a chunk from + // stream with reader. + let chunkPromise = reader.read() + + // 9. Let isFirstChunk be true. + let isFirstChunk = true + + // 10. In parallel, while true: + // Note: "In parallel" just means non-blocking + // Note 2: readOperation itself cannot be async as double + // reading the body would then reject the promise, instead + // of throwing an error. + ;(async () => { + while (!fr[kAborted]) { + // 1. Wait for chunkPromise to be fulfilled or rejected. + try { + const { done, value } = await chunkPromise + + // 2. If chunkPromise is fulfilled, and isFirstChunk is + // true, queue a task to fire a progress event called + // loadstart at fr. + if (isFirstChunk && !fr[kAborted]) { + queueMicrotask(() => { + fireAProgressEvent('loadstart', fr) + }) + } + + // 3. Set isFirstChunk to false. + isFirstChunk = false + + // 4. If chunkPromise is fulfilled with an object whose + // done property is false and whose value property is + // a Uint8Array object, run these steps: + if (!done && types.isUint8Array(value)) { + // 1. Let bs be the byte sequence represented by the + // Uint8Array object. + + // 2. Append bs to bytes. + bytes.push(value) + + // 3. If roughly 50ms have passed since these steps + // were last invoked, queue a task to fire a + // progress event called progress at fr. + if ( + ( + fr[kLastProgressEventFired] === undefined || + Date.now() - fr[kLastProgressEventFired] >= 50 + ) && + !fr[kAborted] + ) { + fr[kLastProgressEventFired] = Date.now() + queueMicrotask(() => { + fireAProgressEvent('progress', fr) + }) + } + + // 4. Set chunkPromise to the result of reading a + // chunk from stream with reader. + chunkPromise = reader.read() + } else if (done) { + // 5. Otherwise, if chunkPromise is fulfilled with an + // object whose done property is true, queue a task + // to run the following steps and abort this algorithm: + queueMicrotask(() => { + // 1. Set fr’s state to "done". + fr[kState] = 'done' + + // 2. Let result be the result of package data given + // bytes, type, blob’s type, and encodingName. + try { + const result = packageData(bytes, type, blob.type, encodingName) + + // 4. Else: + + if (fr[kAborted]) { + return + } + + // 1. Set fr’s result to result. + fr[kResult] = result + + // 2. Fire a progress event called load at the fr. + fireAProgressEvent('load', fr) + } catch (error) { + // 3. If package data threw an exception error: + + // 1. Set fr’s error to error. + fr[kError] = error + + // 2. Fire a progress event called error at fr. + fireAProgressEvent('error', fr) + } + + // 5. If fr’s state is not "loading", fire a progress + // event called loadend at the fr. + if (fr[kState] !== 'loading') { + fireAProgressEvent('loadend', fr) + } + }) + + break + } + } catch (error) { + if (fr[kAborted]) { + return + } + + // 6. Otherwise, if chunkPromise is rejected with an + // error error, queue a task to run the following + // steps and abort this algorithm: + queueMicrotask(() => { + // 1. Set fr’s state to "done". + fr[kState] = 'done' + + // 2. Set fr’s error to error. + fr[kError] = error + + // 3. Fire a progress event called error at fr. + fireAProgressEvent('error', fr) + + // 4. If fr’s state is not "loading", fire a progress + // event called loadend at fr. + if (fr[kState] !== 'loading') { + fireAProgressEvent('loadend', fr) + } + }) + + break + } + } + })() +} + +/** + * @see https://w3c.github.io/FileAPI/#fire-a-progress-event + * @see https://dom.spec.whatwg.org/#concept-event-fire + * @param {string} e The name of the event + * @param {import('./filereader').FileReader} reader + */ +function fireAProgressEvent (e, reader) { + // The progress event e does not bubble. e.bubbles must be false + // The progress event e is NOT cancelable. e.cancelable must be false + const event = new ProgressEvent(e, { + bubbles: false, + cancelable: false + }) + + reader.dispatchEvent(event) +} + +/** + * @see https://w3c.github.io/FileAPI/#blob-package-data + * @param {Uint8Array[]} bytes + * @param {string} type + * @param {string?} mimeType + * @param {string?} encodingName + */ +function packageData (bytes, type, mimeType, encodingName) { + // 1. A Blob has an associated package data algorithm, given + // bytes, a type, a optional mimeType, and a optional + // encodingName, which switches on type and runs the + // associated steps: + + switch (type) { + case 'DataURL': { + // 1. Return bytes as a DataURL [RFC2397] subject to + // the considerations below: + // * Use mimeType as part of the Data URL if it is + // available in keeping with the Data URL + // specification [RFC2397]. + // * If mimeType is not available return a Data URL + // without a media-type. [RFC2397]. + + // https://datatracker.ietf.org/doc/html/rfc2397#section-3 + // dataurl := "data:" [ mediatype ] [ ";base64" ] "," data + // mediatype := [ type "/" subtype ] *( ";" parameter ) + // data := *urlchar + // parameter := attribute "=" value + let dataURL = 'data:' + + const parsed = parseMIMEType(mimeType || 'application/octet-stream') + + if (parsed !== 'failure') { + dataURL += serializeAMimeType(parsed) + } + + dataURL += ';base64,' + + const decoder = new StringDecoder('latin1') + + for (const chunk of bytes) { + dataURL += btoa(decoder.write(chunk)) + } + + dataURL += btoa(decoder.end()) + + return dataURL + } + case 'Text': { + // 1. Let encoding be failure + let encoding = 'failure' + + // 2. If the encodingName is present, set encoding to the + // result of getting an encoding from encodingName. + if (encodingName) { + encoding = getEncoding(encodingName) + } + + // 3. If encoding is failure, and mimeType is present: + if (encoding === 'failure' && mimeType) { + // 1. Let type be the result of parse a MIME type + // given mimeType. + const type = parseMIMEType(mimeType) + + // 2. If type is not failure, set encoding to the result + // of getting an encoding from type’s parameters["charset"]. + if (type !== 'failure') { + encoding = getEncoding(type.parameters.get('charset')) + } + } + + // 4. If encoding is failure, then set encoding to UTF-8. + if (encoding === 'failure') { + encoding = 'UTF-8' + } + + // 5. Decode bytes using fallback encoding encoding, and + // return the result. + return decode(bytes, encoding) + } + case 'ArrayBuffer': { + // Return a new ArrayBuffer whose contents are bytes. + const sequence = combineByteSequences(bytes) + + return sequence.buffer + } + case 'BinaryString': { + // Return bytes as a binary string, in which every byte + // is represented by a code unit of equal value [0..255]. + let binaryString = '' + + const decoder = new StringDecoder('latin1') + + for (const chunk of bytes) { + binaryString += decoder.write(chunk) + } + + binaryString += decoder.end() + + return binaryString + } + } +} + +/** + * @see https://encoding.spec.whatwg.org/#decode + * @param {Uint8Array[]} ioQueue + * @param {string} encoding + */ +function decode (ioQueue, encoding) { + const bytes = combineByteSequences(ioQueue) + + // 1. Let BOMEncoding be the result of BOM sniffing ioQueue. + const BOMEncoding = BOMSniffing(bytes) + + let slice = 0 + + // 2. If BOMEncoding is non-null: + if (BOMEncoding !== null) { + // 1. Set encoding to BOMEncoding. + encoding = BOMEncoding + + // 2. Read three bytes from ioQueue, if BOMEncoding is + // UTF-8; otherwise read two bytes. + // (Do nothing with those bytes.) + slice = BOMEncoding === 'UTF-8' ? 3 : 2 + } + + // 3. Process a queue with an instance of encoding’s + // decoder, ioQueue, output, and "replacement". + + // 4. Return output. + + const sliced = bytes.slice(slice) + return new TextDecoder(encoding).decode(sliced) +} + +/** + * @see https://encoding.spec.whatwg.org/#bom-sniff + * @param {Uint8Array} ioQueue + */ +function BOMSniffing (ioQueue) { + // 1. Let BOM be the result of peeking 3 bytes from ioQueue, + // converted to a byte sequence. + const [a, b, c] = ioQueue + + // 2. For each of the rows in the table below, starting with + // the first one and going down, if BOM starts with the + // bytes given in the first column, then return the + // encoding given in the cell in the second column of that + // row. Otherwise, return null. + if (a === 0xEF && b === 0xBB && c === 0xBF) { + return 'UTF-8' + } else if (a === 0xFE && b === 0xFF) { + return 'UTF-16BE' + } else if (a === 0xFF && b === 0xFE) { + return 'UTF-16LE' + } + + return null +} + +/** + * @param {Uint8Array[]} sequences + */ +function combineByteSequences (sequences) { + const size = sequences.reduce((a, b) => { + return a + b.byteLength + }, 0) + + let offset = 0 + + return sequences.reduce((a, b) => { + a.set(b, offset) + offset += b.byteLength + return a + }, new Uint8Array(size)) +} + +module.exports = { + staticPropertyDescriptors, + readOperation, + fireAProgressEvent +} + + +/***/ }), + +/***/ 1892: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +// We include a version number for the Dispatcher API. In case of breaking changes, +// this version number must be increased to avoid conflicts. +const globalDispatcher = Symbol.for('undici.globalDispatcher.1') +const { InvalidArgumentError } = __nccwpck_require__(8045) +const Agent = __nccwpck_require__(7890) + +if (getGlobalDispatcher() === undefined) { + setGlobalDispatcher(new Agent()) +} + +function setGlobalDispatcher (agent) { + if (!agent || typeof agent.dispatch !== 'function') { + throw new InvalidArgumentError('Argument agent must implement Agent') + } + Object.defineProperty(globalThis, globalDispatcher, { + value: agent, + writable: true, + enumerable: false, + configurable: false + }) +} + +function getGlobalDispatcher () { + return globalThis[globalDispatcher] +} + +module.exports = { + setGlobalDispatcher, + getGlobalDispatcher +} + + +/***/ }), + +/***/ 6930: +/***/ ((module) => { + +"use strict"; + + +module.exports = class DecoratorHandler { + constructor (handler) { + this.handler = handler + } + + onConnect (...args) { + return this.handler.onConnect(...args) + } + + onError (...args) { + return this.handler.onError(...args) + } + + onUpgrade (...args) { + return this.handler.onUpgrade(...args) + } + + onHeaders (...args) { + return this.handler.onHeaders(...args) + } + + onData (...args) { + return this.handler.onData(...args) + } + + onComplete (...args) { + return this.handler.onComplete(...args) + } + + onBodySent (...args) { + return this.handler.onBodySent(...args) + } +} + + +/***/ }), + +/***/ 2860: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const util = __nccwpck_require__(3983) +const { kBodyUsed } = __nccwpck_require__(2785) +const assert = __nccwpck_require__(9491) +const { InvalidArgumentError } = __nccwpck_require__(8045) +const EE = __nccwpck_require__(2361) + +const redirectableStatusCodes = [300, 301, 302, 303, 307, 308] + +const kBody = Symbol('body') + +class BodyAsyncIterable { + constructor (body) { + this[kBody] = body + this[kBodyUsed] = false + } + + async * [Symbol.asyncIterator] () { + assert(!this[kBodyUsed], 'disturbed') + this[kBodyUsed] = true + yield * this[kBody] + } +} + +class RedirectHandler { + constructor (dispatch, maxRedirections, opts, handler) { + if (maxRedirections != null && (!Number.isInteger(maxRedirections) || maxRedirections < 0)) { + throw new InvalidArgumentError('maxRedirections must be a positive number') + } + + util.validateHandler(handler, opts.method, opts.upgrade) + + this.dispatch = dispatch + this.location = null + this.abort = null + this.opts = { ...opts, maxRedirections: 0 } // opts must be a copy + this.maxRedirections = maxRedirections + this.handler = handler + this.history = [] + + if (util.isStream(this.opts.body)) { + // TODO (fix): Provide some way for the user to cache the file to e.g. /tmp + // so that it can be dispatched again? + // TODO (fix): Do we need 100-expect support to provide a way to do this properly? + if (util.bodyLength(this.opts.body) === 0) { + this.opts.body + .on('data', function () { + assert(false) + }) + } + + if (typeof this.opts.body.readableDidRead !== 'boolean') { + this.opts.body[kBodyUsed] = false + EE.prototype.on.call(this.opts.body, 'data', function () { + this[kBodyUsed] = true + }) + } + } else if (this.opts.body && typeof this.opts.body.pipeTo === 'function') { + // TODO (fix): We can't access ReadableStream internal state + // to determine whether or not it has been disturbed. This is just + // a workaround. + this.opts.body = new BodyAsyncIterable(this.opts.body) + } else if ( + this.opts.body && + typeof this.opts.body !== 'string' && + !ArrayBuffer.isView(this.opts.body) && + util.isIterable(this.opts.body) + ) { + // TODO: Should we allow re-using iterable if !this.opts.idempotent + // or through some other flag? + this.opts.body = new BodyAsyncIterable(this.opts.body) + } + } + + onConnect (abort) { + this.abort = abort + this.handler.onConnect(abort, { history: this.history }) + } + + onUpgrade (statusCode, headers, socket) { + this.handler.onUpgrade(statusCode, headers, socket) + } + + onError (error) { + this.handler.onError(error) + } + + onHeaders (statusCode, headers, resume, statusText) { + this.location = this.history.length >= this.maxRedirections || util.isDisturbed(this.opts.body) + ? null + : parseLocation(statusCode, headers) + + if (this.opts.origin) { + this.history.push(new URL(this.opts.path, this.opts.origin)) + } + + if (!this.location) { + return this.handler.onHeaders(statusCode, headers, resume, statusText) + } + + const { origin, pathname, search } = util.parseURL(new URL(this.location, this.opts.origin && new URL(this.opts.path, this.opts.origin))) + const path = search ? `${pathname}${search}` : pathname + + // Remove headers referring to the original URL. + // By default it is Host only, unless it's a 303 (see below), which removes also all Content-* headers. + // https://tools.ietf.org/html/rfc7231#section-6.4 + this.opts.headers = cleanRequestHeaders(this.opts.headers, statusCode === 303, this.opts.origin !== origin) + this.opts.path = path + this.opts.origin = origin + this.opts.maxRedirections = 0 + this.opts.query = null + + // https://tools.ietf.org/html/rfc7231#section-6.4.4 + // In case of HTTP 303, always replace method to be either HEAD or GET + if (statusCode === 303 && this.opts.method !== 'HEAD') { + this.opts.method = 'GET' + this.opts.body = null + } + } + + onData (chunk) { + if (this.location) { + /* + https://tools.ietf.org/html/rfc7231#section-6.4 + + TLDR: undici always ignores 3xx response bodies. + + Redirection is used to serve the requested resource from another URL, so it is assumes that + no body is generated (and thus can be ignored). Even though generating a body is not prohibited. + + For status 301, 302, 303, 307 and 308 (the latter from RFC 7238), the specs mention that the body usually + (which means it's optional and not mandated) contain just an hyperlink to the value of + the Location response header, so the body can be ignored safely. + + For status 300, which is "Multiple Choices", the spec mentions both generating a Location + response header AND a response body with the other possible location to follow. + Since the spec explicitily chooses not to specify a format for such body and leave it to + servers and browsers implementors, we ignore the body as there is no specified way to eventually parse it. + */ + } else { + return this.handler.onData(chunk) + } + } + + onComplete (trailers) { + if (this.location) { + /* + https://tools.ietf.org/html/rfc7231#section-6.4 + + TLDR: undici always ignores 3xx response trailers as they are not expected in case of redirections + and neither are useful if present. + + See comment on onData method above for more detailed informations. + */ + + this.location = null + this.abort = null + + this.dispatch(this.opts, this) + } else { + this.handler.onComplete(trailers) + } + } + + onBodySent (chunk) { + if (this.handler.onBodySent) { + this.handler.onBodySent(chunk) + } + } +} + +function parseLocation (statusCode, headers) { + if (redirectableStatusCodes.indexOf(statusCode) === -1) { + return null + } + + for (let i = 0; i < headers.length; i += 2) { + if (headers[i].toString().toLowerCase() === 'location') { + return headers[i + 1] + } + } +} + +// https://tools.ietf.org/html/rfc7231#section-6.4.4 +function shouldRemoveHeader (header, removeContent, unknownOrigin) { + return ( + (header.length === 4 && header.toString().toLowerCase() === 'host') || + (removeContent && header.toString().toLowerCase().indexOf('content-') === 0) || + (unknownOrigin && header.length === 13 && header.toString().toLowerCase() === 'authorization') || + (unknownOrigin && header.length === 6 && header.toString().toLowerCase() === 'cookie') + ) +} + +// https://tools.ietf.org/html/rfc7231#section-6.4 +function cleanRequestHeaders (headers, removeContent, unknownOrigin) { + const ret = [] + if (Array.isArray(headers)) { + for (let i = 0; i < headers.length; i += 2) { + if (!shouldRemoveHeader(headers[i], removeContent, unknownOrigin)) { + ret.push(headers[i], headers[i + 1]) + } + } + } else if (headers && typeof headers === 'object') { + for (const key of Object.keys(headers)) { + if (!shouldRemoveHeader(key, removeContent, unknownOrigin)) { + ret.push(key, headers[key]) + } + } + } else { + assert(headers == null, 'headers must be an object or an array') + } + return ret +} + +module.exports = RedirectHandler + + +/***/ }), + +/***/ 2286: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const assert = __nccwpck_require__(9491) + +const { kRetryHandlerDefaultRetry } = __nccwpck_require__(2785) +const { RequestRetryError } = __nccwpck_require__(8045) +const { isDisturbed, parseHeaders, parseRangeHeader } = __nccwpck_require__(3983) + +function calculateRetryAfterHeader (retryAfter) { + const current = Date.now() + const diff = new Date(retryAfter).getTime() - current + + return diff +} + +class RetryHandler { + constructor (opts, handlers) { + const { retryOptions, ...dispatchOpts } = opts + const { + // Retry scoped + retry: retryFn, + maxRetries, + maxTimeout, + minTimeout, + timeoutFactor, + // Response scoped + methods, + errorCodes, + retryAfter, + statusCodes + } = retryOptions ?? {} + + this.dispatch = handlers.dispatch + this.handler = handlers.handler + this.opts = dispatchOpts + this.abort = null + this.aborted = false + this.retryOpts = { + retry: retryFn ?? RetryHandler[kRetryHandlerDefaultRetry], + retryAfter: retryAfter ?? true, + maxTimeout: maxTimeout ?? 30 * 1000, // 30s, + timeout: minTimeout ?? 500, // .5s + timeoutFactor: timeoutFactor ?? 2, + maxRetries: maxRetries ?? 5, + // What errors we should retry + methods: methods ?? ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE', 'TRACE'], + // Indicates which errors to retry + statusCodes: statusCodes ?? [500, 502, 503, 504, 429], + // List of errors to retry + errorCodes: errorCodes ?? [ + 'ECONNRESET', + 'ECONNREFUSED', + 'ENOTFOUND', + 'ENETDOWN', + 'ENETUNREACH', + 'EHOSTDOWN', + 'EHOSTUNREACH', + 'EPIPE' + ] + } + + this.retryCount = 0 + this.start = 0 + this.end = null + this.etag = null + this.resume = null + + // Handle possible onConnect duplication + this.handler.onConnect(reason => { + this.aborted = true + if (this.abort) { + this.abort(reason) + } else { + this.reason = reason + } + }) + } + + onRequestSent () { + if (this.handler.onRequestSent) { + this.handler.onRequestSent() + } + } + + onUpgrade (statusCode, headers, socket) { + if (this.handler.onUpgrade) { + this.handler.onUpgrade(statusCode, headers, socket) + } + } + + onConnect (abort) { + if (this.aborted) { + abort(this.reason) + } else { + this.abort = abort + } + } + + onBodySent (chunk) { + if (this.handler.onBodySent) return this.handler.onBodySent(chunk) + } + + static [kRetryHandlerDefaultRetry] (err, { state, opts }, cb) { + const { statusCode, code, headers } = err + const { method, retryOptions } = opts + const { + maxRetries, + timeout, + maxTimeout, + timeoutFactor, + statusCodes, + errorCodes, + methods + } = retryOptions + let { counter, currentTimeout } = state + + currentTimeout = + currentTimeout != null && currentTimeout > 0 ? currentTimeout : timeout + + // Any code that is not a Undici's originated and allowed to retry + if ( + code && + code !== 'UND_ERR_REQ_RETRY' && + code !== 'UND_ERR_SOCKET' && + !errorCodes.includes(code) + ) { + cb(err) + return + } + + // If a set of method are provided and the current method is not in the list + if (Array.isArray(methods) && !methods.includes(method)) { + cb(err) + return + } + + // If a set of status code are provided and the current status code is not in the list + if ( + statusCode != null && + Array.isArray(statusCodes) && + !statusCodes.includes(statusCode) + ) { + cb(err) + return + } + + // If we reached the max number of retries + if (counter > maxRetries) { + cb(err) + return + } + + let retryAfterHeader = headers != null && headers['retry-after'] + if (retryAfterHeader) { + retryAfterHeader = Number(retryAfterHeader) + retryAfterHeader = isNaN(retryAfterHeader) + ? calculateRetryAfterHeader(retryAfterHeader) + : retryAfterHeader * 1e3 // Retry-After is in seconds + } + + const retryTimeout = + retryAfterHeader > 0 + ? Math.min(retryAfterHeader, maxTimeout) + : Math.min(currentTimeout * timeoutFactor ** counter, maxTimeout) + + state.currentTimeout = retryTimeout + + setTimeout(() => cb(null), retryTimeout) + } + + onHeaders (statusCode, rawHeaders, resume, statusMessage) { + const headers = parseHeaders(rawHeaders) + + this.retryCount += 1 + + if (statusCode >= 300) { + this.abort( + new RequestRetryError('Request failed', statusCode, { + headers, + count: this.retryCount + }) + ) + return false + } + + // Checkpoint for resume from where we left it + if (this.resume != null) { + this.resume = null + + if (statusCode !== 206) { + return true + } + + const contentRange = parseRangeHeader(headers['content-range']) + // If no content range + if (!contentRange) { + this.abort( + new RequestRetryError('Content-Range mismatch', statusCode, { + headers, + count: this.retryCount + }) + ) + return false + } + + // Let's start with a weak etag check + if (this.etag != null && this.etag !== headers.etag) { + this.abort( + new RequestRetryError('ETag mismatch', statusCode, { + headers, + count: this.retryCount + }) + ) + return false + } + + const { start, size, end = size } = contentRange + + assert(this.start === start, 'content-range mismatch') + assert(this.end == null || this.end === end, 'content-range mismatch') + + this.resume = resume + return true + } + + if (this.end == null) { + if (statusCode === 206) { + // First time we receive 206 + const range = parseRangeHeader(headers['content-range']) + + if (range == null) { + return this.handler.onHeaders( + statusCode, + rawHeaders, + resume, + statusMessage + ) + } + + const { start, size, end = size } = range + + assert( + start != null && Number.isFinite(start) && this.start !== start, + 'content-range mismatch' + ) + assert(Number.isFinite(start)) + assert( + end != null && Number.isFinite(end) && this.end !== end, + 'invalid content-length' + ) + + this.start = start + this.end = end + } + + // We make our best to checkpoint the body for further range headers + if (this.end == null) { + const contentLength = headers['content-length'] + this.end = contentLength != null ? Number(contentLength) : null + } + + assert(Number.isFinite(this.start)) + assert( + this.end == null || Number.isFinite(this.end), + 'invalid content-length' + ) + + this.resume = resume + this.etag = headers.etag != null ? headers.etag : null + + return this.handler.onHeaders( + statusCode, + rawHeaders, + resume, + statusMessage + ) + } + + const err = new RequestRetryError('Request failed', statusCode, { + headers, + count: this.retryCount + }) + + this.abort(err) + + return false + } + + onData (chunk) { + this.start += chunk.length + + return this.handler.onData(chunk) + } + + onComplete (rawTrailers) { + this.retryCount = 0 + return this.handler.onComplete(rawTrailers) + } + + onError (err) { + if (this.aborted || isDisturbed(this.opts.body)) { + return this.handler.onError(err) + } + + this.retryOpts.retry( + err, + { + state: { counter: this.retryCount++, currentTimeout: this.retryAfter }, + opts: { retryOptions: this.retryOpts, ...this.opts } + }, + onRetry.bind(this) + ) + + function onRetry (err) { + if (err != null || this.aborted || isDisturbed(this.opts.body)) { + return this.handler.onError(err) + } + + if (this.start !== 0) { + this.opts = { + ...this.opts, + headers: { + ...this.opts.headers, + range: `bytes=${this.start}-${this.end ?? ''}` + } + } + } + + try { + this.dispatch(this.opts, this) + } catch (err) { + this.handler.onError(err) + } + } + } +} + +module.exports = RetryHandler + + +/***/ }), + +/***/ 8861: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const RedirectHandler = __nccwpck_require__(2860) + +function createRedirectInterceptor ({ maxRedirections: defaultMaxRedirections }) { + return (dispatch) => { + return function Intercept (opts, handler) { + const { maxRedirections = defaultMaxRedirections } = opts + + if (!maxRedirections) { + return dispatch(opts, handler) + } + + const redirectHandler = new RedirectHandler(dispatch, maxRedirections, opts, handler) + opts = { ...opts, maxRedirections: 0 } // Stop sub dispatcher from also redirecting. + return dispatch(opts, redirectHandler) + } + } +} + +module.exports = createRedirectInterceptor + + +/***/ }), + +/***/ 953: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.SPECIAL_HEADERS = exports.HEADER_STATE = exports.MINOR = exports.MAJOR = exports.CONNECTION_TOKEN_CHARS = exports.HEADER_CHARS = exports.TOKEN = exports.STRICT_TOKEN = exports.HEX = exports.URL_CHAR = exports.STRICT_URL_CHAR = exports.USERINFO_CHARS = exports.MARK = exports.ALPHANUM = exports.NUM = exports.HEX_MAP = exports.NUM_MAP = exports.ALPHA = exports.FINISH = exports.H_METHOD_MAP = exports.METHOD_MAP = exports.METHODS_RTSP = exports.METHODS_ICE = exports.METHODS_HTTP = exports.METHODS = exports.LENIENT_FLAGS = exports.FLAGS = exports.TYPE = exports.ERROR = void 0; +const utils_1 = __nccwpck_require__(1891); +// C headers +var ERROR; +(function (ERROR) { + ERROR[ERROR["OK"] = 0] = "OK"; + ERROR[ERROR["INTERNAL"] = 1] = "INTERNAL"; + ERROR[ERROR["STRICT"] = 2] = "STRICT"; + ERROR[ERROR["LF_EXPECTED"] = 3] = "LF_EXPECTED"; + ERROR[ERROR["UNEXPECTED_CONTENT_LENGTH"] = 4] = "UNEXPECTED_CONTENT_LENGTH"; + ERROR[ERROR["CLOSED_CONNECTION"] = 5] = "CLOSED_CONNECTION"; + ERROR[ERROR["INVALID_METHOD"] = 6] = "INVALID_METHOD"; + ERROR[ERROR["INVALID_URL"] = 7] = "INVALID_URL"; + ERROR[ERROR["INVALID_CONSTANT"] = 8] = "INVALID_CONSTANT"; + ERROR[ERROR["INVALID_VERSION"] = 9] = "INVALID_VERSION"; + ERROR[ERROR["INVALID_HEADER_TOKEN"] = 10] = "INVALID_HEADER_TOKEN"; + ERROR[ERROR["INVALID_CONTENT_LENGTH"] = 11] = "INVALID_CONTENT_LENGTH"; + ERROR[ERROR["INVALID_CHUNK_SIZE"] = 12] = "INVALID_CHUNK_SIZE"; + ERROR[ERROR["INVALID_STATUS"] = 13] = "INVALID_STATUS"; + ERROR[ERROR["INVALID_EOF_STATE"] = 14] = "INVALID_EOF_STATE"; + ERROR[ERROR["INVALID_TRANSFER_ENCODING"] = 15] = "INVALID_TRANSFER_ENCODING"; + ERROR[ERROR["CB_MESSAGE_BEGIN"] = 16] = "CB_MESSAGE_BEGIN"; + ERROR[ERROR["CB_HEADERS_COMPLETE"] = 17] = "CB_HEADERS_COMPLETE"; + ERROR[ERROR["CB_MESSAGE_COMPLETE"] = 18] = "CB_MESSAGE_COMPLETE"; + ERROR[ERROR["CB_CHUNK_HEADER"] = 19] = "CB_CHUNK_HEADER"; + ERROR[ERROR["CB_CHUNK_COMPLETE"] = 20] = "CB_CHUNK_COMPLETE"; + ERROR[ERROR["PAUSED"] = 21] = "PAUSED"; + ERROR[ERROR["PAUSED_UPGRADE"] = 22] = "PAUSED_UPGRADE"; + ERROR[ERROR["PAUSED_H2_UPGRADE"] = 23] = "PAUSED_H2_UPGRADE"; + ERROR[ERROR["USER"] = 24] = "USER"; +})(ERROR = exports.ERROR || (exports.ERROR = {})); +var TYPE; +(function (TYPE) { + TYPE[TYPE["BOTH"] = 0] = "BOTH"; + TYPE[TYPE["REQUEST"] = 1] = "REQUEST"; + TYPE[TYPE["RESPONSE"] = 2] = "RESPONSE"; +})(TYPE = exports.TYPE || (exports.TYPE = {})); +var FLAGS; +(function (FLAGS) { + FLAGS[FLAGS["CONNECTION_KEEP_ALIVE"] = 1] = "CONNECTION_KEEP_ALIVE"; + FLAGS[FLAGS["CONNECTION_CLOSE"] = 2] = "CONNECTION_CLOSE"; + FLAGS[FLAGS["CONNECTION_UPGRADE"] = 4] = "CONNECTION_UPGRADE"; + FLAGS[FLAGS["CHUNKED"] = 8] = "CHUNKED"; + FLAGS[FLAGS["UPGRADE"] = 16] = "UPGRADE"; + FLAGS[FLAGS["CONTENT_LENGTH"] = 32] = "CONTENT_LENGTH"; + FLAGS[FLAGS["SKIPBODY"] = 64] = "SKIPBODY"; + FLAGS[FLAGS["TRAILING"] = 128] = "TRAILING"; + // 1 << 8 is unused + FLAGS[FLAGS["TRANSFER_ENCODING"] = 512] = "TRANSFER_ENCODING"; +})(FLAGS = exports.FLAGS || (exports.FLAGS = {})); +var LENIENT_FLAGS; +(function (LENIENT_FLAGS) { + LENIENT_FLAGS[LENIENT_FLAGS["HEADERS"] = 1] = "HEADERS"; + LENIENT_FLAGS[LENIENT_FLAGS["CHUNKED_LENGTH"] = 2] = "CHUNKED_LENGTH"; + LENIENT_FLAGS[LENIENT_FLAGS["KEEP_ALIVE"] = 4] = "KEEP_ALIVE"; +})(LENIENT_FLAGS = exports.LENIENT_FLAGS || (exports.LENIENT_FLAGS = {})); +var METHODS; +(function (METHODS) { + METHODS[METHODS["DELETE"] = 0] = "DELETE"; + METHODS[METHODS["GET"] = 1] = "GET"; + METHODS[METHODS["HEAD"] = 2] = "HEAD"; + METHODS[METHODS["POST"] = 3] = "POST"; + METHODS[METHODS["PUT"] = 4] = "PUT"; + /* pathological */ + METHODS[METHODS["CONNECT"] = 5] = "CONNECT"; + METHODS[METHODS["OPTIONS"] = 6] = "OPTIONS"; + METHODS[METHODS["TRACE"] = 7] = "TRACE"; + /* WebDAV */ + METHODS[METHODS["COPY"] = 8] = "COPY"; + METHODS[METHODS["LOCK"] = 9] = "LOCK"; + METHODS[METHODS["MKCOL"] = 10] = "MKCOL"; + METHODS[METHODS["MOVE"] = 11] = "MOVE"; + METHODS[METHODS["PROPFIND"] = 12] = "PROPFIND"; + METHODS[METHODS["PROPPATCH"] = 13] = "PROPPATCH"; + METHODS[METHODS["SEARCH"] = 14] = "SEARCH"; + METHODS[METHODS["UNLOCK"] = 15] = "UNLOCK"; + METHODS[METHODS["BIND"] = 16] = "BIND"; + METHODS[METHODS["REBIND"] = 17] = "REBIND"; + METHODS[METHODS["UNBIND"] = 18] = "UNBIND"; + METHODS[METHODS["ACL"] = 19] = "ACL"; + /* subversion */ + METHODS[METHODS["REPORT"] = 20] = "REPORT"; + METHODS[METHODS["MKACTIVITY"] = 21] = "MKACTIVITY"; + METHODS[METHODS["CHECKOUT"] = 22] = "CHECKOUT"; + METHODS[METHODS["MERGE"] = 23] = "MERGE"; + /* upnp */ + METHODS[METHODS["M-SEARCH"] = 24] = "M-SEARCH"; + METHODS[METHODS["NOTIFY"] = 25] = "NOTIFY"; + METHODS[METHODS["SUBSCRIBE"] = 26] = "SUBSCRIBE"; + METHODS[METHODS["UNSUBSCRIBE"] = 27] = "UNSUBSCRIBE"; + /* RFC-5789 */ + METHODS[METHODS["PATCH"] = 28] = "PATCH"; + METHODS[METHODS["PURGE"] = 29] = "PURGE"; + /* CalDAV */ + METHODS[METHODS["MKCALENDAR"] = 30] = "MKCALENDAR"; + /* RFC-2068, section 19.6.1.2 */ + METHODS[METHODS["LINK"] = 31] = "LINK"; + METHODS[METHODS["UNLINK"] = 32] = "UNLINK"; + /* icecast */ + METHODS[METHODS["SOURCE"] = 33] = "SOURCE"; + /* RFC-7540, section 11.6 */ + METHODS[METHODS["PRI"] = 34] = "PRI"; + /* RFC-2326 RTSP */ + METHODS[METHODS["DESCRIBE"] = 35] = "DESCRIBE"; + METHODS[METHODS["ANNOUNCE"] = 36] = "ANNOUNCE"; + METHODS[METHODS["SETUP"] = 37] = "SETUP"; + METHODS[METHODS["PLAY"] = 38] = "PLAY"; + METHODS[METHODS["PAUSE"] = 39] = "PAUSE"; + METHODS[METHODS["TEARDOWN"] = 40] = "TEARDOWN"; + METHODS[METHODS["GET_PARAMETER"] = 41] = "GET_PARAMETER"; + METHODS[METHODS["SET_PARAMETER"] = 42] = "SET_PARAMETER"; + METHODS[METHODS["REDIRECT"] = 43] = "REDIRECT"; + METHODS[METHODS["RECORD"] = 44] = "RECORD"; + /* RAOP */ + METHODS[METHODS["FLUSH"] = 45] = "FLUSH"; +})(METHODS = exports.METHODS || (exports.METHODS = {})); +exports.METHODS_HTTP = [ + METHODS.DELETE, + METHODS.GET, + METHODS.HEAD, + METHODS.POST, + METHODS.PUT, + METHODS.CONNECT, + METHODS.OPTIONS, + METHODS.TRACE, + METHODS.COPY, + METHODS.LOCK, + METHODS.MKCOL, + METHODS.MOVE, + METHODS.PROPFIND, + METHODS.PROPPATCH, + METHODS.SEARCH, + METHODS.UNLOCK, + METHODS.BIND, + METHODS.REBIND, + METHODS.UNBIND, + METHODS.ACL, + METHODS.REPORT, + METHODS.MKACTIVITY, + METHODS.CHECKOUT, + METHODS.MERGE, + METHODS['M-SEARCH'], + METHODS.NOTIFY, + METHODS.SUBSCRIBE, + METHODS.UNSUBSCRIBE, + METHODS.PATCH, + METHODS.PURGE, + METHODS.MKCALENDAR, + METHODS.LINK, + METHODS.UNLINK, + METHODS.PRI, + // TODO(indutny): should we allow it with HTTP? + METHODS.SOURCE, +]; +exports.METHODS_ICE = [ + METHODS.SOURCE, +]; +exports.METHODS_RTSP = [ + METHODS.OPTIONS, + METHODS.DESCRIBE, + METHODS.ANNOUNCE, + METHODS.SETUP, + METHODS.PLAY, + METHODS.PAUSE, + METHODS.TEARDOWN, + METHODS.GET_PARAMETER, + METHODS.SET_PARAMETER, + METHODS.REDIRECT, + METHODS.RECORD, + METHODS.FLUSH, + // For AirPlay + METHODS.GET, + METHODS.POST, +]; +exports.METHOD_MAP = utils_1.enumToMap(METHODS); +exports.H_METHOD_MAP = {}; +Object.keys(exports.METHOD_MAP).forEach((key) => { + if (/^H/.test(key)) { + exports.H_METHOD_MAP[key] = exports.METHOD_MAP[key]; + } +}); +var FINISH; +(function (FINISH) { + FINISH[FINISH["SAFE"] = 0] = "SAFE"; + FINISH[FINISH["SAFE_WITH_CB"] = 1] = "SAFE_WITH_CB"; + FINISH[FINISH["UNSAFE"] = 2] = "UNSAFE"; +})(FINISH = exports.FINISH || (exports.FINISH = {})); +exports.ALPHA = []; +for (let i = 'A'.charCodeAt(0); i <= 'Z'.charCodeAt(0); i++) { + // Upper case + exports.ALPHA.push(String.fromCharCode(i)); + // Lower case + exports.ALPHA.push(String.fromCharCode(i + 0x20)); +} +exports.NUM_MAP = { + 0: 0, 1: 1, 2: 2, 3: 3, 4: 4, + 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, +}; +exports.HEX_MAP = { + 0: 0, 1: 1, 2: 2, 3: 3, 4: 4, + 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, + A: 0XA, B: 0XB, C: 0XC, D: 0XD, E: 0XE, F: 0XF, + a: 0xa, b: 0xb, c: 0xc, d: 0xd, e: 0xe, f: 0xf, +}; +exports.NUM = [ + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', +]; +exports.ALPHANUM = exports.ALPHA.concat(exports.NUM); +exports.MARK = ['-', '_', '.', '!', '~', '*', '\'', '(', ')']; +exports.USERINFO_CHARS = exports.ALPHANUM + .concat(exports.MARK) + .concat(['%', ';', ':', '&', '=', '+', '$', ',']); +// TODO(indutny): use RFC +exports.STRICT_URL_CHAR = [ + '!', '"', '$', '%', '&', '\'', + '(', ')', '*', '+', ',', '-', '.', '/', + ':', ';', '<', '=', '>', + '@', '[', '\\', ']', '^', '_', + '`', + '{', '|', '}', '~', +].concat(exports.ALPHANUM); +exports.URL_CHAR = exports.STRICT_URL_CHAR + .concat(['\t', '\f']); +// All characters with 0x80 bit set to 1 +for (let i = 0x80; i <= 0xff; i++) { + exports.URL_CHAR.push(i); +} +exports.HEX = exports.NUM.concat(['a', 'b', 'c', 'd', 'e', 'f', 'A', 'B', 'C', 'D', 'E', 'F']); +/* Tokens as defined by rfc 2616. Also lowercases them. + * token = 1* + * separators = "(" | ")" | "<" | ">" | "@" + * | "," | ";" | ":" | "\" | <"> + * | "/" | "[" | "]" | "?" | "=" + * | "{" | "}" | SP | HT + */ +exports.STRICT_TOKEN = [ + '!', '#', '$', '%', '&', '\'', + '*', '+', '-', '.', + '^', '_', '`', + '|', '~', +].concat(exports.ALPHANUM); +exports.TOKEN = exports.STRICT_TOKEN.concat([' ']); +/* + * Verify that a char is a valid visible (printable) US-ASCII + * character or %x80-FF + */ +exports.HEADER_CHARS = ['\t']; +for (let i = 32; i <= 255; i++) { + if (i !== 127) { + exports.HEADER_CHARS.push(i); + } +} +// ',' = \x44 +exports.CONNECTION_TOKEN_CHARS = exports.HEADER_CHARS.filter((c) => c !== 44); +exports.MAJOR = exports.NUM_MAP; +exports.MINOR = exports.MAJOR; +var HEADER_STATE; +(function (HEADER_STATE) { + HEADER_STATE[HEADER_STATE["GENERAL"] = 0] = "GENERAL"; + HEADER_STATE[HEADER_STATE["CONNECTION"] = 1] = "CONNECTION"; + HEADER_STATE[HEADER_STATE["CONTENT_LENGTH"] = 2] = "CONTENT_LENGTH"; + HEADER_STATE[HEADER_STATE["TRANSFER_ENCODING"] = 3] = "TRANSFER_ENCODING"; + HEADER_STATE[HEADER_STATE["UPGRADE"] = 4] = "UPGRADE"; + HEADER_STATE[HEADER_STATE["CONNECTION_KEEP_ALIVE"] = 5] = "CONNECTION_KEEP_ALIVE"; + HEADER_STATE[HEADER_STATE["CONNECTION_CLOSE"] = 6] = "CONNECTION_CLOSE"; + HEADER_STATE[HEADER_STATE["CONNECTION_UPGRADE"] = 7] = "CONNECTION_UPGRADE"; + HEADER_STATE[HEADER_STATE["TRANSFER_ENCODING_CHUNKED"] = 8] = "TRANSFER_ENCODING_CHUNKED"; +})(HEADER_STATE = exports.HEADER_STATE || (exports.HEADER_STATE = {})); +exports.SPECIAL_HEADERS = { + 'connection': HEADER_STATE.CONNECTION, + 'content-length': HEADER_STATE.CONTENT_LENGTH, + 'proxy-connection': HEADER_STATE.CONNECTION, + 'transfer-encoding': HEADER_STATE.TRANSFER_ENCODING, + 'upgrade': HEADER_STATE.UPGRADE, +}; +//# sourceMappingURL=constants.js.map + +/***/ }), + +/***/ 1145: +/***/ ((module) => { + +module.exports = 'AGFzbQEAAAABMAhgAX8Bf2ADf39/AX9gBH9/f38Bf2AAAGADf39/AGABfwBgAn9/AGAGf39/f39/AALLAQgDZW52GHdhc21fb25faGVhZGVyc19jb21wbGV0ZQACA2VudhV3YXNtX29uX21lc3NhZ2VfYmVnaW4AAANlbnYLd2FzbV9vbl91cmwAAQNlbnYOd2FzbV9vbl9zdGF0dXMAAQNlbnYUd2FzbV9vbl9oZWFkZXJfZmllbGQAAQNlbnYUd2FzbV9vbl9oZWFkZXJfdmFsdWUAAQNlbnYMd2FzbV9vbl9ib2R5AAEDZW52GHdhc21fb25fbWVzc2FnZV9jb21wbGV0ZQAAA0ZFAwMEAAAFAAAAAAAABQEFAAUFBQAABgAAAAAGBgYGAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAAABAQcAAAUFAwABBAUBcAESEgUDAQACBggBfwFBgNQECwfRBSIGbWVtb3J5AgALX2luaXRpYWxpemUACRlfX2luZGlyZWN0X2Z1bmN0aW9uX3RhYmxlAQALbGxodHRwX2luaXQAChhsbGh0dHBfc2hvdWxkX2tlZXBfYWxpdmUAQQxsbGh0dHBfYWxsb2MADAZtYWxsb2MARgtsbGh0dHBfZnJlZQANBGZyZWUASA9sbGh0dHBfZ2V0X3R5cGUADhVsbGh0dHBfZ2V0X2h0dHBfbWFqb3IADxVsbGh0dHBfZ2V0X2h0dHBfbWlub3IAEBFsbGh0dHBfZ2V0X21ldGhvZAARFmxsaHR0cF9nZXRfc3RhdHVzX2NvZGUAEhJsbGh0dHBfZ2V0X3VwZ3JhZGUAEwxsbGh0dHBfcmVzZXQAFA5sbGh0dHBfZXhlY3V0ZQAVFGxsaHR0cF9zZXR0aW5nc19pbml0ABYNbGxodHRwX2ZpbmlzaAAXDGxsaHR0cF9wYXVzZQAYDWxsaHR0cF9yZXN1bWUAGRtsbGh0dHBfcmVzdW1lX2FmdGVyX3VwZ3JhZGUAGhBsbGh0dHBfZ2V0X2Vycm5vABsXbGxodHRwX2dldF9lcnJvcl9yZWFzb24AHBdsbGh0dHBfc2V0X2Vycm9yX3JlYXNvbgAdFGxsaHR0cF9nZXRfZXJyb3JfcG9zAB4RbGxodHRwX2Vycm5vX25hbWUAHxJsbGh0dHBfbWV0aG9kX25hbWUAIBJs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ADcDACAAQShqQgA3AwAgAEEgakIANwMAIABBEGpCADcDACAAQQhqQgA3AwAgAEHdATYCHEEAC3sBAX8CQCAAKAIMIgMNAAJAIAAoAgRFDQAgACABNgIECwJAIAAgASACEMSAgIAAIgMNACAAKAIMDwsgACADNgIcQQAhAyAAKAIEIgFFDQAgACABIAIgACgCCBGBgICAAAAiAUUNACAAIAI2AhQgACABNgIMIAEhAwsgAwvk8wEDDn8DfgR/I4CAgIAAQRBrIgMkgICAgAAgASEEIAEhBSABIQYgASEHIAEhCCABIQkgASEKIAEhCyABIQwgASENIAEhDiABIQ8CQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAgACgCHCIQQX9qDt0B2gEB2QECAwQFBgcICQoLDA0O2AEPENcBERLWARMUFRYXGBkaG+AB3wEcHR7VAR8gISIjJCXUASYnKCkqKyzTAdIBLS7RAdABLzAxMjM0NTY3ODk6Ozw9Pj9AQUJDREVG2wFHSElKzwHOAUvNAUzMAU1OT1BRUlNUVVZXWFlaW1xdXl9gYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXp7fH1+f4ABgQGCAYMBhAGFAYYBhwGIAYkB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QAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAC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FCAAIBA2AgxBACEQDLIBCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQp4CAgAAiEA0AIAEhAQxNCyAAQdMANgIcIAAgATYCFCAAIBA2AgxBACEQDLEBCyAAKAIEIRAgAEEANgIEAkAgACAQIAEQp4CAgAAiEA0AIAEhAQxRCyAAQeUANgIcIAAgATYCFCAAIBA2AgxBACEQDLABCyAAQQA2AhwgACABNgIUIABBxoqAgAA2AhAgAEEHNgIMQQAhEAyvAQsgACgCBCEQIABBADYCBAJAIAAgECABEKeAgIAAIhANACABIQEMSQsgAEHSADYCHCAAIAE2AhQgACAQNgIMQQAhEAyuAQsgACgCBCEQIABBADYCBAJAIAAgECABEKeAgIAAIhANACABIQEMSQsgAEHTADYCHCAAIAE2AhQgACAQNgIMQQAhEAytAQsgACgCBCEQIABBADYCBAJAIAAgECABEKeAgIAAIhANACABIQEMTQsgAEHlADYCHCAAIAE2AhQgACAQNgIMQQAhEAysAQsgAEEANgIcIAAgATYCFCAAQdyIgIAANgIQIABBBzYCDEEAIRAMqwELIBBBP0cNASABQQFqIQELQQUhEAyQAQtBACEQIABBADYCHCAAIAE2AhQgAEH9koCAADYCECAAQQc2AgwMqAELIAAoAgQhECAAQQA2AgQCQCAAIBAgARCngICAACIQDQAgASEBDEILIABB0gA2AhwgACABNgIUIAAgEDYCDEEAIRAMpwELIAAoAgQhECAAQQA2AgQCQCAAIBAgARCngICAACIQDQAgASEBDEILIABB0wA2AhwgACABNgIUIAAgEDYCDEEAIRAMpgELIAAoAgQhECAAQQA2AgQCQCAAIBAgARCngICAACIQDQAgASEBDEYLIABB5QA2AhwgACABNgIUIAAgEDYCDEEAIRAMpQELIAAoAgQhASAAQQA2AgQCQCAAIAEgFBCngICAACIBDQAgFCEBDD8LIABB0gA2AhwgACAUNgIUIAAgATYCDEEAIRAMpAELIAAoAgQhASAAQQA2AgQCQCAAIAEgFBCngICAACIBDQAgFCEBDD8LIABB0wA2AhwgACAUNgIUIAAgATYCDEEAIRAMowELIAAoAgQhASAAQQA2AgQCQCAAIAEgFBCngICAACIBDQAgFCEBDEMLIABB5QA2AhwgACAU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YWdlX2JlZ2luYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfZXh0ZW5zaW9uX3ZhbHVlYCBjYWxsYmFjayBlcnJvcgBgb25fc3RhdHVzX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fdmVyc2lvbl9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX3VybF9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX2NodW5rX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25faGVhZGVyX3ZhbHVlX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fbWVzc2FnZV9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX21ldGhvZF9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX2hlYWRlcl9maWVsZF9jb21wbGV0ZWAgY2FsbGJhY2sgZXJyb3IAYG9uX2NodW5rX2V4dGVuc2lvbl9uYW1lYCBjYWxsYmFjayBlcnJvcgBVbmV4cGVjdGVkIGNoYXIgaW4gdXJsIHNlcnZlcgBJbnZhbGlkIGhlYWRlciB2YWx1ZSBjaGFyAEludmFsaWQgaGVhZGVyIGZpZWxkIGNoYXIAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl92ZXJzaW9uAEludmFsaWQgbWlub3IgdmVyc2lvbgBJbnZhbGlkIG1ham9yIHZlcnNpb24ARXhwZWN0ZWQgc3BhY2UgYWZ0ZXIgdmVyc2lvbgBFeHBlY3RlZCBDUkxGIGFmdGVyIHZlcnNpb24ASW52YWxpZCBIVFRQIHZlcnNpb24ASW52YWxpZCBoZWFkZXIgdG9rZW4AU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl91cmwASW52YWxpZCBjaGFyYWN0ZXJzIGluIHVybABVbmV4cGVjdGVkIHN0YXJ0IGNoYXIgaW4gdXJsAERvdWJsZSBAIGluIHVybABFbXB0eSBDb250ZW50LUxlbmd0aABJbnZhbGlkIGNoYXJhY3RlciBpbiBDb250ZW50LUxlbmd0aABEdXBsaWNhdGUgQ29udGVudC1MZW5ndGgASW52YWxpZCBjaGFyIGluIHVybCBwYXRoAENvbnRlbnQtTGVuZ3RoIGNhbid0IGJlIHByZXNlbnQgd2l0aCBUcmFuc2Zlci1FbmNvZGluZwBJbnZhbGlkIGNoYXJhY3RlciBpbiBjaHVuayBzaXplAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25faGVhZGVyX3ZhbHVlAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25fY2h1bmtfZXh0ZW5zaW9uX3ZhbHVlAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMgdmFsdWUATWlzc2luZyBleHBlY3RlZCBMRiBhZnRlciBoZWFkZXIgdmFsdWUASW52YWxpZCBgVHJhbnNmZXItRW5jb2RpbmdgIGhlYWRlciB2YWx1ZQBJbnZhbGlkIGNoYXJhY3RlciBpbiBjaHVuayBleHRlbnNpb25zIHF1b3RlIHZhbHVlAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMgcXVvdGVkIHZhbHVlAFBhdXNlZCBieSBvbl9oZWFkZXJzX2NvbXBsZXRlAEludmFsaWQgRU9GIHN0YXRlAG9uX3Jlc2V0IHBhdXNlAG9uX2NodW5rX2hlYWRlciBwYXVzZQBvbl9tZXNzYWdlX2JlZ2luIHBhdXNlAG9uX2NodW5rX2V4dGVuc2lvbl92YWx1ZSBwYXVzZQBvbl9zdGF0dXNfY29tcGxldGUgcGF1c2UAb25fdmVyc2lvbl9jb21wbGV0ZSBwYXVzZQBvbl91cmxfY29tcGxldGUgcGF1c2UAb25fY2h1bmtfY29tcGxldGUgcGF1c2UAb25faGVhZGVyX3ZhbHVlX2NvbXBsZXRlIHBhdXNlAG9uX21lc3NhZ2VfY29tcGxldGUgcGF1c2UAb25fbWV0aG9kX2NvbXBsZXRlIHBhdXNlAG9uX2hlYWRlcl9maWVsZF9jb21wbGV0ZSBwYXVzZQBvbl9jaHVua19leHRlbnNpb25fbmFtZSBwYXVzZQBVbmV4cGVjdGVkIHNwYWNlIGFmdGVyIHN0YXJ0IGxpbmUAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9jaHVua19leHRlbnNpb25fbmFtZQBJbnZhbGlkIGNoYXJhY3RlciBpbiBjaHVuayBleHRlbnNpb25zIG5hbWUAUGF1c2Ugb24gQ09OTkVDVC9VcGdyYWRlAFBhdXNlIG9uIFBSSS9VcGdyYWRlAEV4cGVjdGVkIEhUVFAvMiBDb25uZWN0aW9uIFByZWZhY2UAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9tZXRob2QARXhwZWN0ZWQgc3BhY2UgYWZ0ZXIgbWV0aG9kAFNwYW4gY2FsbGJhY2sgZXJyb3IgaW4gb25faGVhZGVyX2ZpZWxkAFBhdXNlZABJbnZhbGlkIHdvcmQgZW5jb3VudGVyZWQASW52YWxpZCBtZXRob2QgZW5jb3VudGVyZWQAVW5leHBlY3RlZCBjaGFyIGluIHVybCBzY2hlbWEAUmVxdWVzdCBoYXMgaW52YWxpZCBgVHJhbnNmZXItRW5jb2RpbmdgAFNXSVRDSF9QUk9YWQBVU0VfUFJPWFkATUtBQ1RJVklUWQBVTlBST0NFU1NBQkxFX0VOVElUWQBDT1BZAE1PVkVEX1BFUk1BTkVOVExZAFRPT19FQVJMWQBOT1RJRlkARkFJTEVEX0RFUEVOREVOQ1kAQkFEX0dBVEVXQVkAUExBWQBQVVQAQ0hFQ0tPVVQAR0FURVdBWV9USU1FT1VUAFJFUVVFU1RfVElNRU9VVABORVRXT1JLX0NPTk5FQ1RfVElNRU9VVABDT05ORUNUSU9OX1RJTUVPVVQATE9HSU5fVElNRU9VVABORVRXT1JLX1JFQURfVElNRU9VVABQT1NUAE1JU0RJUkVDVEVEX1JFUVVFU1QAQ0xJRU5UX0NMT1NFRF9SRVFVRVNUAENMSUVOVF9DTE9TRURfTE9BRF9CQUxBTkNFRF9SRVFVRVNUAEJBRF9SRVFVRVNUAEhUVFBfUkVRVUVTVF9TRU5UX1RPX0hUVFBTX1BPUlQAUkVQT1JUAElNX0FfVEVBUE9UAFJFU0VUX0NPTlRFTlQATk9fQ09OVEVOVABQQVJUSUFMX0NPTlRFTlQASFBFX0lOVkFMSURfQ09OU1RBTlQASFBFX0NCX1JFU0VUAEdFVABIUEVfU1RSSUNUAENPTkZMSUNUAFRFTVBPUkFSWV9SRURJUkVDVABQRVJNQU5FTlRfUkVESVJFQ1QAQ09OTkVDVABNVUxUSV9TVEFUVVMASFBFX0lOVkFMSURfU1RBVFVTAFRPT19NQU5ZX1JFUVVFU1RTAEVBUkxZX0hJTlRTAFVOQVZBSUxBQkxFX0ZPUl9MRUdBTF9SRUFTT05TAE9QVElPTlMAU1dJVENISU5HX1BST1RPQ09MUwBWQVJJQU5UX0FMU09fTkVHT1RJQVRFUwBNVUxUSVBMRV9DSE9JQ0VTAElOVEVSTkFMX1NFUlZFUl9FUlJPUgBXRUJfU0VSVkVSX1VOS05PV05fRVJST1IAUkFJTEdVTl9FUlJPUgBJREVOVElUWV9QUk9WSURFUl9BVVRIRU5USUNBVElPTl9FUlJPUgBTU0xfQ0VSVElGSUNBVEVfRVJST1IASU5WQUxJRF9YX0ZPUldBUkRFRF9GT1IAU0VUX1BBUkFNRVRFUgBHRVRfUEFSQU1FVEVSAEhQRV9VU0VSAFNFRV9PVEhFUgBIUEVfQ0JfQ0hVTktfSEVBREVSAE1LQ0FMRU5EQVIAU0VUVVAAV0VCX1NFUlZFUl9JU19ET1dOAFRFQVJET1dOAEhQRV9DTE9TRURfQ09OTkVDVElPTgBIRVVSSVNUSUNfRVhQSVJBVElPTgBESVNDT05ORUNURURfT1BFUkFUSU9OAE5PTl9BVVRIT1JJVEFUSVZFX0lORk9STUFUSU9OAEhQRV9JTlZBTElEX1ZFUlNJT04ASFBFX0NCX01FU1NBR0VfQkVHSU4AU0lURV9JU19GUk9aRU4ASFBFX0lOVkFMSURfSEVBREVSX1RPS0VOAElOVkFMSURfVE9LRU4ARk9SQklEREVOAEVOSEFOQ0VfWU9VUl9DQUxNAEhQRV9JTlZBTElEX1VSTABCTE9DS0VEX0JZX1BBUkVOVEFMX0NPTlRST0wATUtDT0wAQUNMAEhQRV9JTlRFUk5BTABSRVFVRVNUX0hFQURFUl9GSUVMRFNfVE9PX0xBUkdFX1VOT0ZGSUNJQUwASFBFX09LAFVOTElOSwBVTkxPQ0sAUFJJAFJFVFJZX1dJVEgASFBFX0lOVkFMSURfQ09OVEVOVF9MRU5HVEgASFBFX1VORVhQRUNURURfQ09OVEVOVF9MRU5HVEgARkxVU0gAUFJPUFBBVENIAE0tU0VBUkNIAFVSSV9UT09fTE9ORwBQUk9DRVNTSU5HAE1JU0NFTExBTkVPVVNfUEVSU0lTVEVOVF9XQVJOSU5HAE1JU0NFTExBTkVPVVNfV0FSTklORwBIUEVfSU5WQUxJRF9UUkFOU0ZFUl9FTkNPRElORwBFeHBlY3RlZCBDUkxGAEhQRV9JTlZBTElEX0NIVU5LX1NJWkUATU9WRQBDT05USU5VRQBIUEVfQ0JfU1RBVFVTX0NPTVBMRVRFAEhQRV9DQl9IRUFERVJTX0NPTVBMRVRFAEhQRV9DQl9WRVJTSU9OX0NPTVBMRVRFAEhQRV9DQl9VUkxfQ09NUExFVEUASFBFX0NCX0NIVU5LX0NPTVBMRVRFAEhQRV9DQl9IRUFERVJfVkFMVUVfQ09NUExFVEUASFBFX0NCX0NIVU5LX0VYVEVOU0lPTl9WQUxVRV9DT01QTEVURQBIUEVfQ0JfQ0hVTktfRVhURU5TSU9OX05BTUVfQ09NUExFVEUASFBFX0NCX01FU1NBR0VfQ09NUExFVEUASFBFX0NCX01FVEhPRF9DT01QTEVURQBIUEVfQ0JfSEVBREVSX0ZJRUxEX0NPTVBMRVRFAERFTEVURQBIUEVfSU5WQUxJRF9FT0ZfU1RBVEUASU5WQUxJRF9TU0xfQ0VSVElGSUNBVEUAUEFVU0UATk9fUkVTUE9OU0UAVU5TVVBQT1JURURfTUVESUFfVFlQRQBHT05FAE5PVF9BQ0NFUFRBQkxFAFNFUlZJQ0VfVU5BVkFJTEFCTEUAUkFOR0VfTk9UX1NBVElTRklBQkxFAE9SSUdJTl9JU19VTlJFQUNIQUJMRQBSRVNQT05TRV9JU19TVEFMRQBQVVJHRQBNRVJHRQBSRVFVRVNUX0hFQURFUl9GSUVMRFNfVE9PX0xBUkdFAFJFUVVFU1RfSEVBREVSX1RPT19MQVJHRQBQQVlMT0FEX1RPT19MQVJHRQBJTlNVRkZJQ0lFTlRfU1RPUkFHRQBIUEVfUEFVU0VEX1VQR1JBREUASFBFX1BBVVNFRF9IMl9VUEdSQURFAFNPVVJDRQBBTk5PVU5DRQBUUkFDRQBIUEVfVU5FWFBFQ1RFRF9TUEFDRQBERVNDUklCRQBVTlNVQlNDUklCRQBSRUNPUkQASFBFX0lOVkFMSURfTUVUSE9EAE5PVF9GT1VORABQUk9QRklORABVTkJJTkQAUkVCSU5EAFVOQVVUSE9SSVpFRABNRVRIT0RfTk9UX0FMTE9XRUQASFRUUF9WRVJTSU9OX05PVF9TVVBQT1JURUQAQUxSRUFEWV9SRVBPUlRFRABBQ0NFUFRFRABOT1RfSU1QTEVNRU5URUQATE9PUF9ERVRFQ1RFRABIUEVfQ1JfRVhQRUNURUQASFBFX0xGX0VYUEVDVEVEAENSRUFURUQASU1fVVNFRABIUEVfUEFVU0VEAFRJTUVPVVRfT0NDVVJFRABQQVlNRU5UX1JFUVVJUkVEAFBSRUNPTkRJVElPTl9SRVFVSVJFRABQUk9YWV9BVVRIRU5USUNBVElPTl9SRVFVSVJFRABORVRXT1JLX0FVVEhFTlRJQ0FUSU9OX1JFUVVJUkVEAExFTkdUSF9SRVFVSVJFRABTU0xfQ0VSVElGSUNBVEVfUkVRVUlSRUQAVVBHUkFERV9SRVFVSVJFRABQQUdFX0VYUElSRUQAUFJFQ09ORElUSU9OX0ZBSUxFRABFWFBFQ1RBVElPTl9GQUlMRUQAUkVWQUxJREFUSU9OX0ZBSUxFRABTU0xfSEFORFNIQUtFX0ZBSUxFRABMT0NLRUQAVFJBTlNGT1JNQVRJT05fQVBQTElFRABOT1RfTU9ESUZJRUQATk9UX0VYVEVOREVEAEJBTkRXSURUSF9MSU1JVF9FWENFRURFRABTSVRFX0lTX09WRVJMT0FERUQASEVBRABFeHBlY3RlZCBIVFRQLwAAXhMAACYTAAAwEAAA8BcAAJ0TAAAVEgAAORcAAPASAAAKEAAAdRIAAK0SAACCEwAATxQAAH8QAACgFQAAIxQAAIkSAACLFAAATRUAANQRAADPFAAAEBgAAMkWAADcFgAAwREAAOAXAAC7FAAAdBQAAHwVAADlFAAACBcAAB8QAABlFQAAoxQAACgVAAACFQAAmRUAACwQAACLGQAATw8AANQOAABqEAAAzhAAAAIXAACJDgAAbhMAABwTAABmFAAAVhcAAMETAADNEwAAbBMAAGgXAABmFwAAXxcAACITAADODwAAaQ4AANgOAABjFgAAyxMAAKoOAAAoFwAAJhcAAMUTAABdFgAA6BEAAGcTAABlEwAA8hYAAHMTAAAdFwAA+RYAAPMRAADPDgAAzhUAAAwSAACzEQAApREAAGEQAAAyFwAAuxMAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQIBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIDAgICAgIAAAICAAICAAICAgICAgICAgIABAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgIAAAACAgICAgICAgICAgICAgICAgICAgICAgICAgICAgACAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAICAgICAAACAgACAgACAgICAgICAgICAAMABAAAAAICAgICAgICAgICAgICAgICAgICAgICAgICAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAAgACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbG9zZWVlcC1hbGl2ZQAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQIBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBY2h1bmtlZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEAAQEBAQEAAAEBAAEBAAEBAQEBAQEBAQEAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABlY3Rpb25lbnQtbGVuZ3Rob25yb3h5LWNvbm5lY3Rpb24AAAAAAAAAAAAAAAAAAAByYW5zZmVyLWVuY29kaW5ncGdyYWRlDQoNCg0KU00NCg0KVFRQL0NFL1RTUC8AAAAAAAAAAAAAAAABAgABAwAAAAAAAAAAAAAAAAAAAAAAAAQBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAQIAAQMAAAAAAAAAAAAAAAAAAAAAAAAEAQEFAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAEAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAQAAAgAAAAAAAAAAAAAAAAAAAAAAAAMEAAAEBAQEBAQEBAQEBAUEBAQEBAQEBAQEBAQABAAGBwQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEAAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwAAAAAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAIAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAAAAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABOT1VOQ0VFQ0tPVVRORUNURVRFQ1JJQkVMVVNIRVRFQURTRUFSQ0hSR0VDVElWSVRZTEVOREFSVkVPVElGWVBUSU9OU0NIU0VBWVNUQVRDSEdFT1JESVJFQ1RPUlRSQ0hQQVJBTUVURVJVUkNFQlNDUklCRUFSRE9XTkFDRUlORE5LQ0tVQlNDUklCRUhUVFAvQURUUC8=' + + +/***/ }), + +/***/ 5627: +/***/ ((module) => { + +module.exports = 'AGFzbQEAAAABMAhgAX8Bf2ADf39/AX9gBH9/f38Bf2AAAGADf39/AGABfwBgAn9/AGAGf39/f39/AALLAQgDZW52GHdhc21fb25faGVhZGVyc19jb21wbGV0ZQACA2VudhV3YXNtX29uX21lc3NhZ2VfYmVnaW4AAANlbnYLd2FzbV9vbl91cmwAAQNlbnYOd2FzbV9vbl9zdGF0dXMAAQNlbnYUd2FzbV9vbl9oZWFkZXJfZmllbGQAAQNlbnYUd2FzbV9vbl9oZWFkZXJfdmFsdWUAAQNlbnYMd2FzbV9vbl9ib2R5AAEDZW52GHdhc21fb25fbWVzc2FnZV9jb21wbGV0ZQAAA0ZFAwMEAAAFAAAAAAAABQEFAAUFBQAABgAAAAAGBgYGAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAAABAQcAAAUFAwABBAUBcAESEgUDAQACBggBfwFBgNQECwfRBSIGbWVtb3J5AgALX2luaXRpYWxpemUACRlfX2luZGlyZWN0X2Z1bmN0aW9uX3RhYmxlAQALbGxodHRwX2luaXQAChhsbGh0dHBfc2hvdWxkX2tlZXBfYWxpdmUAQQxsbGh0dHBfYWxsb2MADAZtYWxsb2MARgtsbGh0dHBfZnJlZQANBGZyZWUASA9sbGh0dHBfZ2V0X3R5cGUADhVsbGh0dHBfZ2V0X2h0dHBfbWFqb3IADxVsbGh0dHBfZ2V0X2h0dHBfbWlub3IAEBFsbGh0dHBfZ2V0X21ldGhvZAARFmxsaHR0cF9nZXRfc3RhdHVzX2NvZGUAEhJsbGh0dHBfZ2V0X3VwZ3JhZGUAEwxsbGh0dHBfcmVzZXQAFA5sbGh0dHBfZXhlY3V0ZQAVFGxsaHR0cF9zZXR0aW5nc19pbml0ABYNbGxodHRwX2ZpbmlzaAAXDGxsaHR0cF9wYXVzZQAYDWxsaHR0cF9yZXN1bWUAGRtsbGh0dHBfcmVzdW1lX2FmdGVyX3VwZ3JhZGUAGhBsbGh0dHBfZ2V0X2Vycm5vABsXbGxodHRwX2dldF9lcnJvcl9yZWFzb24AHBdsbGh0dHBfc2V0X2Vycm9yX3JlYXNvbgAdFGxsaHR0cF9nZXRfZXJyb3JfcG9zAB4RbGxodHRwX2Vycm5vX25hbWUAHxJsbGh0dHBfbWV0aG9kX25hbWUAIBJs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IAH9CwMAIABBMGogAf0LAwAgAEEgaiAB/QsDACAAQd0BNgIcQQALewEBfwJAIAAoAgwiAw0AAkAgACgCBEUNACAAIAE2AgQLAkAgACABIAIQxICAgAAiAw0AIAAoAgwPCyAAIAM2AhxBACEDIAAoAgQiAUUNACAAIAEgAiAAKAIIEYGAgIAAACIBRQ0AIAAgAjYCFCAAIAE2AgwgASEDCyADC+TzAQMOfwN+BH8jgICAgABBEGsiAySAgICAACABIQQgASEFIAEhBiABIQcgASEIIAEhCSABIQogASELIAEhDCABIQ0gASEOIAEhDwJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQCAAKAIcIhBBf2oO3QHaAQHZAQIDBAUGBwgJCgsMDQ7YAQ8Q1wEREtYBExQVFhcYGRob4AHfARwdHtUBHyAhIiMkJdQBJicoKSorLNMB0gEtLtEB0AEvMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUbbAUdISUrPAc4BS80BTMwBTU5PUFFSU1RVVldYWVpbXF1eX2BhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ent8fX5/gAGBAYIBgwGEAYUBhgGHAYgBiQGKAYsBjAGNAY4BjwGQAZEB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QAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAIBAOxwEAAQID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YWNrIGVycm9yAGBvbl9jaHVua19leHRlbnNpb25fdmFsdWVgIGNhbGxiYWNrIGVycm9yAGBvbl9zdGF0dXNfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl92ZXJzaW9uX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fdXJsX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9oZWFkZXJfdmFsdWVfY29tcGxldGVgIGNhbGxiYWNrIGVycm9yAGBvbl9tZXNzYWdlX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fbWV0aG9kX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25faGVhZGVyX2ZpZWxkX2NvbXBsZXRlYCBjYWxsYmFjayBlcnJvcgBgb25fY2h1bmtfZXh0ZW5zaW9uX25hbWVgIGNhbGxiYWNrIGVycm9yAFVuZXhwZWN0ZWQgY2hhciBpbiB1cmwgc2VydmVyAEludmFsaWQgaGVhZGVyIHZhbHVlIGNoYXIASW52YWxpZCBoZWFkZXIgZmllbGQgY2hhcgBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX3ZlcnNpb24ASW52YWxpZCBtaW5vciB2ZXJzaW9uAEludmFsaWQgbWFqb3IgdmVyc2lvbgBFeHBlY3RlZCBzcGFjZSBhZnRlciB2ZXJzaW9uAEV4cGVjdGVkIENSTEYgYWZ0ZXIgdmVyc2lvbgBJbnZhbGlkIEhUVFAgdmVyc2lvbgBJbnZhbGlkIGhlYWRlciB0b2tlbgBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX3VybABJbnZhbGlkIGNoYXJhY3RlcnMgaW4gdXJsAFVuZXhwZWN0ZWQgc3RhcnQgY2hhciBpbiB1cmwARG91YmxlIEAgaW4gdXJsAEVtcHR5IENvbnRlbnQtTGVuZ3RoAEludmFsaWQgY2hhcmFjdGVyIGluIENvbnRlbnQtTGVuZ3RoAER1cGxpY2F0ZSBDb250ZW50LUxlbmd0aABJbnZhbGlkIGNoYXIgaW4gdXJsIHBhdGgAQ29udGVudC1MZW5ndGggY2FuJ3QgYmUgcHJlc2VudCB3aXRoIFRyYW5zZmVyLUVuY29kaW5nAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIHNpemUAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9oZWFkZXJfdmFsdWUAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9jaHVua19leHRlbnNpb25fdmFsdWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyB2YWx1ZQBNaXNzaW5nIGV4cGVjdGVkIExGIGFmdGVyIGhlYWRlciB2YWx1ZQBJbnZhbGlkIGBUcmFuc2Zlci1FbmNvZGluZ2AgaGVhZGVyIHZhbHVlAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMgcXVvdGUgdmFsdWUASW52YWxpZCBjaGFyYWN0ZXIgaW4gY2h1bmsgZXh0ZW5zaW9ucyBxdW90ZWQgdmFsdWUAUGF1c2VkIGJ5IG9uX2hlYWRlcnNfY29tcGxldGUASW52YWxpZCBFT0Ygc3RhdGUAb25fcmVzZXQgcGF1c2UAb25fY2h1bmtfaGVhZGVyIHBhdXNlAG9uX21lc3NhZ2VfYmVnaW4gcGF1c2UAb25fY2h1bmtfZXh0ZW5zaW9uX3ZhbHVlIHBhdXNlAG9uX3N0YXR1c19jb21wbGV0ZSBwYXVzZQBvbl92ZXJzaW9uX2NvbXBsZXRlIHBhdXNlAG9uX3VybF9jb21wbGV0ZSBwYXVzZQBvbl9jaHVua19jb21wbGV0ZSBwYXVzZQBvbl9oZWFkZXJfdmFsdWVfY29tcGxldGUgcGF1c2UAb25fbWVzc2FnZV9jb21wbGV0ZSBwYXVzZQBvbl9tZXRob2RfY29tcGxldGUgcGF1c2UAb25faGVhZGVyX2ZpZWxkX2NvbXBsZXRlIHBhdXNlAG9uX2NodW5rX2V4dGVuc2lvbl9uYW1lIHBhdXNlAFVuZXhwZWN0ZWQgc3BhY2UgYWZ0ZXIgc3RhcnQgbGluZQBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX2NodW5rX2V4dGVuc2lvbl9uYW1lAEludmFsaWQgY2hhcmFjdGVyIGluIGNodW5rIGV4dGVuc2lvbnMgbmFtZQBQYXVzZSBvbiBDT05ORUNUL1VwZ3JhZGUAUGF1c2Ugb24gUFJJL1VwZ3JhZGUARXhwZWN0ZWQgSFRUUC8yIENvbm5lY3Rpb24gUHJlZmFjZQBTcGFuIGNhbGxiYWNrIGVycm9yIGluIG9uX21ldGhvZABFeHBlY3RlZCBzcGFjZSBhZnRlciBtZXRob2QAU3BhbiBjYWxsYmFjayBlcnJvciBpbiBvbl9oZWFkZXJfZmllbGQAUGF1c2VkAEludmFsaWQgd29yZCBlbmNvdW50ZXJlZABJbnZhbGlkIG1ldGhvZCBlbmNvdW50ZXJlZABVbmV4cGVjdGVkIGNoYXIgaW4gdXJsIHNjaGVtYQBSZXF1ZXN0IGhhcyBpbnZhbGlkIGBUcmFuc2Zlci1FbmNvZGluZ2AAU1dJVENIX1BST1hZAFVTRV9QUk9YWQBNS0FDVElWSVRZAFVOUFJPQ0VTU0FCTEVfRU5USVRZAENPUFkATU9WRURfUEVSTUFORU5UTFkAVE9PX0VBUkxZAE5PVElGWQBGQUlMRURfREVQRU5ERU5DWQBCQURfR0FURVdBWQBQTEFZAFBVVABDSEVDS09VVABHQVRFV0FZX1RJTUVPVVQAUkVRVUVTVF9USU1FT1VUAE5FVFdPUktfQ09OTkVDVF9USU1FT1VUAENPTk5FQ1RJT05fVElNRU9VVABMT0dJTl9USU1FT1VUAE5FVFdPUktfUkVBRF9USU1FT1VUAFBPU1QATUlTRElSRUNURURfUkVRVUVTVABDTElFTlRfQ0xPU0VEX1JFUVVFU1QAQ0xJRU5UX0NMT1NFRF9MT0FEX0JBTEFOQ0VEX1JFUVVFU1QAQkFEX1JFUVVFU1QASFRUUF9SRVFVRVNUX1NFTlRfVE9fSFRUUFNfUE9SVABSRVBPUlQASU1fQV9URUFQT1QAUkVTRVRfQ09OVEVOVABOT19DT05URU5UAFBBUlRJQUxfQ09OVEVOVABIUEVfSU5WQUxJRF9DT05TVEFOVABIUEVfQ0JfUkVTRVQAR0VUAEhQRV9TVFJJQ1QAQ09ORkxJQ1QAVEVNUE9SQVJZX1JFRElSRUNUAFBFUk1BTkVOVF9SRURJUkVDVABDT05ORUNUAE1VTFRJX1NUQVRVUwBIUEVfSU5WQUxJRF9TVEFUVVMAVE9PX01BTllfUkVRVUVTVFMARUFSTFlfSElOVFMAVU5BVkFJTEFCTEVfRk9SX0xFR0FMX1JFQVNPTlMAT1BUSU9OUwBTV0lUQ0hJTkdfUFJPVE9DT0xTAFZBUklBTlRfQUxTT19ORUdPVElBVEVTAE1VTFRJUExFX0NIT0lDRVMASU5URVJOQUxfU0VSVkVSX0VSUk9SAFdFQl9TRVJWRVJfVU5LTk9XTl9FUlJPUgBSQUlMR1VOX0VSUk9SAElERU5USVRZX1BST1ZJREVSX0FVVEhFTlRJQ0FUSU9OX0VSUk9SAFNTTF9DRVJUSUZJQ0FURV9FUlJPUgBJTlZBTElEX1hfRk9SV0FSREVEX0ZPUgBTRVRfUEFSQU1FVEVSAEdFVF9QQVJBTUVURVIASFBFX1VTRVIAU0VFX09USEVSAEhQRV9DQl9DSFVOS19IRUFERVIATUtDQUxFTkRBUgBTRVRVUABXRUJfU0VSVkVSX0lTX0RPV04AVEVBUkRPV04ASFBFX0NMT1NFRF9DT05ORUNUSU9OAEhFVVJJU1RJQ19FWFBJUkFUSU9OAERJU0NPTk5FQ1RFRF9PUEVSQVRJT04ATk9OX0FVVEhPUklUQVRJVkVfSU5GT1JNQVRJT04ASFBFX0lOVkFMSURfVkVSU0lPTgBIUEVfQ0JfTUVTU0FHRV9CRUdJTgBTSVRFX0lTX0ZST1pFTgBIUEVfSU5WQUxJRF9IRUFERVJfVE9LRU4ASU5WQUxJRF9UT0tFTgBGT1JCSURERU4ARU5IQU5DRV9ZT1VSX0NBTE0ASFBFX0lOVkFMSURfVVJMAEJMT0NLRURfQllfUEFSRU5UQUxfQ09OVFJPTABNS0NPTABBQ0wASFBFX0lOVEVSTkFMAFJFUVVFU1RfSEVBREVSX0ZJRUxEU19UT09fTEFSR0VfVU5PRkZJQ0lBTABIUEVfT0sAVU5MSU5LAFVOTE9DSwBQUkkAUkVUUllfV0lUSABIUEVfSU5WQUxJRF9DT05URU5UX0xFTkdUSABIUEVfVU5FWFBFQ1RFRF9DT05URU5UX0xFTkdUSABGTFVTSABQUk9QUEFUQ0gATS1TRUFSQ0gAVVJJX1RPT19MT05HAFBST0NFU1NJTkcATUlTQ0VMTEFORU9VU19QRVJTSVNURU5UX1dBUk5JTkcATUlTQ0VMTEFORU9VU19XQVJOSU5HAEhQRV9JTlZBTElEX1RSQU5TRkVSX0VOQ09ESU5HAEV4cGVjdGVkIENSTEYASFBFX0lOVkFMSURfQ0hVTktfU0laRQBNT1ZFAENPTlRJTlVFAEhQRV9DQl9TVEFUVVNfQ09NUExFVEUASFBFX0NCX0hFQURFUlNfQ09NUExFVEUASFBFX0NCX1ZFUlNJT05fQ09NUExFVEUASFBFX0NCX1VSTF9DT01QTEVURQBIUEVfQ0JfQ0hVTktfQ09NUExFVEUASFBFX0NCX0hFQURFUl9WQUxVRV9DT01QTEVURQBIUEVfQ0JfQ0hVTktfRVhURU5TSU9OX1ZBTFVFX0NPTVBMRVRFAEhQRV9DQl9DSFVOS19FWFRFTlNJT05fTkFNRV9DT01QTEVURQBIUEVfQ0JfTUVTU0FHRV9DT01QTEVURQBIUEVfQ0JfTUVUSE9EX0NPTVBMRVRFAEhQRV9DQl9IRUFERVJfRklFTERfQ09NUExFVEUAREVMRVRFAEhQRV9JTlZBTElEX0VPRl9TVEFURQBJTlZBTElEX1NTTF9DRVJUSUZJQ0FURQBQQVVTRQBOT19SRVNQT05TRQBVTlNVUFBPUlRFRF9NRURJQV9UWVBFAEdPTkUATk9UX0FDQ0VQVEFCTEUAU0VSVklDRV9VTkFWQUlMQUJMRQBSQU5HRV9OT1RfU0FUSVNGSUFCTEUAT1JJR0lOX0lTX1VOUkVBQ0hBQkxFAFJFU1BPTlNFX0lTX1NUQUxFAFBVUkdFAE1FUkdFAFJFUVVFU1RfSEVBREVSX0ZJRUxEU19UT09fTEFSR0UAUkVRVUVTVF9IRUFERVJfVE9PX0xBUkdFAFBBWUxPQURfVE9PX0xBUkdFAElOU1VGRklDSUVOVF9TVE9SQUdFAEhQRV9QQVVTRURfVVBHUkFERQBIUEVfUEFVU0VEX0gyX1VQR1JBREUAU09VUkNFAEFOTk9VTkNFAFRSQUNFAEhQRV9VTkVYUEVDVEVEX1NQQUNFAERFU0NSSUJFAFVOU1VCU0NSSUJFAFJFQ09SRABIUEVfSU5WQUxJRF9NRVRIT0QATk9UX0ZPVU5EAFBST1BGSU5EAFVOQklORABSRUJJTkQAVU5BVVRIT1JJWkVEAE1FVEhPRF9OT1RfQUxMT1dFRABIVFRQX1ZFUlNJT05fTk9UX1NVUFBPUlRFRABBTFJFQURZX1JFUE9SVEVEAEFDQ0VQVEVEAE5PVF9JTVBMRU1FTlRFRABMT09QX0RFVEVDVEVEAEhQRV9DUl9FWFBFQ1RFRABIUEVfTEZfRVhQRUNURUQAQ1JFQVRFRABJTV9VU0VEAEhQRV9QQVVTRUQAVElNRU9VVF9PQ0NVUkVEAFBBWU1FTlRfUkVRVUlSRUQAUFJFQ09ORElUSU9OX1JFUVVJUkVEAFBST1hZX0FVVEhFTlRJQ0FUSU9OX1JFUVVJUkVEAE5FVFdPUktfQVVUSEVOVElDQVRJT05fUkVRVUlSRUQATEVOR1RIX1JFUVVJUkVEAFNTTF9DRVJUSUZJQ0FURV9SRVFVSVJFRABVUEdSQURFX1JFUVVJUkVEAFBBR0VfRVhQSVJFRABQUkVDT05ESVRJT05fRkFJTEVEAEVYUEVDVEFUSU9OX0ZBSUxFRABSRVZBTElEQVRJT05fRkFJTEVEAFNTTF9IQU5EU0hBS0VfRkFJTEVEAExPQ0tFRABUUkFOU0ZPUk1BVElPTl9BUFBMSUVEAE5PVF9NT0RJRklFRABOT1RfRVhURU5ERUQAQkFORFdJRFRIX0xJTUlUX0VYQ0VFREVEAFNJVEVfSVNfT1ZFUkxPQURFRABIRUFEAEV4cGVjdGVkIEhUVFAvAABeEwAAJhMAADAQAADwFwAAnRMAABUSAAA5FwAA8BIAAAoQAAB1EgAArRIAAIITAABPFAAAfxAAAKAVAAAjFAAAiRIAAIsUAABNFQAA1BEAAM8UAAAQGAAAyRYAANwWAADBEQAA4BcAALsUAAB0FAAAfBUAAOUUAAAIFwAAHxAAAGUVAACjFAAAKBUAAAIVAACZFQAALBAAAIsZAABPDwAA1A4AAGoQAADOEAAAAhcAAIkOAABuEwAAHBMAAGYUAABWFwAAwRMAAM0TAABsEwAAaBcAAGYXAABfFwAAIhMAAM4PAABpDgAA2A4AAGMWAADLEwAAqg4AACgXAAAmFwAAxRMAAF0WAADoEQAAZxMAAGUTAADyFgAAcxMAAB0XAAD5FgAA8xEAAM8OAADOFQAADBIAALMRAAClEQAAYRAAADIXAAC7EwAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAgEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgMCAgICAgAAAgIAAgIAAgICAgICAgICAgAEAAAAAAACAgICAgICAgICAgICAgICAgICAgICAgICAgAAAAICAgICAgICAgICAgICAgICAgICAgICAgICAgICAAIAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAgICAgIAAAICAAICAAICAgICAgICAgIAAwAEAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgIAAAACAgICAgICAgICAgICAgICAgICAgICAgICAgICAgACAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABsb3NlZWVwLWFsaXZlAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAgEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQFjaHVua2VkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQABAQEBAQAAAQEAAQEAAQEBAQEBAQEBAQAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGVjdGlvbmVudC1sZW5ndGhvbnJveHktY29ubmVjdGlvbgAAAAAAAAAAAAAAAAAAAHJhbnNmZXItZW5jb2RpbmdwZ3JhZGUNCg0KDQpTTQ0KDQpUVFAvQ0UvVFNQLwAAAAAAAAAAAAAAAAECAAEDAAAAAAAAAAAAAAAAAAAAAAAABAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAABAgABAwAAAAAAAAAAAAAAAAAAAAAAAAQBAQUBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAQAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAABAAACAAAAAAAAAAAAAAAAAAAAAAAAAwQAAAQEBAQEBAQEBAQEBQQEBAQEBAQEBAQEBAAEAAYHBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAQABAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAAAAAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAgAAAAACAAAAAAAAAAAAAAAAAAAAAAADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwAAAAAAAAMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE5PVU5DRUVDS09VVE5FQ1RFVEVDUklCRUxVU0hFVEVBRFNFQVJDSFJHRUNUSVZJVFlMRU5EQVJWRU9USUZZUFRJT05TQ0hTRUFZU1RBVENIR0VPUkRJUkVDVE9SVFJDSFBBUkFNRVRFUlVSQ0VCU0NSSUJFQVJET1dOQUNFSU5ETktDS1VCU0NSSUJFSFRUUC9BRFRQLw==' + + +/***/ }), + +/***/ 1891: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.enumToMap = void 0; +function enumToMap(obj) { + const res = {}; + Object.keys(obj).forEach((key) => { + const value = obj[key]; + if (typeof value === 'number') { + res[key] = value; + } + }); + return res; +} +exports.enumToMap = enumToMap; +//# sourceMappingURL=utils.js.map + +/***/ }), + +/***/ 6771: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { kClients } = __nccwpck_require__(2785) +const Agent = __nccwpck_require__(7890) +const { + kAgent, + kMockAgentSet, + kMockAgentGet, + kDispatches, + kIsMockActive, + kNetConnect, + kGetNetConnect, + kOptions, + kFactory +} = __nccwpck_require__(4347) +const MockClient = __nccwpck_require__(8687) +const MockPool = __nccwpck_require__(6193) +const { matchValue, buildMockOptions } = __nccwpck_require__(9323) +const { InvalidArgumentError, UndiciError } = __nccwpck_require__(8045) +const Dispatcher = __nccwpck_require__(412) +const Pluralizer = __nccwpck_require__(8891) +const PendingInterceptorsFormatter = __nccwpck_require__(6823) + +class FakeWeakRef { + constructor (value) { + this.value = value + } + + deref () { + return this.value + } +} + +class MockAgent extends Dispatcher { + constructor (opts) { + super(opts) + + this[kNetConnect] = true + this[kIsMockActive] = true + + // Instantiate Agent and encapsulate + if ((opts && opts.agent && typeof opts.agent.dispatch !== 'function')) { + throw new InvalidArgumentError('Argument opts.agent must implement Agent') + } + const agent = opts && opts.agent ? opts.agent : new Agent(opts) + this[kAgent] = agent + + this[kClients] = agent[kClients] + this[kOptions] = buildMockOptions(opts) + } + + get (origin) { + let dispatcher = this[kMockAgentGet](origin) + + if (!dispatcher) { + dispatcher = this[kFactory](origin) + this[kMockAgentSet](origin, dispatcher) + } + return dispatcher + } + + dispatch (opts, handler) { + // Call MockAgent.get to perform additional setup before dispatching as normal + this.get(opts.origin) + return this[kAgent].dispatch(opts, handler) + } + + async close () { + await this[kAgent].close() + this[kClients].clear() + } + + deactivate () { + this[kIsMockActive] = false + } + + activate () { + this[kIsMockActive] = true + } + + enableNetConnect (matcher) { + if (typeof matcher === 'string' || typeof matcher === 'function' || matcher instanceof RegExp) { + if (Array.isArray(this[kNetConnect])) { + this[kNetConnect].push(matcher) + } else { + this[kNetConnect] = [matcher] + } + } else if (typeof matcher === 'undefined') { + this[kNetConnect] = true + } else { + throw new InvalidArgumentError('Unsupported matcher. Must be one of String|Function|RegExp.') + } + } + + disableNetConnect () { + this[kNetConnect] = false + } + + // This is required to bypass issues caused by using global symbols - see: + // https://github.com/nodejs/undici/issues/1447 + get isMockActive () { + return this[kIsMockActive] + } + + [kMockAgentSet] (origin, dispatcher) { + this[kClients].set(origin, new FakeWeakRef(dispatcher)) + } + + [kFactory] (origin) { + const mockOptions = Object.assign({ agent: this }, this[kOptions]) + return this[kOptions] && this[kOptions].connections === 1 + ? new MockClient(origin, mockOptions) + : new MockPool(origin, mockOptions) + } + + [kMockAgentGet] (origin) { + // First check if we can immediately find it + const ref = this[kClients].get(origin) + if (ref) { + return ref.deref() + } + + // If the origin is not a string create a dummy parent pool and return to user + if (typeof origin !== 'string') { + const dispatcher = this[kFactory]('http://localhost:9999') + this[kMockAgentSet](origin, dispatcher) + return dispatcher + } + + // If we match, create a pool and assign the same dispatches + for (const [keyMatcher, nonExplicitRef] of Array.from(this[kClients])) { + const nonExplicitDispatcher = nonExplicitRef.deref() + if (nonExplicitDispatcher && typeof keyMatcher !== 'string' && matchValue(keyMatcher, origin)) { + const dispatcher = this[kFactory](origin) + this[kMockAgentSet](origin, dispatcher) + dispatcher[kDispatches] = nonExplicitDispatcher[kDispatches] + return dispatcher + } + } + } + + [kGetNetConnect] () { + return this[kNetConnect] + } + + pendingInterceptors () { + const mockAgentClients = this[kClients] + + return Array.from(mockAgentClients.entries()) + .flatMap(([origin, scope]) => scope.deref()[kDispatches].map(dispatch => ({ ...dispatch, origin }))) + .filter(({ pending }) => pending) + } + + assertNoPendingInterceptors ({ pendingInterceptorsFormatter = new PendingInterceptorsFormatter() } = {}) { + const pending = this.pendingInterceptors() + + if (pending.length === 0) { + return + } + + const pluralizer = new Pluralizer('interceptor', 'interceptors').pluralize(pending.length) + + throw new UndiciError(` +${pluralizer.count} ${pluralizer.noun} ${pluralizer.is} pending: + +${pendingInterceptorsFormatter.format(pending)} +`.trim()) + } +} + +module.exports = MockAgent + + +/***/ }), + +/***/ 8687: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { promisify } = __nccwpck_require__(3837) +const Client = __nccwpck_require__(3598) +const { buildMockDispatch } = __nccwpck_require__(9323) +const { + kDispatches, + kMockAgent, + kClose, + kOriginalClose, + kOrigin, + kOriginalDispatch, + kConnected +} = __nccwpck_require__(4347) +const { MockInterceptor } = __nccwpck_require__(410) +const Symbols = __nccwpck_require__(2785) +const { InvalidArgumentError } = __nccwpck_require__(8045) + +/** + * MockClient provides an API that extends the Client to influence the mockDispatches. + */ +class MockClient extends Client { + constructor (origin, opts) { + super(origin, opts) + + if (!opts || !opts.agent || typeof opts.agent.dispatch !== 'function') { + throw new InvalidArgumentError('Argument opts.agent must implement Agent') + } + + this[kMockAgent] = opts.agent + this[kOrigin] = origin + this[kDispatches] = [] + this[kConnected] = 1 + this[kOriginalDispatch] = this.dispatch + this[kOriginalClose] = this.close.bind(this) + + this.dispatch = buildMockDispatch.call(this) + this.close = this[kClose] + } + + get [Symbols.kConnected] () { + return this[kConnected] + } + + /** + * Sets up the base interceptor for mocking replies from undici. + */ + intercept (opts) { + return new MockInterceptor(opts, this[kDispatches]) + } + + async [kClose] () { + await promisify(this[kOriginalClose])() + this[kConnected] = 0 + this[kMockAgent][Symbols.kClients].delete(this[kOrigin]) + } +} + +module.exports = MockClient + + +/***/ }), + +/***/ 888: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { UndiciError } = __nccwpck_require__(8045) + +class MockNotMatchedError extends UndiciError { + constructor (message) { + super(message) + Error.captureStackTrace(this, MockNotMatchedError) + this.name = 'MockNotMatchedError' + this.message = message || 'The request does not match any registered mock dispatches' + this.code = 'UND_MOCK_ERR_MOCK_NOT_MATCHED' + } +} + +module.exports = { + MockNotMatchedError +} + + +/***/ }), + +/***/ 410: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { getResponseData, buildKey, addMockDispatch } = __nccwpck_require__(9323) +const { + kDispatches, + kDispatchKey, + kDefaultHeaders, + kDefaultTrailers, + kContentLength, + kMockDispatch +} = __nccwpck_require__(4347) +const { InvalidArgumentError } = __nccwpck_require__(8045) +const { buildURL } = __nccwpck_require__(3983) + +/** + * Defines the scope API for an interceptor reply + */ +class MockScope { + constructor (mockDispatch) { + this[kMockDispatch] = mockDispatch + } + + /** + * Delay a reply by a set amount in ms. + */ + delay (waitInMs) { + if (typeof waitInMs !== 'number' || !Number.isInteger(waitInMs) || waitInMs <= 0) { + throw new InvalidArgumentError('waitInMs must be a valid integer > 0') + } + + this[kMockDispatch].delay = waitInMs + return this + } + + /** + * For a defined reply, never mark as consumed. + */ + persist () { + this[kMockDispatch].persist = true + return this + } + + /** + * Allow one to define a reply for a set amount of matching requests. + */ + times (repeatTimes) { + if (typeof repeatTimes !== 'number' || !Number.isInteger(repeatTimes) || repeatTimes <= 0) { + throw new InvalidArgumentError('repeatTimes must be a valid integer > 0') + } + + this[kMockDispatch].times = repeatTimes + return this + } +} + +/** + * Defines an interceptor for a Mock + */ +class MockInterceptor { + constructor (opts, mockDispatches) { + if (typeof opts !== 'object') { + throw new InvalidArgumentError('opts must be an object') + } + if (typeof opts.path === 'undefined') { + throw new InvalidArgumentError('opts.path must be defined') + } + if (typeof opts.method === 'undefined') { + opts.method = 'GET' + } + // See https://github.com/nodejs/undici/issues/1245 + // As per RFC 3986, clients are not supposed to send URI + // fragments to servers when they retrieve a document, + if (typeof opts.path === 'string') { + if (opts.query) { + opts.path = buildURL(opts.path, opts.query) + } else { + // Matches https://github.com/nodejs/undici/blob/main/lib/fetch/index.js#L1811 + const parsedURL = new URL(opts.path, 'data://') + opts.path = parsedURL.pathname + parsedURL.search + } + } + if (typeof opts.method === 'string') { + opts.method = opts.method.toUpperCase() + } + + this[kDispatchKey] = buildKey(opts) + this[kDispatches] = mockDispatches + this[kDefaultHeaders] = {} + this[kDefaultTrailers] = {} + this[kContentLength] = false + } + + createMockScopeDispatchData (statusCode, data, responseOptions = {}) { + const responseData = getResponseData(data) + const contentLength = this[kContentLength] ? { 'content-length': responseData.length } : {} + const headers = { ...this[kDefaultHeaders], ...contentLength, ...responseOptions.headers } + const trailers = { ...this[kDefaultTrailers], ...responseOptions.trailers } + + return { statusCode, data, headers, trailers } + } + + validateReplyParameters (statusCode, data, responseOptions) { + if (typeof statusCode === 'undefined') { + throw new InvalidArgumentError('statusCode must be defined') + } + if (typeof data === 'undefined') { + throw new InvalidArgumentError('data must be defined') + } + if (typeof responseOptions !== 'object') { + throw new InvalidArgumentError('responseOptions must be an object') + } + } + + /** + * Mock an undici request with a defined reply. + */ + reply (replyData) { + // Values of reply aren't available right now as they + // can only be available when the reply callback is invoked. + if (typeof replyData === 'function') { + // We'll first wrap the provided callback in another function, + // this function will properly resolve the data from the callback + // when invoked. + const wrappedDefaultsCallback = (opts) => { + // Our reply options callback contains the parameter for statusCode, data and options. + const resolvedData = replyData(opts) + + // Check if it is in the right format + if (typeof resolvedData !== 'object') { + throw new InvalidArgumentError('reply options callback must return an object') + } + + const { statusCode, data = '', responseOptions = {} } = resolvedData + this.validateReplyParameters(statusCode, data, responseOptions) + // Since the values can be obtained immediately we return them + // from this higher order function that will be resolved later. + return { + ...this.createMockScopeDispatchData(statusCode, data, responseOptions) + } + } + + // Add usual dispatch data, but this time set the data parameter to function that will eventually provide data. + const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], wrappedDefaultsCallback) + return new MockScope(newMockDispatch) + } + + // We can have either one or three parameters, if we get here, + // we should have 1-3 parameters. So we spread the arguments of + // this function to obtain the parameters, since replyData will always + // just be the statusCode. + const [statusCode, data = '', responseOptions = {}] = [...arguments] + this.validateReplyParameters(statusCode, data, responseOptions) + + // Send in-already provided data like usual + const dispatchData = this.createMockScopeDispatchData(statusCode, data, responseOptions) + const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], dispatchData) + return new MockScope(newMockDispatch) + } + + /** + * Mock an undici request with a defined error. + */ + replyWithError (error) { + if (typeof error === 'undefined') { + throw new InvalidArgumentError('error must be defined') + } + + const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], { error }) + return new MockScope(newMockDispatch) + } + + /** + * Set default reply headers on the interceptor for subsequent replies + */ + defaultReplyHeaders (headers) { + if (typeof headers === 'undefined') { + throw new InvalidArgumentError('headers must be defined') + } + + this[kDefaultHeaders] = headers + return this + } + + /** + * Set default reply trailers on the interceptor for subsequent replies + */ + defaultReplyTrailers (trailers) { + if (typeof trailers === 'undefined') { + throw new InvalidArgumentError('trailers must be defined') + } + + this[kDefaultTrailers] = trailers + return this + } + + /** + * Set reply content length header for replies on the interceptor + */ + replyContentLength () { + this[kContentLength] = true + return this + } +} + +module.exports.MockInterceptor = MockInterceptor +module.exports.MockScope = MockScope + + +/***/ }), + +/***/ 6193: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { promisify } = __nccwpck_require__(3837) +const Pool = __nccwpck_require__(4634) +const { buildMockDispatch } = __nccwpck_require__(9323) +const { + kDispatches, + kMockAgent, + kClose, + kOriginalClose, + kOrigin, + kOriginalDispatch, + kConnected +} = __nccwpck_require__(4347) +const { MockInterceptor } = __nccwpck_require__(410) +const Symbols = __nccwpck_require__(2785) +const { InvalidArgumentError } = __nccwpck_require__(8045) + +/** + * MockPool provides an API that extends the Pool to influence the mockDispatches. + */ +class MockPool extends Pool { + constructor (origin, opts) { + super(origin, opts) + + if (!opts || !opts.agent || typeof opts.agent.dispatch !== 'function') { + throw new InvalidArgumentError('Argument opts.agent must implement Agent') + } + + this[kMockAgent] = opts.agent + this[kOrigin] = origin + this[kDispatches] = [] + this[kConnected] = 1 + this[kOriginalDispatch] = this.dispatch + this[kOriginalClose] = this.close.bind(this) + + this.dispatch = buildMockDispatch.call(this) + this.close = this[kClose] + } + + get [Symbols.kConnected] () { + return this[kConnected] + } + + /** + * Sets up the base interceptor for mocking replies from undici. + */ + intercept (opts) { + return new MockInterceptor(opts, this[kDispatches]) + } + + async [kClose] () { + await promisify(this[kOriginalClose])() + this[kConnected] = 0 + this[kMockAgent][Symbols.kClients].delete(this[kOrigin]) + } +} + +module.exports = MockPool + + +/***/ }), + +/***/ 4347: +/***/ ((module) => { + +"use strict"; + + +module.exports = { + kAgent: Symbol('agent'), + kOptions: Symbol('options'), + kFactory: Symbol('factory'), + kDispatches: Symbol('dispatches'), + kDispatchKey: Symbol('dispatch key'), + kDefaultHeaders: Symbol('default headers'), + kDefaultTrailers: Symbol('default trailers'), + kContentLength: Symbol('content length'), + kMockAgent: Symbol('mock agent'), + kMockAgentSet: Symbol('mock agent set'), + kMockAgentGet: Symbol('mock agent get'), + kMockDispatch: Symbol('mock dispatch'), + kClose: Symbol('close'), + kOriginalClose: Symbol('original agent close'), + kOrigin: Symbol('origin'), + kIsMockActive: Symbol('is mock active'), + kNetConnect: Symbol('net connect'), + kGetNetConnect: Symbol('get net connect'), + kConnected: Symbol('connected') +} + + +/***/ }), + +/***/ 9323: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { MockNotMatchedError } = __nccwpck_require__(888) +const { + kDispatches, + kMockAgent, + kOriginalDispatch, + kOrigin, + kGetNetConnect +} = __nccwpck_require__(4347) +const { buildURL, nop } = __nccwpck_require__(3983) +const { STATUS_CODES } = __nccwpck_require__(3685) +const { + types: { + isPromise + } +} = __nccwpck_require__(3837) + +function matchValue (match, value) { + if (typeof match === 'string') { + return match === value + } + if (match instanceof RegExp) { + return match.test(value) + } + if (typeof match === 'function') { + return match(value) === true + } + return false +} + +function lowerCaseEntries (headers) { + return Object.fromEntries( + Object.entries(headers).map(([headerName, headerValue]) => { + return [headerName.toLocaleLowerCase(), headerValue] + }) + ) +} + +/** + * @param {import('../../index').Headers|string[]|Record} headers + * @param {string} key + */ +function getHeaderByName (headers, key) { + if (Array.isArray(headers)) { + for (let i = 0; i < headers.length; i += 2) { + if (headers[i].toLocaleLowerCase() === key.toLocaleLowerCase()) { + return headers[i + 1] + } + } + + return undefined + } else if (typeof headers.get === 'function') { + return headers.get(key) + } else { + return lowerCaseEntries(headers)[key.toLocaleLowerCase()] + } +} + +/** @param {string[]} headers */ +function buildHeadersFromArray (headers) { // fetch HeadersList + const clone = headers.slice() + const entries = [] + for (let index = 0; index < clone.length; index += 2) { + entries.push([clone[index], clone[index + 1]]) + } + return Object.fromEntries(entries) +} + +function matchHeaders (mockDispatch, headers) { + if (typeof mockDispatch.headers === 'function') { + if (Array.isArray(headers)) { // fetch HeadersList + headers = buildHeadersFromArray(headers) + } + return mockDispatch.headers(headers ? lowerCaseEntries(headers) : {}) + } + if (typeof mockDispatch.headers === 'undefined') { + return true + } + if (typeof headers !== 'object' || typeof mockDispatch.headers !== 'object') { + return false + } + + for (const [matchHeaderName, matchHeaderValue] of Object.entries(mockDispatch.headers)) { + const headerValue = getHeaderByName(headers, matchHeaderName) + + if (!matchValue(matchHeaderValue, headerValue)) { + return false + } + } + return true +} + +function safeUrl (path) { + if (typeof path !== 'string') { + return path + } + + const pathSegments = path.split('?') + + if (pathSegments.length !== 2) { + return path + } + + const qp = new URLSearchParams(pathSegments.pop()) + qp.sort() + return [...pathSegments, qp.toString()].join('?') +} + +function matchKey (mockDispatch, { path, method, body, headers }) { + const pathMatch = matchValue(mockDispatch.path, path) + const methodMatch = matchValue(mockDispatch.method, method) + const bodyMatch = typeof mockDispatch.body !== 'undefined' ? matchValue(mockDispatch.body, body) : true + const headersMatch = matchHeaders(mockDispatch, headers) + return pathMatch && methodMatch && bodyMatch && headersMatch +} + +function getResponseData (data) { + if (Buffer.isBuffer(data)) { + return data + } else if (typeof data === 'object') { + return JSON.stringify(data) + } else { + return data.toString() + } +} + +function getMockDispatch (mockDispatches, key) { + const basePath = key.query ? buildURL(key.path, key.query) : key.path + const resolvedPath = typeof basePath === 'string' ? safeUrl(basePath) : basePath + + // Match path + let matchedMockDispatches = mockDispatches.filter(({ consumed }) => !consumed).filter(({ path }) => matchValue(safeUrl(path), resolvedPath)) + if (matchedMockDispatches.length === 0) { + throw new MockNotMatchedError(`Mock dispatch not matched for path '${resolvedPath}'`) + } + + // Match method + matchedMockDispatches = matchedMockDispatches.filter(({ method }) => matchValue(method, key.method)) + if (matchedMockDispatches.length === 0) { + throw new MockNotMatchedError(`Mock dispatch not matched for method '${key.method}'`) + } + + // Match body + matchedMockDispatches = matchedMockDispatches.filter(({ body }) => typeof body !== 'undefined' ? matchValue(body, key.body) : true) + if (matchedMockDispatches.length === 0) { + throw new MockNotMatchedError(`Mock dispatch not matched for body '${key.body}'`) + } + + // Match headers + matchedMockDispatches = matchedMockDispatches.filter((mockDispatch) => matchHeaders(mockDispatch, key.headers)) + if (matchedMockDispatches.length === 0) { + throw new MockNotMatchedError(`Mock dispatch not matched for headers '${typeof key.headers === 'object' ? JSON.stringify(key.headers) : key.headers}'`) + } + + return matchedMockDispatches[0] +} + +function addMockDispatch (mockDispatches, key, data) { + const baseData = { timesInvoked: 0, times: 1, persist: false, consumed: false } + const replyData = typeof data === 'function' ? { callback: data } : { ...data } + const newMockDispatch = { ...baseData, ...key, pending: true, data: { error: null, ...replyData } } + mockDispatches.push(newMockDispatch) + return newMockDispatch +} + +function deleteMockDispatch (mockDispatches, key) { + const index = mockDispatches.findIndex(dispatch => { + if (!dispatch.consumed) { + return false + } + return matchKey(dispatch, key) + }) + if (index !== -1) { + mockDispatches.splice(index, 1) + } +} + +function buildKey (opts) { + const { path, method, body, headers, query } = opts + return { + path, + method, + body, + headers, + query + } +} + +function generateKeyValues (data) { + return Object.entries(data).reduce((keyValuePairs, [key, value]) => [ + ...keyValuePairs, + Buffer.from(`${key}`), + Array.isArray(value) ? value.map(x => Buffer.from(`${x}`)) : Buffer.from(`${value}`) + ], []) +} + +/** + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status + * @param {number} statusCode + */ +function getStatusText (statusCode) { + return STATUS_CODES[statusCode] || 'unknown' +} + +async function getResponse (body) { + const buffers = [] + for await (const data of body) { + buffers.push(data) + } + return Buffer.concat(buffers).toString('utf8') +} + +/** + * Mock dispatch function used to simulate undici dispatches + */ +function mockDispatch (opts, handler) { + // Get mock dispatch from built key + const key = buildKey(opts) + const mockDispatch = getMockDispatch(this[kDispatches], key) + + mockDispatch.timesInvoked++ + + // Here's where we resolve a callback if a callback is present for the dispatch data. + if (mockDispatch.data.callback) { + mockDispatch.data = { ...mockDispatch.data, ...mockDispatch.data.callback(opts) } + } + + // Parse mockDispatch data + const { data: { statusCode, data, headers, trailers, error }, delay, persist } = mockDispatch + const { timesInvoked, times } = mockDispatch + + // If it's used up and not persistent, mark as consumed + mockDispatch.consumed = !persist && timesInvoked >= times + mockDispatch.pending = timesInvoked < times + + // If specified, trigger dispatch error + if (error !== null) { + deleteMockDispatch(this[kDispatches], key) + handler.onError(error) + return true + } + + // Handle the request with a delay if necessary + if (typeof delay === 'number' && delay > 0) { + setTimeout(() => { + handleReply(this[kDispatches]) + }, delay) + } else { + handleReply(this[kDispatches]) + } + + function handleReply (mockDispatches, _data = data) { + // fetch's HeadersList is a 1D string array + const optsHeaders = Array.isArray(opts.headers) + ? buildHeadersFromArray(opts.headers) + : opts.headers + const body = typeof _data === 'function' + ? _data({ ...opts, headers: optsHeaders }) + : _data + + // util.types.isPromise is likely needed for jest. + if (isPromise(body)) { + // If handleReply is asynchronous, throwing an error + // in the callback will reject the promise, rather than + // synchronously throw the error, which breaks some tests. + // Rather, we wait for the callback to resolve if it is a + // promise, and then re-run handleReply with the new body. + body.then((newData) => handleReply(mockDispatches, newData)) + return + } + + const responseData = getResponseData(body) + const responseHeaders = generateKeyValues(headers) + const responseTrailers = generateKeyValues(trailers) + + handler.abort = nop + handler.onHeaders(statusCode, responseHeaders, resume, getStatusText(statusCode)) + handler.onData(Buffer.from(responseData)) + handler.onComplete(responseTrailers) + deleteMockDispatch(mockDispatches, key) + } + + function resume () {} + + return true +} + +function buildMockDispatch () { + const agent = this[kMockAgent] + const origin = this[kOrigin] + const originalDispatch = this[kOriginalDispatch] + + return function dispatch (opts, handler) { + if (agent.isMockActive) { + try { + mockDispatch.call(this, opts, handler) + } catch (error) { + if (error instanceof MockNotMatchedError) { + const netConnect = agent[kGetNetConnect]() + if (netConnect === false) { + throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect disabled)`) + } + if (checkNetConnect(netConnect, origin)) { + originalDispatch.call(this, opts, handler) + } else { + throw new MockNotMatchedError(`${error.message}: subsequent request to origin ${origin} was not allowed (net.connect is not enabled for this origin)`) + } + } else { + throw error + } + } + } else { + originalDispatch.call(this, opts, handler) + } + } +} + +function checkNetConnect (netConnect, origin) { + const url = new URL(origin) + if (netConnect === true) { + return true + } else if (Array.isArray(netConnect) && netConnect.some((matcher) => matchValue(matcher, url.host))) { + return true + } + return false +} + +function buildMockOptions (opts) { + if (opts) { + const { agent, ...mockOptions } = opts + return mockOptions + } +} + +module.exports = { + getResponseData, + getMockDispatch, + addMockDispatch, + deleteMockDispatch, + buildKey, + generateKeyValues, + matchValue, + getResponse, + getStatusText, + mockDispatch, + buildMockDispatch, + checkNetConnect, + buildMockOptions, + getHeaderByName +} + + +/***/ }), + +/***/ 6823: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { Transform } = __nccwpck_require__(2781) +const { Console } = __nccwpck_require__(6206) + +/** + * Gets the output of `console.table(…)` as a string. + */ +module.exports = class PendingInterceptorsFormatter { + constructor ({ disableColors } = {}) { + this.transform = new Transform({ + transform (chunk, _enc, cb) { + cb(null, chunk) + } + }) + + this.logger = new Console({ + stdout: this.transform, + inspectOptions: { + colors: !disableColors && !process.env.CI + } + }) + } + + format (pendingInterceptors) { + const withPrettyHeaders = pendingInterceptors.map( + ({ method, path, data: { statusCode }, persist, times, timesInvoked, origin }) => ({ + Method: method, + Origin: origin, + Path: path, + 'Status code': statusCode, + Persistent: persist ? '✅' : '❌', + Invocations: timesInvoked, + Remaining: persist ? Infinity : times - timesInvoked + })) + + this.logger.table(withPrettyHeaders) + return this.transform.read().toString() + } +} + + +/***/ }), + +/***/ 8891: +/***/ ((module) => { + +"use strict"; + + +const singulars = { + pronoun: 'it', + is: 'is', + was: 'was', + this: 'this' +} + +const plurals = { + pronoun: 'they', + is: 'are', + was: 'were', + this: 'these' +} + +module.exports = class Pluralizer { + constructor (singular, plural) { + this.singular = singular + this.plural = plural + } + + pluralize (count) { + const one = count === 1 + const keys = one ? singulars : plurals + const noun = one ? this.singular : this.plural + return { ...keys, count, noun } + } +} + + +/***/ }), + +/***/ 8266: +/***/ ((module) => { + +"use strict"; +/* eslint-disable */ + + + +// Extracted from node/lib/internal/fixed_queue.js + +// Currently optimal queue size, tested on V8 6.0 - 6.6. Must be power of two. +const kSize = 2048; +const kMask = kSize - 1; + +// The FixedQueue is implemented as a singly-linked list of fixed-size +// circular buffers. It looks something like this: +// +// head tail +// | | +// v v +// +-----------+ <-----\ +-----------+ <------\ +-----------+ +// | [null] | \----- | next | \------- | next | +// +-----------+ +-----------+ +-----------+ +// | item | <-- bottom | item | <-- bottom | [empty] | +// | item | | item | | [empty] | +// | item | | item | | [empty] | +// | item | | item | | [empty] | +// | item | | item | bottom --> | item | +// | item | | item | | item | +// | ... | | ... | | ... | +// | item | | item | | item | +// | item | | item | | item | +// | [empty] | <-- top | item | | item | +// | [empty] | | item | | item | +// | [empty] | | [empty] | <-- top top --> | [empty] | +// +-----------+ +-----------+ +-----------+ +// +// Or, if there is only one circular buffer, it looks something +// like either of these: +// +// head tail head tail +// | | | | +// v v v v +// +-----------+ +-----------+ +// | [null] | | [null] | +// +-----------+ +-----------+ +// | [empty] | | item | +// | [empty] | | item | +// | item | <-- bottom top --> | [empty] | +// | item | | [empty] | +// | [empty] | <-- top bottom --> | item | +// | [empty] | | item | +// +-----------+ +-----------+ +// +// Adding a value means moving `top` forward by one, removing means +// moving `bottom` forward by one. After reaching the end, the queue +// wraps around. +// +// When `top === bottom` the current queue is empty and when +// `top + 1 === bottom` it's full. This wastes a single space of storage +// but allows much quicker checks. + +class FixedCircularBuffer { + constructor() { + this.bottom = 0; + this.top = 0; + this.list = new Array(kSize); + this.next = null; + } + + isEmpty() { + return this.top === this.bottom; + } + + isFull() { + return ((this.top + 1) & kMask) === this.bottom; + } + + push(data) { + this.list[this.top] = data; + this.top = (this.top + 1) & kMask; + } + + shift() { + const nextItem = this.list[this.bottom]; + if (nextItem === undefined) + return null; + this.list[this.bottom] = undefined; + this.bottom = (this.bottom + 1) & kMask; + return nextItem; + } +} + +module.exports = class FixedQueue { + constructor() { + this.head = this.tail = new FixedCircularBuffer(); + } + + isEmpty() { + return this.head.isEmpty(); + } + + push(data) { + if (this.head.isFull()) { + // Head is full: Creates a new queue, sets the old queue's `.next` to it, + // and sets it as the new main queue. + this.head = this.head.next = new FixedCircularBuffer(); + } + this.head.push(data); + } + + shift() { + const tail = this.tail; + const next = tail.shift(); + if (tail.isEmpty() && tail.next !== null) { + // If there is another queue, it forms the new tail. + this.tail = tail.next; + } + return next; + } +}; + + +/***/ }), + +/***/ 3198: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const DispatcherBase = __nccwpck_require__(4839) +const FixedQueue = __nccwpck_require__(8266) +const { kConnected, kSize, kRunning, kPending, kQueued, kBusy, kFree, kUrl, kClose, kDestroy, kDispatch } = __nccwpck_require__(2785) +const PoolStats = __nccwpck_require__(9689) + +const kClients = Symbol('clients') +const kNeedDrain = Symbol('needDrain') +const kQueue = Symbol('queue') +const kClosedResolve = Symbol('closed resolve') +const kOnDrain = Symbol('onDrain') +const kOnConnect = Symbol('onConnect') +const kOnDisconnect = Symbol('onDisconnect') +const kOnConnectionError = Symbol('onConnectionError') +const kGetDispatcher = Symbol('get dispatcher') +const kAddClient = Symbol('add client') +const kRemoveClient = Symbol('remove client') +const kStats = Symbol('stats') + +class PoolBase extends DispatcherBase { + constructor () { + super() + + this[kQueue] = new FixedQueue() + this[kClients] = [] + this[kQueued] = 0 + + const pool = this + + this[kOnDrain] = function onDrain (origin, targets) { + const queue = pool[kQueue] + + let needDrain = false + + while (!needDrain) { + const item = queue.shift() + if (!item) { + break + } + pool[kQueued]-- + needDrain = !this.dispatch(item.opts, item.handler) + } + + this[kNeedDrain] = needDrain + + if (!this[kNeedDrain] && pool[kNeedDrain]) { + pool[kNeedDrain] = false + pool.emit('drain', origin, [pool, ...targets]) + } + + if (pool[kClosedResolve] && queue.isEmpty()) { + Promise + .all(pool[kClients].map(c => c.close())) + .then(pool[kClosedResolve]) + } + } + + this[kOnConnect] = (origin, targets) => { + pool.emit('connect', origin, [pool, ...targets]) + } + + this[kOnDisconnect] = (origin, targets, err) => { + pool.emit('disconnect', origin, [pool, ...targets], err) + } + + this[kOnConnectionError] = (origin, targets, err) => { + pool.emit('connectionError', origin, [pool, ...targets], err) + } + + this[kStats] = new PoolStats(this) + } + + get [kBusy] () { + return this[kNeedDrain] + } + + get [kConnected] () { + return this[kClients].filter(client => client[kConnected]).length + } + + get [kFree] () { + return this[kClients].filter(client => client[kConnected] && !client[kNeedDrain]).length + } + + get [kPending] () { + let ret = this[kQueued] + for (const { [kPending]: pending } of this[kClients]) { + ret += pending + } + return ret + } + + get [kRunning] () { + let ret = 0 + for (const { [kRunning]: running } of this[kClients]) { + ret += running + } + return ret + } + + get [kSize] () { + let ret = this[kQueued] + for (const { [kSize]: size } of this[kClients]) { + ret += size + } + return ret + } + + get stats () { + return this[kStats] + } + + async [kClose] () { + if (this[kQueue].isEmpty()) { + return Promise.all(this[kClients].map(c => c.close())) + } else { + return new Promise((resolve) => { + this[kClosedResolve] = resolve + }) + } + } + + async [kDestroy] (err) { + while (true) { + const item = this[kQueue].shift() + if (!item) { + break + } + item.handler.onError(err) + } + + return Promise.all(this[kClients].map(c => c.destroy(err))) + } + + [kDispatch] (opts, handler) { + const dispatcher = this[kGetDispatcher]() + + if (!dispatcher) { + this[kNeedDrain] = true + this[kQueue].push({ opts, handler }) + this[kQueued]++ + } else if (!dispatcher.dispatch(opts, handler)) { + dispatcher[kNeedDrain] = true + this[kNeedDrain] = !this[kGetDispatcher]() + } + + return !this[kNeedDrain] + } + + [kAddClient] (client) { + client + .on('drain', this[kOnDrain]) + .on('connect', this[kOnConnect]) + .on('disconnect', this[kOnDisconnect]) + .on('connectionError', this[kOnConnectionError]) + + this[kClients].push(client) + + if (this[kNeedDrain]) { + process.nextTick(() => { + if (this[kNeedDrain]) { + this[kOnDrain](client[kUrl], [this, client]) + } + }) + } + + return this + } + + [kRemoveClient] (client) { + client.close(() => { + const idx = this[kClients].indexOf(client) + if (idx !== -1) { + this[kClients].splice(idx, 1) + } + }) + + this[kNeedDrain] = this[kClients].some(dispatcher => ( + !dispatcher[kNeedDrain] && + dispatcher.closed !== true && + dispatcher.destroyed !== true + )) + } +} + +module.exports = { + PoolBase, + kClients, + kNeedDrain, + kAddClient, + kRemoveClient, + kGetDispatcher +} + + +/***/ }), + +/***/ 9689: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +const { kFree, kConnected, kPending, kQueued, kRunning, kSize } = __nccwpck_require__(2785) +const kPool = Symbol('pool') + +class PoolStats { + constructor (pool) { + this[kPool] = pool + } + + get connected () { + return this[kPool][kConnected] + } + + get free () { + return this[kPool][kFree] + } + + get pending () { + return this[kPool][kPending] + } + + get queued () { + return this[kPool][kQueued] + } + + get running () { + return this[kPool][kRunning] + } + + get size () { + return this[kPool][kSize] + } +} + +module.exports = PoolStats + + +/***/ }), + +/***/ 4634: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { + PoolBase, + kClients, + kNeedDrain, + kAddClient, + kGetDispatcher +} = __nccwpck_require__(3198) +const Client = __nccwpck_require__(3598) +const { + InvalidArgumentError +} = __nccwpck_require__(8045) +const util = __nccwpck_require__(3983) +const { kUrl, kInterceptors } = __nccwpck_require__(2785) +const buildConnector = __nccwpck_require__(2067) + +const kOptions = Symbol('options') +const kConnections = Symbol('connections') +const kFactory = Symbol('factory') + +function defaultFactory (origin, opts) { + return new Client(origin, opts) +} + +class Pool extends PoolBase { + constructor (origin, { + connections, + factory = defaultFactory, + connect, + connectTimeout, + tls, + maxCachedSessions, + socketPath, + autoSelectFamily, + autoSelectFamilyAttemptTimeout, + allowH2, + ...options + } = {}) { + super() + + if (connections != null && (!Number.isFinite(connections) || connections < 0)) { + throw new InvalidArgumentError('invalid connections') + } + + if (typeof factory !== 'function') { + throw new InvalidArgumentError('factory must be a function.') + } + + if (connect != null && typeof connect !== 'function' && typeof connect !== 'object') { + throw new InvalidArgumentError('connect must be a function or an object') + } + + if (typeof connect !== 'function') { + connect = buildConnector({ + ...tls, + maxCachedSessions, + allowH2, + socketPath, + timeout: connectTimeout, + ...(util.nodeHasAutoSelectFamily && autoSelectFamily ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined), + ...connect + }) + } + + this[kInterceptors] = options.interceptors && options.interceptors.Pool && Array.isArray(options.interceptors.Pool) + ? options.interceptors.Pool + : [] + this[kConnections] = connections || null + this[kUrl] = util.parseOrigin(origin) + this[kOptions] = { ...util.deepClone(options), connect, allowH2 } + this[kOptions].interceptors = options.interceptors + ? { ...options.interceptors } + : undefined + this[kFactory] = factory + } + + [kGetDispatcher] () { + let dispatcher = this[kClients].find(dispatcher => !dispatcher[kNeedDrain]) + + if (dispatcher) { + return dispatcher + } + + if (!this[kConnections] || this[kClients].length < this[kConnections]) { + dispatcher = this[kFactory](this[kUrl], this[kOptions]) + this[kAddClient](dispatcher) + } + + return dispatcher + } +} + +module.exports = Pool + + +/***/ }), + +/***/ 7858: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { kProxy, kClose, kDestroy, kInterceptors } = __nccwpck_require__(2785) +const { URL } = __nccwpck_require__(7310) +const Agent = __nccwpck_require__(7890) +const Pool = __nccwpck_require__(4634) +const DispatcherBase = __nccwpck_require__(4839) +const { InvalidArgumentError, RequestAbortedError } = __nccwpck_require__(8045) +const buildConnector = __nccwpck_require__(2067) + +const kAgent = Symbol('proxy agent') +const kClient = Symbol('proxy client') +const kProxyHeaders = Symbol('proxy headers') +const kRequestTls = Symbol('request tls settings') +const kProxyTls = Symbol('proxy tls settings') +const kConnectEndpoint = Symbol('connect endpoint function') + +function defaultProtocolPort (protocol) { + return protocol === 'https:' ? 443 : 80 +} + +function buildProxyOptions (opts) { + if (typeof opts === 'string') { + opts = { uri: opts } + } + + if (!opts || !opts.uri) { + throw new InvalidArgumentError('Proxy opts.uri is mandatory') + } + + return { + uri: opts.uri, + protocol: opts.protocol || 'https' + } +} + +function defaultFactory (origin, opts) { + return new Pool(origin, opts) +} + +class ProxyAgent extends DispatcherBase { + constructor (opts) { + super(opts) + this[kProxy] = buildProxyOptions(opts) + this[kAgent] = new Agent(opts) + this[kInterceptors] = opts.interceptors && opts.interceptors.ProxyAgent && Array.isArray(opts.interceptors.ProxyAgent) + ? opts.interceptors.ProxyAgent + : [] + + if (typeof opts === 'string') { + opts = { uri: opts } + } + + if (!opts || !opts.uri) { + throw new InvalidArgumentError('Proxy opts.uri is mandatory') + } + + const { clientFactory = defaultFactory } = opts + + if (typeof clientFactory !== 'function') { + throw new InvalidArgumentError('Proxy opts.clientFactory must be a function.') + } + + this[kRequestTls] = opts.requestTls + this[kProxyTls] = opts.proxyTls + this[kProxyHeaders] = opts.headers || {} + + const resolvedUrl = new URL(opts.uri) + const { origin, port, host, username, password } = resolvedUrl + + if (opts.auth && opts.token) { + throw new InvalidArgumentError('opts.auth cannot be used in combination with opts.token') + } else if (opts.auth) { + /* @deprecated in favour of opts.token */ + this[kProxyHeaders]['proxy-authorization'] = `Basic ${opts.auth}` + } else if (opts.token) { + this[kProxyHeaders]['proxy-authorization'] = opts.token + } else if (username && password) { + this[kProxyHeaders]['proxy-authorization'] = `Basic ${Buffer.from(`${decodeURIComponent(username)}:${decodeURIComponent(password)}`).toString('base64')}` + } + + const connect = buildConnector({ ...opts.proxyTls }) + this[kConnectEndpoint] = buildConnector({ ...opts.requestTls }) + this[kClient] = clientFactory(resolvedUrl, { connect }) + this[kAgent] = new Agent({ + ...opts, + connect: async (opts, callback) => { + let requestedHost = opts.host + if (!opts.port) { + requestedHost += `:${defaultProtocolPort(opts.protocol)}` + } + try { + const { socket, statusCode } = await this[kClient].connect({ + origin, + port, + path: requestedHost, + signal: opts.signal, + headers: { + ...this[kProxyHeaders], + host + } + }) + if (statusCode !== 200) { + socket.on('error', () => {}).destroy() + callback(new RequestAbortedError(`Proxy response (${statusCode}) !== 200 when HTTP Tunneling`)) + } + if (opts.protocol !== 'https:') { + callback(null, socket) + return + } + let servername + if (this[kRequestTls]) { + servername = this[kRequestTls].servername + } else { + servername = opts.servername + } + this[kConnectEndpoint]({ ...opts, servername, httpSocket: socket }, callback) + } catch (err) { + callback(err) + } + } + }) + } + + dispatch (opts, handler) { + const { host } = new URL(opts.origin) + const headers = buildHeaders(opts.headers) + throwIfProxyAuthIsSent(headers) + return this[kAgent].dispatch( + { + ...opts, + headers: { + ...headers, + host + } + }, + handler + ) + } + + async [kClose] () { + await this[kAgent].close() + await this[kClient].close() + } + + async [kDestroy] () { + await this[kAgent].destroy() + await this[kClient].destroy() + } +} + +/** + * @param {string[] | Record} headers + * @returns {Record} + */ +function buildHeaders (headers) { + // When using undici.fetch, the headers list is stored + // as an array. + if (Array.isArray(headers)) { + /** @type {Record} */ + const headersPair = {} + + for (let i = 0; i < headers.length; i += 2) { + headersPair[headers[i]] = headers[i + 1] + } + + return headersPair + } + + return headers +} + +/** + * @param {Record} headers + * + * Previous versions of ProxyAgent suggests the Proxy-Authorization in request headers + * Nevertheless, it was changed and to avoid a security vulnerability by end users + * this check was created. + * It should be removed in the next major version for performance reasons + */ +function throwIfProxyAuthIsSent (headers) { + const existProxyAuth = headers && Object.keys(headers) + .find((key) => key.toLowerCase() === 'proxy-authorization') + if (existProxyAuth) { + throw new InvalidArgumentError('Proxy-Authorization should be sent in ProxyAgent constructor') + } +} + +module.exports = ProxyAgent + + +/***/ }), + +/***/ 9459: +/***/ ((module) => { + +"use strict"; + + +let fastNow = Date.now() +let fastNowTimeout + +const fastTimers = [] + +function onTimeout () { + fastNow = Date.now() + + let len = fastTimers.length + let idx = 0 + while (idx < len) { + const timer = fastTimers[idx] + + if (timer.state === 0) { + timer.state = fastNow + timer.delay + } else if (timer.state > 0 && fastNow >= timer.state) { + timer.state = -1 + timer.callback(timer.opaque) + } + + if (timer.state === -1) { + timer.state = -2 + if (idx !== len - 1) { + fastTimers[idx] = fastTimers.pop() + } else { + fastTimers.pop() + } + len -= 1 + } else { + idx += 1 + } + } + + if (fastTimers.length > 0) { + refreshTimeout() + } +} + +function refreshTimeout () { + if (fastNowTimeout && fastNowTimeout.refresh) { + fastNowTimeout.refresh() + } else { + clearTimeout(fastNowTimeout) + fastNowTimeout = setTimeout(onTimeout, 1e3) + if (fastNowTimeout.unref) { + fastNowTimeout.unref() + } + } +} + +class Timeout { + constructor (callback, delay, opaque) { + this.callback = callback + this.delay = delay + this.opaque = opaque + + // -2 not in timer list + // -1 in timer list but inactive + // 0 in timer list waiting for time + // > 0 in timer list waiting for time to expire + this.state = -2 + + this.refresh() + } + + refresh () { + if (this.state === -2) { + fastTimers.push(this) + if (!fastNowTimeout || fastTimers.length === 1) { + refreshTimeout() + } + } + + this.state = 0 + } + + clear () { + this.state = -1 + } +} + +module.exports = { + setTimeout (callback, delay, opaque) { + return delay < 1e3 + ? setTimeout(callback, delay, opaque) + : new Timeout(callback, delay, opaque) + }, + clearTimeout (timeout) { + if (timeout instanceof Timeout) { + timeout.clear() + } else { + clearTimeout(timeout) + } + } +} + + +/***/ }), + +/***/ 5354: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const diagnosticsChannel = __nccwpck_require__(7643) +const { uid, states } = __nccwpck_require__(9188) +const { + kReadyState, + kSentClose, + kByteParser, + kReceivedClose +} = __nccwpck_require__(7578) +const { fireEvent, failWebsocketConnection } = __nccwpck_require__(5515) +const { CloseEvent } = __nccwpck_require__(2611) +const { makeRequest } = __nccwpck_require__(8359) +const { fetching } = __nccwpck_require__(4881) +const { Headers } = __nccwpck_require__(554) +const { getGlobalDispatcher } = __nccwpck_require__(1892) +const { kHeadersList } = __nccwpck_require__(2785) + +const channels = {} +channels.open = diagnosticsChannel.channel('undici:websocket:open') +channels.close = diagnosticsChannel.channel('undici:websocket:close') +channels.socketError = diagnosticsChannel.channel('undici:websocket:socket_error') + +/** @type {import('crypto')} */ +let crypto +try { + crypto = __nccwpck_require__(6113) +} catch { + +} + +/** + * @see https://websockets.spec.whatwg.org/#concept-websocket-establish + * @param {URL} url + * @param {string|string[]} protocols + * @param {import('./websocket').WebSocket} ws + * @param {(response: any) => void} onEstablish + * @param {Partial} options + */ +function establishWebSocketConnection (url, protocols, ws, onEstablish, options) { + // 1. Let requestURL be a copy of url, with its scheme set to "http", if url’s + // scheme is "ws", and to "https" otherwise. + const requestURL = url + + requestURL.protocol = url.protocol === 'ws:' ? 'http:' : 'https:' + + // 2. Let request be a new request, whose URL is requestURL, client is client, + // service-workers mode is "none", referrer is "no-referrer", mode is + // "websocket", credentials mode is "include", cache mode is "no-store" , + // and redirect mode is "error". + const request = makeRequest({ + urlList: [requestURL], + serviceWorkers: 'none', + referrer: 'no-referrer', + mode: 'websocket', + credentials: 'include', + cache: 'no-store', + redirect: 'error' + }) + + // Note: undici extension, allow setting custom headers. + if (options.headers) { + const headersList = new Headers(options.headers)[kHeadersList] + + request.headersList = headersList + } + + // 3. Append (`Upgrade`, `websocket`) to request’s header list. + // 4. Append (`Connection`, `Upgrade`) to request’s header list. + // Note: both of these are handled by undici currently. + // https://github.com/nodejs/undici/blob/68c269c4144c446f3f1220951338daef4a6b5ec4/lib/client.js#L1397 + + // 5. Let keyValue be a nonce consisting of a randomly selected + // 16-byte value that has been forgiving-base64-encoded and + // isomorphic encoded. + const keyValue = crypto.randomBytes(16).toString('base64') + + // 6. Append (`Sec-WebSocket-Key`, keyValue) to request’s + // header list. + request.headersList.append('sec-websocket-key', keyValue) + + // 7. Append (`Sec-WebSocket-Version`, `13`) to request’s + // header list. + request.headersList.append('sec-websocket-version', '13') + + // 8. For each protocol in protocols, combine + // (`Sec-WebSocket-Protocol`, protocol) in request’s header + // list. + for (const protocol of protocols) { + request.headersList.append('sec-websocket-protocol', protocol) + } + + // 9. Let permessageDeflate be a user-agent defined + // "permessage-deflate" extension header value. + // https://github.com/mozilla/gecko-dev/blob/ce78234f5e653a5d3916813ff990f053510227bc/netwerk/protocol/websocket/WebSocketChannel.cpp#L2673 + // TODO: enable once permessage-deflate is supported + const permessageDeflate = '' // 'permessage-deflate; 15' + + // 10. Append (`Sec-WebSocket-Extensions`, permessageDeflate) to + // request’s header list. + // request.headersList.append('sec-websocket-extensions', permessageDeflate) + + // 11. Fetch request with useParallelQueue set to true, and + // processResponse given response being these steps: + const controller = fetching({ + request, + useParallelQueue: true, + dispatcher: options.dispatcher ?? getGlobalDispatcher(), + processResponse (response) { + // 1. If response is a network error or its status is not 101, + // fail the WebSocket connection. + if (response.type === 'error' || response.status !== 101) { + failWebsocketConnection(ws, 'Received network error or non-101 status code.') + return + } + + // 2. If protocols is not the empty list and extracting header + // list values given `Sec-WebSocket-Protocol` and response’s + // header list results in null, failure, or the empty byte + // sequence, then fail the WebSocket connection. + if (protocols.length !== 0 && !response.headersList.get('Sec-WebSocket-Protocol')) { + failWebsocketConnection(ws, 'Server did not respond with sent protocols.') + return + } + + // 3. Follow the requirements stated step 2 to step 6, inclusive, + // of the last set of steps in section 4.1 of The WebSocket + // Protocol to validate response. This either results in fail + // the WebSocket connection or the WebSocket connection is + // established. + + // 2. If the response lacks an |Upgrade| header field or the |Upgrade| + // header field contains a value that is not an ASCII case- + // insensitive match for the value "websocket", the client MUST + // _Fail the WebSocket Connection_. + if (response.headersList.get('Upgrade')?.toLowerCase() !== 'websocket') { + failWebsocketConnection(ws, 'Server did not set Upgrade header to "websocket".') + return + } + + // 3. If the response lacks a |Connection| header field or the + // |Connection| header field doesn't contain a token that is an + // ASCII case-insensitive match for the value "Upgrade", the client + // MUST _Fail the WebSocket Connection_. + if (response.headersList.get('Connection')?.toLowerCase() !== 'upgrade') { + failWebsocketConnection(ws, 'Server did not set Connection header to "upgrade".') + return + } + + // 4. If the response lacks a |Sec-WebSocket-Accept| header field or + // the |Sec-WebSocket-Accept| contains a value other than the + // base64-encoded SHA-1 of the concatenation of the |Sec-WebSocket- + // Key| (as a string, not base64-decoded) with the string "258EAFA5- + // E914-47DA-95CA-C5AB0DC85B11" but ignoring any leading and + // trailing whitespace, the client MUST _Fail the WebSocket + // Connection_. + const secWSAccept = response.headersList.get('Sec-WebSocket-Accept') + const digest = crypto.createHash('sha1').update(keyValue + uid).digest('base64') + if (secWSAccept !== digest) { + failWebsocketConnection(ws, 'Incorrect hash received in Sec-WebSocket-Accept header.') + return + } + + // 5. If the response includes a |Sec-WebSocket-Extensions| header + // field and this header field indicates the use of an extension + // that was not present in the client's handshake (the server has + // indicated an extension not requested by the client), the client + // MUST _Fail the WebSocket Connection_. (The parsing of this + // header field to determine which extensions are requested is + // discussed in Section 9.1.) + const secExtension = response.headersList.get('Sec-WebSocket-Extensions') + + if (secExtension !== null && secExtension !== permessageDeflate) { + failWebsocketConnection(ws, 'Received different permessage-deflate than the one set.') + return + } + + // 6. If the response includes a |Sec-WebSocket-Protocol| header field + // and this header field indicates the use of a subprotocol that was + // not present in the client's handshake (the server has indicated a + // subprotocol not requested by the client), the client MUST _Fail + // the WebSocket Connection_. + const secProtocol = response.headersList.get('Sec-WebSocket-Protocol') + + if (secProtocol !== null && secProtocol !== request.headersList.get('Sec-WebSocket-Protocol')) { + failWebsocketConnection(ws, 'Protocol was not set in the opening handshake.') + return + } + + response.socket.on('data', onSocketData) + response.socket.on('close', onSocketClose) + response.socket.on('error', onSocketError) + + if (channels.open.hasSubscribers) { + channels.open.publish({ + address: response.socket.address(), + protocol: secProtocol, + extensions: secExtension + }) + } + + onEstablish(response) + } + }) + + return controller +} + +/** + * @param {Buffer} chunk + */ +function onSocketData (chunk) { + if (!this.ws[kByteParser].write(chunk)) { + this.pause() + } +} + +/** + * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol + * @see https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.4 + */ +function onSocketClose () { + const { ws } = this + + // If the TCP connection was closed after the + // WebSocket closing handshake was completed, the WebSocket connection + // is said to have been closed _cleanly_. + const wasClean = ws[kSentClose] && ws[kReceivedClose] + + let code = 1005 + let reason = '' + + const result = ws[kByteParser].closingInfo + + if (result) { + code = result.code ?? 1005 + reason = result.reason + } else if (!ws[kSentClose]) { + // If _The WebSocket + // Connection is Closed_ and no Close control frame was received by the + // endpoint (such as could occur if the underlying transport connection + // is lost), _The WebSocket Connection Close Code_ is considered to be + // 1006. + code = 1006 + } + + // 1. Change the ready state to CLOSED (3). + ws[kReadyState] = states.CLOSED + + // 2. If the user agent was required to fail the WebSocket + // connection, or if the WebSocket connection was closed + // after being flagged as full, fire an event named error + // at the WebSocket object. + // TODO + + // 3. Fire an event named close at the WebSocket object, + // using CloseEvent, with the wasClean attribute + // initialized to true if the connection closed cleanly + // and false otherwise, the code attribute initialized to + // the WebSocket connection close code, and the reason + // attribute initialized to the result of applying UTF-8 + // decode without BOM to the WebSocket connection close + // reason. + fireEvent('close', ws, CloseEvent, { + wasClean, code, reason + }) + + if (channels.close.hasSubscribers) { + channels.close.publish({ + websocket: ws, + code, + reason + }) + } +} + +function onSocketError (error) { + const { ws } = this + + ws[kReadyState] = states.CLOSING + + if (channels.socketError.hasSubscribers) { + channels.socketError.publish(error) + } + + this.destroy() +} + +module.exports = { + establishWebSocketConnection +} + + +/***/ }), + +/***/ 9188: +/***/ ((module) => { + +"use strict"; + + +// This is a Globally Unique Identifier unique used +// to validate that the endpoint accepts websocket +// connections. +// See https://www.rfc-editor.org/rfc/rfc6455.html#section-1.3 +const uid = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' + +/** @type {PropertyDescriptor} */ +const staticPropertyDescriptors = { + enumerable: true, + writable: false, + configurable: false +} + +const states = { + CONNECTING: 0, + OPEN: 1, + CLOSING: 2, + CLOSED: 3 +} + +const opcodes = { + CONTINUATION: 0x0, + TEXT: 0x1, + BINARY: 0x2, + CLOSE: 0x8, + PING: 0x9, + PONG: 0xA +} + +const maxUnsigned16Bit = 2 ** 16 - 1 // 65535 + +const parserStates = { + INFO: 0, + PAYLOADLENGTH_16: 2, + PAYLOADLENGTH_64: 3, + READ_DATA: 4 +} + +const emptyBuffer = Buffer.allocUnsafe(0) + +module.exports = { + uid, + staticPropertyDescriptors, + states, + opcodes, + maxUnsigned16Bit, + parserStates, + emptyBuffer +} + + +/***/ }), + +/***/ 2611: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { webidl } = __nccwpck_require__(1744) +const { kEnumerableProperty } = __nccwpck_require__(3983) +const { MessagePort } = __nccwpck_require__(1267) + +/** + * @see https://html.spec.whatwg.org/multipage/comms.html#messageevent + */ +class MessageEvent extends Event { + #eventInit + + constructor (type, eventInitDict = {}) { + webidl.argumentLengthCheck(arguments, 1, { header: 'MessageEvent constructor' }) + + type = webidl.converters.DOMString(type) + eventInitDict = webidl.converters.MessageEventInit(eventInitDict) + + super(type, eventInitDict) + + this.#eventInit = eventInitDict + } + + get data () { + webidl.brandCheck(this, MessageEvent) + + return this.#eventInit.data + } + + get origin () { + webidl.brandCheck(this, MessageEvent) + + return this.#eventInit.origin + } + + get lastEventId () { + webidl.brandCheck(this, MessageEvent) + + return this.#eventInit.lastEventId + } + + get source () { + webidl.brandCheck(this, MessageEvent) + + return this.#eventInit.source + } + + get ports () { + webidl.brandCheck(this, MessageEvent) + + if (!Object.isFrozen(this.#eventInit.ports)) { + Object.freeze(this.#eventInit.ports) + } + + return this.#eventInit.ports + } + + initMessageEvent ( + type, + bubbles = false, + cancelable = false, + data = null, + origin = '', + lastEventId = '', + source = null, + ports = [] + ) { + webidl.brandCheck(this, MessageEvent) + + webidl.argumentLengthCheck(arguments, 1, { header: 'MessageEvent.initMessageEvent' }) + + return new MessageEvent(type, { + bubbles, cancelable, data, origin, lastEventId, source, ports + }) + } +} + +/** + * @see https://websockets.spec.whatwg.org/#the-closeevent-interface + */ +class CloseEvent extends Event { + #eventInit + + constructor (type, eventInitDict = {}) { + webidl.argumentLengthCheck(arguments, 1, { header: 'CloseEvent constructor' }) + + type = webidl.converters.DOMString(type) + eventInitDict = webidl.converters.CloseEventInit(eventInitDict) + + super(type, eventInitDict) + + this.#eventInit = eventInitDict + } + + get wasClean () { + webidl.brandCheck(this, CloseEvent) + + return this.#eventInit.wasClean + } + + get code () { + webidl.brandCheck(this, CloseEvent) + + return this.#eventInit.code + } + + get reason () { + webidl.brandCheck(this, CloseEvent) + + return this.#eventInit.reason + } +} + +// https://html.spec.whatwg.org/multipage/webappapis.html#the-errorevent-interface +class ErrorEvent extends Event { + #eventInit + + constructor (type, eventInitDict) { + webidl.argumentLengthCheck(arguments, 1, { header: 'ErrorEvent constructor' }) + + super(type, eventInitDict) + + type = webidl.converters.DOMString(type) + eventInitDict = webidl.converters.ErrorEventInit(eventInitDict ?? {}) + + this.#eventInit = eventInitDict + } + + get message () { + webidl.brandCheck(this, ErrorEvent) + + return this.#eventInit.message + } + + get filename () { + webidl.brandCheck(this, ErrorEvent) + + return this.#eventInit.filename + } + + get lineno () { + webidl.brandCheck(this, ErrorEvent) + + return this.#eventInit.lineno + } + + get colno () { + webidl.brandCheck(this, ErrorEvent) + + return this.#eventInit.colno + } + + get error () { + webidl.brandCheck(this, ErrorEvent) + + return this.#eventInit.error + } +} + +Object.defineProperties(MessageEvent.prototype, { + [Symbol.toStringTag]: { + value: 'MessageEvent', + configurable: true + }, + data: kEnumerableProperty, + origin: kEnumerableProperty, + lastEventId: kEnumerableProperty, + source: kEnumerableProperty, + ports: kEnumerableProperty, + initMessageEvent: kEnumerableProperty +}) + +Object.defineProperties(CloseEvent.prototype, { + [Symbol.toStringTag]: { + value: 'CloseEvent', + configurable: true + }, + reason: kEnumerableProperty, + code: kEnumerableProperty, + wasClean: kEnumerableProperty +}) + +Object.defineProperties(ErrorEvent.prototype, { + [Symbol.toStringTag]: { + value: 'ErrorEvent', + configurable: true + }, + message: kEnumerableProperty, + filename: kEnumerableProperty, + lineno: kEnumerableProperty, + colno: kEnumerableProperty, + error: kEnumerableProperty +}) + +webidl.converters.MessagePort = webidl.interfaceConverter(MessagePort) + +webidl.converters['sequence'] = webidl.sequenceConverter( + webidl.converters.MessagePort +) + +const eventInit = [ + { + key: 'bubbles', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'cancelable', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'composed', + converter: webidl.converters.boolean, + defaultValue: false + } +] + +webidl.converters.MessageEventInit = webidl.dictionaryConverter([ + ...eventInit, + { + key: 'data', + converter: webidl.converters.any, + defaultValue: null + }, + { + key: 'origin', + converter: webidl.converters.USVString, + defaultValue: '' + }, + { + key: 'lastEventId', + converter: webidl.converters.DOMString, + defaultValue: '' + }, + { + key: 'source', + // Node doesn't implement WindowProxy or ServiceWorker, so the only + // valid value for source is a MessagePort. + converter: webidl.nullableConverter(webidl.converters.MessagePort), + defaultValue: null + }, + { + key: 'ports', + converter: webidl.converters['sequence'], + get defaultValue () { + return [] + } + } +]) + +webidl.converters.CloseEventInit = webidl.dictionaryConverter([ + ...eventInit, + { + key: 'wasClean', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'code', + converter: webidl.converters['unsigned short'], + defaultValue: 0 + }, + { + key: 'reason', + converter: webidl.converters.USVString, + defaultValue: '' + } +]) + +webidl.converters.ErrorEventInit = webidl.dictionaryConverter([ + ...eventInit, + { + key: 'message', + converter: webidl.converters.DOMString, + defaultValue: '' + }, + { + key: 'filename', + converter: webidl.converters.USVString, + defaultValue: '' + }, + { + key: 'lineno', + converter: webidl.converters['unsigned long'], + defaultValue: 0 + }, + { + key: 'colno', + converter: webidl.converters['unsigned long'], + defaultValue: 0 + }, + { + key: 'error', + converter: webidl.converters.any + } +]) + +module.exports = { + MessageEvent, + CloseEvent, + ErrorEvent +} + + +/***/ }), + +/***/ 5444: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { maxUnsigned16Bit } = __nccwpck_require__(9188) + +/** @type {import('crypto')} */ +let crypto +try { + crypto = __nccwpck_require__(6113) +} catch { + +} + +class WebsocketFrameSend { + /** + * @param {Buffer|undefined} data + */ + constructor (data) { + this.frameData = data + this.maskKey = crypto.randomBytes(4) + } + + createFrame (opcode) { + const bodyLength = this.frameData?.byteLength ?? 0 + + /** @type {number} */ + let payloadLength = bodyLength // 0-125 + let offset = 6 + + if (bodyLength > maxUnsigned16Bit) { + offset += 8 // payload length is next 8 bytes + payloadLength = 127 + } else if (bodyLength > 125) { + offset += 2 // payload length is next 2 bytes + payloadLength = 126 + } + + const buffer = Buffer.allocUnsafe(bodyLength + offset) + + // Clear first 2 bytes, everything else is overwritten + buffer[0] = buffer[1] = 0 + buffer[0] |= 0x80 // FIN + buffer[0] = (buffer[0] & 0xF0) + opcode // opcode + + /*! ws. MIT License. Einar Otto Stangvik */ + buffer[offset - 4] = this.maskKey[0] + buffer[offset - 3] = this.maskKey[1] + buffer[offset - 2] = this.maskKey[2] + buffer[offset - 1] = this.maskKey[3] + + buffer[1] = payloadLength + + if (payloadLength === 126) { + buffer.writeUInt16BE(bodyLength, 2) + } else if (payloadLength === 127) { + // Clear extended payload length + buffer[2] = buffer[3] = 0 + buffer.writeUIntBE(bodyLength, 4, 6) + } + + buffer[1] |= 0x80 // MASK + + // mask body + for (let i = 0; i < bodyLength; i++) { + buffer[offset + i] = this.frameData[i] ^ this.maskKey[i % 4] + } + + return buffer + } +} + +module.exports = { + WebsocketFrameSend +} + + +/***/ }), + +/***/ 1688: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { Writable } = __nccwpck_require__(2781) +const diagnosticsChannel = __nccwpck_require__(7643) +const { parserStates, opcodes, states, emptyBuffer } = __nccwpck_require__(9188) +const { kReadyState, kSentClose, kResponse, kReceivedClose } = __nccwpck_require__(7578) +const { isValidStatusCode, failWebsocketConnection, websocketMessageReceived } = __nccwpck_require__(5515) +const { WebsocketFrameSend } = __nccwpck_require__(5444) + +// This code was influenced by ws released under the MIT license. +// Copyright (c) 2011 Einar Otto Stangvik +// Copyright (c) 2013 Arnout Kazemier and contributors +// Copyright (c) 2016 Luigi Pinca and contributors + +const channels = {} +channels.ping = diagnosticsChannel.channel('undici:websocket:ping') +channels.pong = diagnosticsChannel.channel('undici:websocket:pong') + +class ByteParser extends Writable { + #buffers = [] + #byteOffset = 0 + + #state = parserStates.INFO + + #info = {} + #fragments = [] + + constructor (ws) { + super() + + this.ws = ws + } + + /** + * @param {Buffer} chunk + * @param {() => void} callback + */ + _write (chunk, _, callback) { + this.#buffers.push(chunk) + this.#byteOffset += chunk.length + + this.run(callback) + } + + /** + * Runs whenever a new chunk is received. + * Callback is called whenever there are no more chunks buffering, + * or not enough bytes are buffered to parse. + */ + run (callback) { + while (true) { + if (this.#state === parserStates.INFO) { + // If there aren't enough bytes to parse the payload length, etc. + if (this.#byteOffset < 2) { + return callback() + } + + const buffer = this.consume(2) + + this.#info.fin = (buffer[0] & 0x80) !== 0 + this.#info.opcode = buffer[0] & 0x0F + + // If we receive a fragmented message, we use the type of the first + // frame to parse the full message as binary/text, when it's terminated + this.#info.originalOpcode ??= this.#info.opcode + + this.#info.fragmented = !this.#info.fin && this.#info.opcode !== opcodes.CONTINUATION + + if (this.#info.fragmented && this.#info.opcode !== opcodes.BINARY && this.#info.opcode !== opcodes.TEXT) { + // Only text and binary frames can be fragmented + failWebsocketConnection(this.ws, 'Invalid frame type was fragmented.') + return + } + + const payloadLength = buffer[1] & 0x7F + + if (payloadLength <= 125) { + this.#info.payloadLength = payloadLength + this.#state = parserStates.READ_DATA + } else if (payloadLength === 126) { + this.#state = parserStates.PAYLOADLENGTH_16 + } else if (payloadLength === 127) { + this.#state = parserStates.PAYLOADLENGTH_64 + } + + if (this.#info.fragmented && payloadLength > 125) { + // A fragmented frame can't be fragmented itself + failWebsocketConnection(this.ws, 'Fragmented frame exceeded 125 bytes.') + return + } else if ( + (this.#info.opcode === opcodes.PING || + this.#info.opcode === opcodes.PONG || + this.#info.opcode === opcodes.CLOSE) && + payloadLength > 125 + ) { + // Control frames can have a payload length of 125 bytes MAX + failWebsocketConnection(this.ws, 'Payload length for control frame exceeded 125 bytes.') + return + } else if (this.#info.opcode === opcodes.CLOSE) { + if (payloadLength === 1) { + failWebsocketConnection(this.ws, 'Received close frame with a 1-byte body.') + return + } + + const body = this.consume(payloadLength) + + this.#info.closeInfo = this.parseCloseBody(false, body) + + if (!this.ws[kSentClose]) { + // If an endpoint receives a Close frame and did not previously send a + // Close frame, the endpoint MUST send a Close frame in response. (When + // sending a Close frame in response, the endpoint typically echos the + // status code it received.) + const body = Buffer.allocUnsafe(2) + body.writeUInt16BE(this.#info.closeInfo.code, 0) + const closeFrame = new WebsocketFrameSend(body) + + this.ws[kResponse].socket.write( + closeFrame.createFrame(opcodes.CLOSE), + (err) => { + if (!err) { + this.ws[kSentClose] = true + } + } + ) + } + + // Upon either sending or receiving a Close control frame, it is said + // that _The WebSocket Closing Handshake is Started_ and that the + // WebSocket connection is in the CLOSING state. + this.ws[kReadyState] = states.CLOSING + this.ws[kReceivedClose] = true + + this.end() + + return + } else if (this.#info.opcode === opcodes.PING) { + // Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in + // response, unless it already received a Close frame. + // A Pong frame sent in response to a Ping frame must have identical + // "Application data" + + const body = this.consume(payloadLength) + + if (!this.ws[kReceivedClose]) { + const frame = new WebsocketFrameSend(body) + + this.ws[kResponse].socket.write(frame.createFrame(opcodes.PONG)) + + if (channels.ping.hasSubscribers) { + channels.ping.publish({ + payload: body + }) + } + } + + this.#state = parserStates.INFO + + if (this.#byteOffset > 0) { + continue + } else { + callback() + return + } + } else if (this.#info.opcode === opcodes.PONG) { + // A Pong frame MAY be sent unsolicited. This serves as a + // unidirectional heartbeat. A response to an unsolicited Pong frame is + // not expected. + + const body = this.consume(payloadLength) + + if (channels.pong.hasSubscribers) { + channels.pong.publish({ + payload: body + }) + } + + if (this.#byteOffset > 0) { + continue + } else { + callback() + return + } + } + } else if (this.#state === parserStates.PAYLOADLENGTH_16) { + if (this.#byteOffset < 2) { + return callback() + } + + const buffer = this.consume(2) + + this.#info.payloadLength = buffer.readUInt16BE(0) + this.#state = parserStates.READ_DATA + } else if (this.#state === parserStates.PAYLOADLENGTH_64) { + if (this.#byteOffset < 8) { + return callback() + } + + const buffer = this.consume(8) + const upper = buffer.readUInt32BE(0) + + // 2^31 is the maxinimum bytes an arraybuffer can contain + // on 32-bit systems. Although, on 64-bit systems, this is + // 2^53-1 bytes. + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_array_length + // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/common/globals.h;drc=1946212ac0100668f14eb9e2843bdd846e510a1e;bpv=1;bpt=1;l=1275 + // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/objects/js-array-buffer.h;l=34;drc=1946212ac0100668f14eb9e2843bdd846e510a1e + if (upper > 2 ** 31 - 1) { + failWebsocketConnection(this.ws, 'Received payload length > 2^31 bytes.') + return + } + + const lower = buffer.readUInt32BE(4) + + this.#info.payloadLength = (upper << 8) + lower + this.#state = parserStates.READ_DATA + } else if (this.#state === parserStates.READ_DATA) { + if (this.#byteOffset < this.#info.payloadLength) { + // If there is still more data in this chunk that needs to be read + return callback() + } else if (this.#byteOffset >= this.#info.payloadLength) { + // If the server sent multiple frames in a single chunk + + const body = this.consume(this.#info.payloadLength) + + this.#fragments.push(body) + + // If the frame is unfragmented, or a fragmented frame was terminated, + // a message was received + if (!this.#info.fragmented || (this.#info.fin && this.#info.opcode === opcodes.CONTINUATION)) { + const fullMessage = Buffer.concat(this.#fragments) + + websocketMessageReceived(this.ws, this.#info.originalOpcode, fullMessage) + + this.#info = {} + this.#fragments.length = 0 + } + + this.#state = parserStates.INFO + } + } + + if (this.#byteOffset > 0) { + continue + } else { + callback() + break + } + } + } + + /** + * Take n bytes from the buffered Buffers + * @param {number} n + * @returns {Buffer|null} + */ + consume (n) { + if (n > this.#byteOffset) { + return null + } else if (n === 0) { + return emptyBuffer + } + + if (this.#buffers[0].length === n) { + this.#byteOffset -= this.#buffers[0].length + return this.#buffers.shift() + } + + const buffer = Buffer.allocUnsafe(n) + let offset = 0 + + while (offset !== n) { + const next = this.#buffers[0] + const { length } = next + + if (length + offset === n) { + buffer.set(this.#buffers.shift(), offset) + break + } else if (length + offset > n) { + buffer.set(next.subarray(0, n - offset), offset) + this.#buffers[0] = next.subarray(n - offset) + break + } else { + buffer.set(this.#buffers.shift(), offset) + offset += next.length + } + } + + this.#byteOffset -= n + + return buffer + } + + parseCloseBody (onlyCode, data) { + // https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.5 + /** @type {number|undefined} */ + let code + + if (data.length >= 2) { + // _The WebSocket Connection Close Code_ is + // defined as the status code (Section 7.4) contained in the first Close + // control frame received by the application + code = data.readUInt16BE(0) + } + + if (onlyCode) { + if (!isValidStatusCode(code)) { + return null + } + + return { code } + } + + // https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.6 + /** @type {Buffer} */ + let reason = data.subarray(2) + + // Remove BOM + if (reason[0] === 0xEF && reason[1] === 0xBB && reason[2] === 0xBF) { + reason = reason.subarray(3) + } + + if (code !== undefined && !isValidStatusCode(code)) { + return null + } + + try { + // TODO: optimize this + reason = new TextDecoder('utf-8', { fatal: true }).decode(reason) + } catch { + return null + } + + return { code, reason } + } + + get closingInfo () { + return this.#info.closeInfo + } +} + +module.exports = { + ByteParser +} + + +/***/ }), + +/***/ 7578: +/***/ ((module) => { + +"use strict"; + + +module.exports = { + kWebSocketURL: Symbol('url'), + kReadyState: Symbol('ready state'), + kController: Symbol('controller'), + kResponse: Symbol('response'), + kBinaryType: Symbol('binary type'), + kSentClose: Symbol('sent close'), + kReceivedClose: Symbol('received close'), + kByteParser: Symbol('byte parser') +} + + +/***/ }), + +/***/ 5515: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { kReadyState, kController, kResponse, kBinaryType, kWebSocketURL } = __nccwpck_require__(7578) +const { states, opcodes } = __nccwpck_require__(9188) +const { MessageEvent, ErrorEvent } = __nccwpck_require__(2611) + +/* globals Blob */ + +/** + * @param {import('./websocket').WebSocket} ws + */ +function isEstablished (ws) { + // If the server's response is validated as provided for above, it is + // said that _The WebSocket Connection is Established_ and that the + // WebSocket Connection is in the OPEN state. + return ws[kReadyState] === states.OPEN +} + +/** + * @param {import('./websocket').WebSocket} ws + */ +function isClosing (ws) { + // Upon either sending or receiving a Close control frame, it is said + // that _The WebSocket Closing Handshake is Started_ and that the + // WebSocket connection is in the CLOSING state. + return ws[kReadyState] === states.CLOSING +} + +/** + * @param {import('./websocket').WebSocket} ws + */ +function isClosed (ws) { + return ws[kReadyState] === states.CLOSED +} + +/** + * @see https://dom.spec.whatwg.org/#concept-event-fire + * @param {string} e + * @param {EventTarget} target + * @param {EventInit | undefined} eventInitDict + */ +function fireEvent (e, target, eventConstructor = Event, eventInitDict) { + // 1. If eventConstructor is not given, then let eventConstructor be Event. + + // 2. Let event be the result of creating an event given eventConstructor, + // in the relevant realm of target. + // 3. Initialize event’s type attribute to e. + const event = new eventConstructor(e, eventInitDict) // eslint-disable-line new-cap + + // 4. Initialize any other IDL attributes of event as described in the + // invocation of this algorithm. + + // 5. Return the result of dispatching event at target, with legacy target + // override flag set if set. + target.dispatchEvent(event) +} + +/** + * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol + * @param {import('./websocket').WebSocket} ws + * @param {number} type Opcode + * @param {Buffer} data application data + */ +function websocketMessageReceived (ws, type, data) { + // 1. If ready state is not OPEN (1), then return. + if (ws[kReadyState] !== states.OPEN) { + return + } + + // 2. Let dataForEvent be determined by switching on type and binary type: + let dataForEvent + + if (type === opcodes.TEXT) { + // -> type indicates that the data is Text + // a new DOMString containing data + try { + dataForEvent = new TextDecoder('utf-8', { fatal: true }).decode(data) + } catch { + failWebsocketConnection(ws, 'Received invalid UTF-8 in text frame.') + return + } + } else if (type === opcodes.BINARY) { + if (ws[kBinaryType] === 'blob') { + // -> type indicates that the data is Binary and binary type is "blob" + // a new Blob object, created in the relevant Realm of the WebSocket + // object, that represents data as its raw data + dataForEvent = new Blob([data]) + } else { + // -> type indicates that the data is Binary and binary type is "arraybuffer" + // a new ArrayBuffer object, created in the relevant Realm of the + // WebSocket object, whose contents are data + dataForEvent = new Uint8Array(data).buffer + } + } + + // 3. Fire an event named message at the WebSocket object, using MessageEvent, + // with the origin attribute initialized to the serialization of the WebSocket + // object’s url's origin, and the data attribute initialized to dataForEvent. + fireEvent('message', ws, MessageEvent, { + origin: ws[kWebSocketURL].origin, + data: dataForEvent + }) +} + +/** + * @see https://datatracker.ietf.org/doc/html/rfc6455 + * @see https://datatracker.ietf.org/doc/html/rfc2616 + * @see https://bugs.chromium.org/p/chromium/issues/detail?id=398407 + * @param {string} protocol + */ +function isValidSubprotocol (protocol) { + // If present, this value indicates one + // or more comma-separated subprotocol the client wishes to speak, + // ordered by preference. The elements that comprise this value + // MUST be non-empty strings with characters in the range U+0021 to + // U+007E not including separator characters as defined in + // [RFC2616] and MUST all be unique strings. + if (protocol.length === 0) { + return false + } + + for (const char of protocol) { + const code = char.charCodeAt(0) + + if ( + code < 0x21 || + code > 0x7E || + char === '(' || + char === ')' || + char === '<' || + char === '>' || + char === '@' || + char === ',' || + char === ';' || + char === ':' || + char === '\\' || + char === '"' || + char === '/' || + char === '[' || + char === ']' || + char === '?' || + char === '=' || + char === '{' || + char === '}' || + code === 32 || // SP + code === 9 // HT + ) { + return false + } + } + + return true +} + +/** + * @see https://datatracker.ietf.org/doc/html/rfc6455#section-7-4 + * @param {number} code + */ +function isValidStatusCode (code) { + if (code >= 1000 && code < 1015) { + return ( + code !== 1004 && // reserved + code !== 1005 && // "MUST NOT be set as a status code" + code !== 1006 // "MUST NOT be set as a status code" + ) + } + + return code >= 3000 && code <= 4999 +} + +/** + * @param {import('./websocket').WebSocket} ws + * @param {string|undefined} reason + */ +function failWebsocketConnection (ws, reason) { + const { [kController]: controller, [kResponse]: response } = ws + + controller.abort() + + if (response?.socket && !response.socket.destroyed) { + response.socket.destroy() + } + + if (reason) { + fireEvent('error', ws, ErrorEvent, { + error: new Error(reason) + }) + } +} + +module.exports = { + isEstablished, + isClosing, + isClosed, + fireEvent, + isValidSubprotocol, + isValidStatusCode, + failWebsocketConnection, + websocketMessageReceived +} + + +/***/ }), + +/***/ 4284: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const { webidl } = __nccwpck_require__(1744) +const { DOMException } = __nccwpck_require__(1037) +const { URLSerializer } = __nccwpck_require__(685) +const { getGlobalOrigin } = __nccwpck_require__(1246) +const { staticPropertyDescriptors, states, opcodes, emptyBuffer } = __nccwpck_require__(9188) +const { + kWebSocketURL, + kReadyState, + kController, + kBinaryType, + kResponse, + kSentClose, + kByteParser +} = __nccwpck_require__(7578) +const { isEstablished, isClosing, isValidSubprotocol, failWebsocketConnection, fireEvent } = __nccwpck_require__(5515) +const { establishWebSocketConnection } = __nccwpck_require__(5354) +const { WebsocketFrameSend } = __nccwpck_require__(5444) +const { ByteParser } = __nccwpck_require__(1688) +const { kEnumerableProperty, isBlobLike } = __nccwpck_require__(3983) +const { getGlobalDispatcher } = __nccwpck_require__(1892) +const { types } = __nccwpck_require__(3837) + +let experimentalWarned = false + +// https://websockets.spec.whatwg.org/#interface-definition +class WebSocket extends EventTarget { + #events = { + open: null, + error: null, + close: null, + message: null + } + + #bufferedAmount = 0 + #protocol = '' + #extensions = '' + + /** + * @param {string} url + * @param {string|string[]} protocols + */ + constructor (url, protocols = []) { + super() + + webidl.argumentLengthCheck(arguments, 1, { header: 'WebSocket constructor' }) + + if (!experimentalWarned) { + experimentalWarned = true + process.emitWarning('WebSockets are experimental, expect them to change at any time.', { + code: 'UNDICI-WS' + }) + } + + const options = webidl.converters['DOMString or sequence or WebSocketInit'](protocols) + + url = webidl.converters.USVString(url) + protocols = options.protocols + + // 1. Let baseURL be this's relevant settings object's API base URL. + const baseURL = getGlobalOrigin() + + // 1. Let urlRecord be the result of applying the URL parser to url with baseURL. + let urlRecord + + try { + urlRecord = new URL(url, baseURL) + } catch (e) { + // 3. If urlRecord is failure, then throw a "SyntaxError" DOMException. + throw new DOMException(e, 'SyntaxError') + } + + // 4. If urlRecord’s scheme is "http", then set urlRecord’s scheme to "ws". + if (urlRecord.protocol === 'http:') { + urlRecord.protocol = 'ws:' + } else if (urlRecord.protocol === 'https:') { + // 5. Otherwise, if urlRecord’s scheme is "https", set urlRecord’s scheme to "wss". + urlRecord.protocol = 'wss:' + } + + // 6. If urlRecord’s scheme is not "ws" or "wss", then throw a "SyntaxError" DOMException. + if (urlRecord.protocol !== 'ws:' && urlRecord.protocol !== 'wss:') { + throw new DOMException( + `Expected a ws: or wss: protocol, got ${urlRecord.protocol}`, + 'SyntaxError' + ) + } + + // 7. If urlRecord’s fragment is non-null, then throw a "SyntaxError" + // DOMException. + if (urlRecord.hash || urlRecord.href.endsWith('#')) { + throw new DOMException('Got fragment', 'SyntaxError') + } + + // 8. If protocols is a string, set protocols to a sequence consisting + // of just that string. + if (typeof protocols === 'string') { + protocols = [protocols] + } + + // 9. If any of the values in protocols occur more than once or otherwise + // fail to match the requirements for elements that comprise the value + // of `Sec-WebSocket-Protocol` fields as defined by The WebSocket + // protocol, then throw a "SyntaxError" DOMException. + if (protocols.length !== new Set(protocols.map(p => p.toLowerCase())).size) { + throw new DOMException('Invalid Sec-WebSocket-Protocol value', 'SyntaxError') + } + + if (protocols.length > 0 && !protocols.every(p => isValidSubprotocol(p))) { + throw new DOMException('Invalid Sec-WebSocket-Protocol value', 'SyntaxError') + } + + // 10. Set this's url to urlRecord. + this[kWebSocketURL] = new URL(urlRecord.href) + + // 11. Let client be this's relevant settings object. + + // 12. Run this step in parallel: + + // 1. Establish a WebSocket connection given urlRecord, protocols, + // and client. + this[kController] = establishWebSocketConnection( + urlRecord, + protocols, + this, + (response) => this.#onConnectionEstablished(response), + options + ) + + // Each WebSocket object has an associated ready state, which is a + // number representing the state of the connection. Initially it must + // be CONNECTING (0). + this[kReadyState] = WebSocket.CONNECTING + + // The extensions attribute must initially return the empty string. + + // The protocol attribute must initially return the empty string. + + // Each WebSocket object has an associated binary type, which is a + // BinaryType. Initially it must be "blob". + this[kBinaryType] = 'blob' + } + + /** + * @see https://websockets.spec.whatwg.org/#dom-websocket-close + * @param {number|undefined} code + * @param {string|undefined} reason + */ + close (code = undefined, reason = undefined) { + webidl.brandCheck(this, WebSocket) + + if (code !== undefined) { + code = webidl.converters['unsigned short'](code, { clamp: true }) + } + + if (reason !== undefined) { + reason = webidl.converters.USVString(reason) + } + + // 1. If code is present, but is neither an integer equal to 1000 nor an + // integer in the range 3000 to 4999, inclusive, throw an + // "InvalidAccessError" DOMException. + if (code !== undefined) { + if (code !== 1000 && (code < 3000 || code > 4999)) { + throw new DOMException('invalid code', 'InvalidAccessError') + } + } + + let reasonByteLength = 0 + + // 2. If reason is present, then run these substeps: + if (reason !== undefined) { + // 1. Let reasonBytes be the result of encoding reason. + // 2. If reasonBytes is longer than 123 bytes, then throw a + // "SyntaxError" DOMException. + reasonByteLength = Buffer.byteLength(reason) + + if (reasonByteLength > 123) { + throw new DOMException( + `Reason must be less than 123 bytes; received ${reasonByteLength}`, + 'SyntaxError' + ) + } + } + + // 3. Run the first matching steps from the following list: + if (this[kReadyState] === WebSocket.CLOSING || this[kReadyState] === WebSocket.CLOSED) { + // If this's ready state is CLOSING (2) or CLOSED (3) + // Do nothing. + } else if (!isEstablished(this)) { + // If the WebSocket connection is not yet established + // Fail the WebSocket connection and set this's ready state + // to CLOSING (2). + failWebsocketConnection(this, 'Connection was closed before it was established.') + this[kReadyState] = WebSocket.CLOSING + } else if (!isClosing(this)) { + // If the WebSocket closing handshake has not yet been started + // Start the WebSocket closing handshake and set this's ready + // state to CLOSING (2). + // - If neither code nor reason is present, the WebSocket Close + // message must not have a body. + // - If code is present, then the status code to use in the + // WebSocket Close message must be the integer given by code. + // - If reason is also present, then reasonBytes must be + // provided in the Close message after the status code. + + const frame = new WebsocketFrameSend() + + // If neither code nor reason is present, the WebSocket Close + // message must not have a body. + + // If code is present, then the status code to use in the + // WebSocket Close message must be the integer given by code. + if (code !== undefined && reason === undefined) { + frame.frameData = Buffer.allocUnsafe(2) + frame.frameData.writeUInt16BE(code, 0) + } else if (code !== undefined && reason !== undefined) { + // If reason is also present, then reasonBytes must be + // provided in the Close message after the status code. + frame.frameData = Buffer.allocUnsafe(2 + reasonByteLength) + frame.frameData.writeUInt16BE(code, 0) + // the body MAY contain UTF-8-encoded data with value /reason/ + frame.frameData.write(reason, 2, 'utf-8') + } else { + frame.frameData = emptyBuffer + } + + /** @type {import('stream').Duplex} */ + const socket = this[kResponse].socket + + socket.write(frame.createFrame(opcodes.CLOSE), (err) => { + if (!err) { + this[kSentClose] = true + } + }) + + // Upon either sending or receiving a Close control frame, it is said + // that _The WebSocket Closing Handshake is Started_ and that the + // WebSocket connection is in the CLOSING state. + this[kReadyState] = states.CLOSING + } else { + // Otherwise + // Set this's ready state to CLOSING (2). + this[kReadyState] = WebSocket.CLOSING + } + } + + /** + * @see https://websockets.spec.whatwg.org/#dom-websocket-send + * @param {NodeJS.TypedArray|ArrayBuffer|Blob|string} data + */ + send (data) { + webidl.brandCheck(this, WebSocket) + + webidl.argumentLengthCheck(arguments, 1, { header: 'WebSocket.send' }) + + data = webidl.converters.WebSocketSendData(data) + + // 1. If this's ready state is CONNECTING, then throw an + // "InvalidStateError" DOMException. + if (this[kReadyState] === WebSocket.CONNECTING) { + throw new DOMException('Sent before connected.', 'InvalidStateError') + } + + // 2. Run the appropriate set of steps from the following list: + // https://datatracker.ietf.org/doc/html/rfc6455#section-6.1 + // https://datatracker.ietf.org/doc/html/rfc6455#section-5.2 + + if (!isEstablished(this) || isClosing(this)) { + return + } + + /** @type {import('stream').Duplex} */ + const socket = this[kResponse].socket + + // If data is a string + if (typeof data === 'string') { + // If the WebSocket connection is established and the WebSocket + // closing handshake has not yet started, then the user agent + // must send a WebSocket Message comprised of the data argument + // using a text frame opcode; if the data cannot be sent, e.g. + // because it would need to be buffered but the buffer is full, + // the user agent must flag the WebSocket as full and then close + // the WebSocket connection. Any invocation of this method with a + // string argument that does not throw an exception must increase + // the bufferedAmount attribute by the number of bytes needed to + // express the argument as UTF-8. + + const value = Buffer.from(data) + const frame = new WebsocketFrameSend(value) + const buffer = frame.createFrame(opcodes.TEXT) + + this.#bufferedAmount += value.byteLength + socket.write(buffer, () => { + this.#bufferedAmount -= value.byteLength + }) + } else if (types.isArrayBuffer(data)) { + // If the WebSocket connection is established, and the WebSocket + // closing handshake has not yet started, then the user agent must + // send a WebSocket Message comprised of data using a binary frame + // opcode; if the data cannot be sent, e.g. because it would need + // to be buffered but the buffer is full, the user agent must flag + // the WebSocket as full and then close the WebSocket connection. + // The data to be sent is the data stored in the buffer described + // by the ArrayBuffer object. Any invocation of this method with an + // ArrayBuffer argument that does not throw an exception must + // increase the bufferedAmount attribute by the length of the + // ArrayBuffer in bytes. + + const value = Buffer.from(data) + const frame = new WebsocketFrameSend(value) + const buffer = frame.createFrame(opcodes.BINARY) + + this.#bufferedAmount += value.byteLength + socket.write(buffer, () => { + this.#bufferedAmount -= value.byteLength + }) + } else if (ArrayBuffer.isView(data)) { + // If the WebSocket connection is established, and the WebSocket + // closing handshake has not yet started, then the user agent must + // send a WebSocket Message comprised of data using a binary frame + // opcode; if the data cannot be sent, e.g. because it would need to + // be buffered but the buffer is full, the user agent must flag the + // WebSocket as full and then close the WebSocket connection. The + // data to be sent is the data stored in the section of the buffer + // described by the ArrayBuffer object that data references. Any + // invocation of this method with this kind of argument that does + // not throw an exception must increase the bufferedAmount attribute + // by the length of data’s buffer in bytes. + + const ab = Buffer.from(data, data.byteOffset, data.byteLength) + + const frame = new WebsocketFrameSend(ab) + const buffer = frame.createFrame(opcodes.BINARY) + + this.#bufferedAmount += ab.byteLength + socket.write(buffer, () => { + this.#bufferedAmount -= ab.byteLength + }) + } else if (isBlobLike(data)) { + // If the WebSocket connection is established, and the WebSocket + // closing handshake has not yet started, then the user agent must + // send a WebSocket Message comprised of data using a binary frame + // opcode; if the data cannot be sent, e.g. because it would need to + // be buffered but the buffer is full, the user agent must flag the + // WebSocket as full and then close the WebSocket connection. The data + // to be sent is the raw data represented by the Blob object. Any + // invocation of this method with a Blob argument that does not throw + // an exception must increase the bufferedAmount attribute by the size + // of the Blob object’s raw data, in bytes. + + const frame = new WebsocketFrameSend() + + data.arrayBuffer().then((ab) => { + const value = Buffer.from(ab) + frame.frameData = value + const buffer = frame.createFrame(opcodes.BINARY) + + this.#bufferedAmount += value.byteLength + socket.write(buffer, () => { + this.#bufferedAmount -= value.byteLength + }) + }) + } + } + + get readyState () { + webidl.brandCheck(this, WebSocket) + + // The readyState getter steps are to return this's ready state. + return this[kReadyState] + } + + get bufferedAmount () { + webidl.brandCheck(this, WebSocket) + + return this.#bufferedAmount + } + + get url () { + webidl.brandCheck(this, WebSocket) + + // The url getter steps are to return this's url, serialized. + return URLSerializer(this[kWebSocketURL]) + } + + get extensions () { + webidl.brandCheck(this, WebSocket) + + return this.#extensions + } + + get protocol () { + webidl.brandCheck(this, WebSocket) + + return this.#protocol + } + + get onopen () { + webidl.brandCheck(this, WebSocket) + + return this.#events.open + } + + set onopen (fn) { + webidl.brandCheck(this, WebSocket) + + if (this.#events.open) { + this.removeEventListener('open', this.#events.open) + } + + if (typeof fn === 'function') { + this.#events.open = fn + this.addEventListener('open', fn) + } else { + this.#events.open = null + } + } + + get onerror () { + webidl.brandCheck(this, WebSocket) + + return this.#events.error + } + + set onerror (fn) { + webidl.brandCheck(this, WebSocket) + + if (this.#events.error) { + this.removeEventListener('error', this.#events.error) + } + + if (typeof fn === 'function') { + this.#events.error = fn + this.addEventListener('error', fn) + } else { + this.#events.error = null + } + } + + get onclose () { + webidl.brandCheck(this, WebSocket) + + return this.#events.close + } + + set onclose (fn) { + webidl.brandCheck(this, WebSocket) + + if (this.#events.close) { + this.removeEventListener('close', this.#events.close) + } + + if (typeof fn === 'function') { + this.#events.close = fn + this.addEventListener('close', fn) + } else { + this.#events.close = null + } + } + + get onmessage () { + webidl.brandCheck(this, WebSocket) + + return this.#events.message + } + + set onmessage (fn) { + webidl.brandCheck(this, WebSocket) + + if (this.#events.message) { + this.removeEventListener('message', this.#events.message) + } + + if (typeof fn === 'function') { + this.#events.message = fn + this.addEventListener('message', fn) + } else { + this.#events.message = null + } + } + + get binaryType () { + webidl.brandCheck(this, WebSocket) + + return this[kBinaryType] + } + + set binaryType (type) { + webidl.brandCheck(this, WebSocket) + + if (type !== 'blob' && type !== 'arraybuffer') { + this[kBinaryType] = 'blob' + } else { + this[kBinaryType] = type + } + } + + /** + * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol + */ + #onConnectionEstablished (response) { + // processResponse is called when the "response’s header list has been received and initialized." + // once this happens, the connection is open + this[kResponse] = response + + const parser = new ByteParser(this) + parser.on('drain', function onParserDrain () { + this.ws[kResponse].socket.resume() + }) + + response.socket.ws = this + this[kByteParser] = parser + + // 1. Change the ready state to OPEN (1). + this[kReadyState] = states.OPEN + + // 2. Change the extensions attribute’s value to the extensions in use, if + // it is not the null value. + // https://datatracker.ietf.org/doc/html/rfc6455#section-9.1 + const extensions = response.headersList.get('sec-websocket-extensions') + + if (extensions !== null) { + this.#extensions = extensions + } + + // 3. Change the protocol attribute’s value to the subprotocol in use, if + // it is not the null value. + // https://datatracker.ietf.org/doc/html/rfc6455#section-1.9 + const protocol = response.headersList.get('sec-websocket-protocol') + + if (protocol !== null) { + this.#protocol = protocol + } + + // 4. Fire an event named open at the WebSocket object. + fireEvent('open', this) + } +} + +// https://websockets.spec.whatwg.org/#dom-websocket-connecting +WebSocket.CONNECTING = WebSocket.prototype.CONNECTING = states.CONNECTING +// https://websockets.spec.whatwg.org/#dom-websocket-open +WebSocket.OPEN = WebSocket.prototype.OPEN = states.OPEN +// https://websockets.spec.whatwg.org/#dom-websocket-closing +WebSocket.CLOSING = WebSocket.prototype.CLOSING = states.CLOSING +// https://websockets.spec.whatwg.org/#dom-websocket-closed +WebSocket.CLOSED = WebSocket.prototype.CLOSED = states.CLOSED + +Object.defineProperties(WebSocket.prototype, { + CONNECTING: staticPropertyDescriptors, + OPEN: staticPropertyDescriptors, + CLOSING: staticPropertyDescriptors, + CLOSED: staticPropertyDescriptors, + url: kEnumerableProperty, + readyState: kEnumerableProperty, + bufferedAmount: kEnumerableProperty, + onopen: kEnumerableProperty, + onerror: kEnumerableProperty, + onclose: kEnumerableProperty, + close: kEnumerableProperty, + onmessage: kEnumerableProperty, + binaryType: kEnumerableProperty, + send: kEnumerableProperty, + extensions: kEnumerableProperty, + protocol: kEnumerableProperty, + [Symbol.toStringTag]: { + value: 'WebSocket', + writable: false, + enumerable: false, + configurable: true + } +}) + +Object.defineProperties(WebSocket, { + CONNECTING: staticPropertyDescriptors, + OPEN: staticPropertyDescriptors, + CLOSING: staticPropertyDescriptors, + CLOSED: staticPropertyDescriptors +}) + +webidl.converters['sequence'] = webidl.sequenceConverter( + webidl.converters.DOMString +) + +webidl.converters['DOMString or sequence'] = function (V) { + if (webidl.util.Type(V) === 'Object' && Symbol.iterator in V) { + return webidl.converters['sequence'](V) + } + + return webidl.converters.DOMString(V) +} + +// This implements the propsal made in https://github.com/whatwg/websockets/issues/42 +webidl.converters.WebSocketInit = webidl.dictionaryConverter([ + { + key: 'protocols', + converter: webidl.converters['DOMString or sequence'], + get defaultValue () { + return [] + } + }, + { + key: 'dispatcher', + converter: (V) => V, + get defaultValue () { + return getGlobalDispatcher() + } + }, + { + key: 'headers', + converter: webidl.nullableConverter(webidl.converters.HeadersInit) + } +]) + +webidl.converters['DOMString or sequence or WebSocketInit'] = function (V) { + if (webidl.util.Type(V) === 'Object' && !(Symbol.iterator in V)) { + return webidl.converters.WebSocketInit(V) + } + + return { protocols: webidl.converters['DOMString or sequence'](V) } +} + +webidl.converters.WebSocketSendData = function (V) { + if (webidl.util.Type(V) === 'Object') { + if (isBlobLike(V)) { + return webidl.converters.Blob(V, { strict: false }) + } + + if (ArrayBuffer.isView(V) || types.isAnyArrayBuffer(V)) { + return webidl.converters.BufferSource(V) + } + } + + return webidl.converters.USVString(V) +} + +module.exports = { + WebSocket +} + + +/***/ }), + +/***/ 5840: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +Object.defineProperty(exports, "v1", ({ + enumerable: true, + get: function () { + return _v.default; + } +})); +Object.defineProperty(exports, "v3", ({ + enumerable: true, + get: function () { + return _v2.default; + } +})); +Object.defineProperty(exports, "v4", ({ + enumerable: true, + get: function () { + return _v3.default; + } +})); +Object.defineProperty(exports, "v5", ({ + enumerable: true, + get: function () { + return _v4.default; + } +})); +Object.defineProperty(exports, "NIL", ({ + enumerable: true, + get: function () { + return _nil.default; + } +})); +Object.defineProperty(exports, "version", ({ + enumerable: true, + get: function () { + return _version.default; + } +})); +Object.defineProperty(exports, "validate", ({ + enumerable: true, + get: function () { + return _validate.default; + } +})); +Object.defineProperty(exports, "stringify", ({ + enumerable: true, + get: function () { + return _stringify.default; + } +})); +Object.defineProperty(exports, "parse", ({ + enumerable: true, + get: function () { + return _parse.default; + } +})); + +var _v = _interopRequireDefault(__nccwpck_require__(8628)); + +var _v2 = _interopRequireDefault(__nccwpck_require__(6409)); + +var _v3 = _interopRequireDefault(__nccwpck_require__(5122)); + +var _v4 = _interopRequireDefault(__nccwpck_require__(9120)); + +var _nil = _interopRequireDefault(__nccwpck_require__(5332)); + +var _version = _interopRequireDefault(__nccwpck_require__(1595)); + +var _validate = _interopRequireDefault(__nccwpck_require__(6900)); + +var _stringify = _interopRequireDefault(__nccwpck_require__(8950)); + +var _parse = _interopRequireDefault(__nccwpck_require__(2746)); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/***/ }), + +/***/ 4569: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = void 0; + +var _crypto = _interopRequireDefault(__nccwpck_require__(6113)); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function md5(bytes) { + if (Array.isArray(bytes)) { + bytes = Buffer.from(bytes); + } else if (typeof bytes === 'string') { + bytes = Buffer.from(bytes, 'utf8'); + } + + return _crypto.default.createHash('md5').update(bytes).digest(); +} + +var _default = md5; +exports["default"] = _default; + +/***/ }), + +/***/ 5332: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = void 0; +var _default = '00000000-0000-0000-0000-000000000000'; +exports["default"] = _default; + +/***/ }), + +/***/ 2746: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = void 0; + +var _validate = _interopRequireDefault(__nccwpck_require__(6900)); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function parse(uuid) { + if (!(0, _validate.default)(uuid)) { + throw TypeError('Invalid UUID'); + } + + let v; + const arr = new Uint8Array(16); // Parse ########-....-....-....-............ + + arr[0] = (v = parseInt(uuid.slice(0, 8), 16)) >>> 24; + arr[1] = v >>> 16 & 0xff; + arr[2] = v >>> 8 & 0xff; + arr[3] = v & 0xff; // Parse ........-####-....-....-............ + + arr[4] = (v = parseInt(uuid.slice(9, 13), 16)) >>> 8; + arr[5] = v & 0xff; // Parse ........-....-####-....-............ + + arr[6] = (v = parseInt(uuid.slice(14, 18), 16)) >>> 8; + arr[7] = v & 0xff; // Parse ........-....-....-####-............ + + arr[8] = (v = parseInt(uuid.slice(19, 23), 16)) >>> 8; + arr[9] = v & 0xff; // Parse ........-....-....-....-############ + // (Use "/" to avoid 32-bit truncation when bit-shifting high-order bytes) + + arr[10] = (v = parseInt(uuid.slice(24, 36), 16)) / 0x10000000000 & 0xff; + arr[11] = v / 0x100000000 & 0xff; + arr[12] = v >>> 24 & 0xff; + arr[13] = v >>> 16 & 0xff; + arr[14] = v >>> 8 & 0xff; + arr[15] = v & 0xff; + return arr; +} + +var _default = parse; +exports["default"] = _default; + +/***/ }), + +/***/ 814: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = void 0; +var _default = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i; +exports["default"] = _default; + +/***/ }), + +/***/ 807: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = rng; + +var _crypto = _interopRequireDefault(__nccwpck_require__(6113)); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +const rnds8Pool = new Uint8Array(256); // # of random values to pre-allocate + +let poolPtr = rnds8Pool.length; + +function rng() { + if (poolPtr > rnds8Pool.length - 16) { + _crypto.default.randomFillSync(rnds8Pool); + + poolPtr = 0; + } + + return rnds8Pool.slice(poolPtr, poolPtr += 16); +} + +/***/ }), + +/***/ 5274: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = void 0; + +var _crypto = _interopRequireDefault(__nccwpck_require__(6113)); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function sha1(bytes) { + if (Array.isArray(bytes)) { + bytes = Buffer.from(bytes); + } else if (typeof bytes === 'string') { + bytes = Buffer.from(bytes, 'utf8'); + } + + return _crypto.default.createHash('sha1').update(bytes).digest(); +} + +var _default = sha1; +exports["default"] = _default; + +/***/ }), + +/***/ 8950: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = void 0; + +var _validate = _interopRequireDefault(__nccwpck_require__(6900)); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/** + * Convert array of 16 byte values to UUID string format of the form: + * XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX + */ +const byteToHex = []; + +for (let i = 0; i < 256; ++i) { + byteToHex.push((i + 0x100).toString(16).substr(1)); +} + +function stringify(arr, offset = 0) { + // Note: Be careful editing this code! It's been tuned for performance + // and works in ways you may not expect. See https://github.com/uuidjs/uuid/pull/434 + const uuid = (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + '-' + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + '-' + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + '-' + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + '-' + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase(); // Consistency check for valid UUID. If this throws, it's likely due to one + // of the following: + // - One or more input array values don't map to a hex octet (leading to + // "undefined" in the uuid) + // - Invalid input values for the RFC `version` or `variant` fields + + if (!(0, _validate.default)(uuid)) { + throw TypeError('Stringified UUID is invalid'); + } + + return uuid; +} + +var _default = stringify; +exports["default"] = _default; + +/***/ }), + +/***/ 8628: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = void 0; + +var _rng = _interopRequireDefault(__nccwpck_require__(807)); + +var _stringify = _interopRequireDefault(__nccwpck_require__(8950)); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +// **`v1()` - Generate time-based UUID** +// +// Inspired by https://github.com/LiosK/UUID.js +// and http://docs.python.org/library/uuid.html +let _nodeId; + +let _clockseq; // Previous uuid creation time + + +let _lastMSecs = 0; +let _lastNSecs = 0; // See https://github.com/uuidjs/uuid for API details + +function v1(options, buf, offset) { + let i = buf && offset || 0; + const b = buf || new Array(16); + options = options || {}; + let node = options.node || _nodeId; + let clockseq = options.clockseq !== undefined ? options.clockseq : _clockseq; // node and clockseq need to be initialized to random values if they're not + // specified. We do this lazily to minimize issues related to insufficient + // system entropy. See #189 + + if (node == null || clockseq == null) { + const seedBytes = options.random || (options.rng || _rng.default)(); + + if (node == null) { + // Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1) + node = _nodeId = [seedBytes[0] | 0x01, seedBytes[1], seedBytes[2], seedBytes[3], seedBytes[4], seedBytes[5]]; + } + + if (clockseq == null) { + // Per 4.2.2, randomize (14 bit) clockseq + clockseq = _clockseq = (seedBytes[6] << 8 | seedBytes[7]) & 0x3fff; + } + } // UUID timestamps are 100 nano-second units since the Gregorian epoch, + // (1582-10-15 00:00). JSNumbers aren't precise enough for this, so + // time is handled internally as 'msecs' (integer milliseconds) and 'nsecs' + // (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00. + + + let msecs = options.msecs !== undefined ? options.msecs : Date.now(); // Per 4.2.1.2, use count of uuid's generated during the current clock + // cycle to simulate higher resolution clock + + let nsecs = options.nsecs !== undefined ? options.nsecs : _lastNSecs + 1; // Time since last uuid creation (in msecs) + + const dt = msecs - _lastMSecs + (nsecs - _lastNSecs) / 10000; // Per 4.2.1.2, Bump clockseq on clock regression + + if (dt < 0 && options.clockseq === undefined) { + clockseq = clockseq + 1 & 0x3fff; + } // Reset nsecs if clock regresses (new clockseq) or we've moved onto a new + // time interval + + + if ((dt < 0 || msecs > _lastMSecs) && options.nsecs === undefined) { + nsecs = 0; + } // Per 4.2.1.2 Throw error if too many uuids are requested + + + if (nsecs >= 10000) { + throw new Error("uuid.v1(): Can't create more than 10M uuids/sec"); + } + + _lastMSecs = msecs; + _lastNSecs = nsecs; + _clockseq = clockseq; // Per 4.1.4 - Convert from unix epoch to Gregorian epoch + + msecs += 12219292800000; // `time_low` + + const tl = ((msecs & 0xfffffff) * 10000 + nsecs) % 0x100000000; + b[i++] = tl >>> 24 & 0xff; + b[i++] = tl >>> 16 & 0xff; + b[i++] = tl >>> 8 & 0xff; + b[i++] = tl & 0xff; // `time_mid` + + const tmh = msecs / 0x100000000 * 10000 & 0xfffffff; + b[i++] = tmh >>> 8 & 0xff; + b[i++] = tmh & 0xff; // `time_high_and_version` + + b[i++] = tmh >>> 24 & 0xf | 0x10; // include version + + b[i++] = tmh >>> 16 & 0xff; // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant) + + b[i++] = clockseq >>> 8 | 0x80; // `clock_seq_low` + + b[i++] = clockseq & 0xff; // `node` + + for (let n = 0; n < 6; ++n) { + b[i + n] = node[n]; + } + + return buf || (0, _stringify.default)(b); +} + +var _default = v1; +exports["default"] = _default; + +/***/ }), + +/***/ 6409: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = void 0; + +var _v = _interopRequireDefault(__nccwpck_require__(5998)); + +var _md = _interopRequireDefault(__nccwpck_require__(4569)); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +const v3 = (0, _v.default)('v3', 0x30, _md.default); +var _default = v3; +exports["default"] = _default; + +/***/ }), + +/***/ 5998: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = _default; +exports.URL = exports.DNS = void 0; + +var _stringify = _interopRequireDefault(__nccwpck_require__(8950)); + +var _parse = _interopRequireDefault(__nccwpck_require__(2746)); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function stringToBytes(str) { + str = unescape(encodeURIComponent(str)); // UTF8 escape + + const bytes = []; + + for (let i = 0; i < str.length; ++i) { + bytes.push(str.charCodeAt(i)); + } + + return bytes; +} + +const DNS = '6ba7b810-9dad-11d1-80b4-00c04fd430c8'; +exports.DNS = DNS; +const URL = '6ba7b811-9dad-11d1-80b4-00c04fd430c8'; +exports.URL = URL; + +function _default(name, version, hashfunc) { + function generateUUID(value, namespace, buf, offset) { + if (typeof value === 'string') { + value = stringToBytes(value); + } + + if (typeof namespace === 'string') { + namespace = (0, _parse.default)(namespace); + } + + if (namespace.length !== 16) { + throw TypeError('Namespace must be array-like (16 iterable integer values, 0-255)'); + } // Compute hash of namespace and value, Per 4.3 + // Future: Use spread syntax when supported on all platforms, e.g. `bytes = + // hashfunc([...namespace, ... value])` + + + let bytes = new Uint8Array(16 + value.length); + bytes.set(namespace); + bytes.set(value, namespace.length); + bytes = hashfunc(bytes); + bytes[6] = bytes[6] & 0x0f | version; + bytes[8] = bytes[8] & 0x3f | 0x80; + + if (buf) { + offset = offset || 0; + + for (let i = 0; i < 16; ++i) { + buf[offset + i] = bytes[i]; + } + + return buf; + } + + return (0, _stringify.default)(bytes); + } // Function#name is not settable on some platforms (#270) + + + try { + generateUUID.name = name; // eslint-disable-next-line no-empty + } catch (err) {} // For CommonJS default export support + + + generateUUID.DNS = DNS; + generateUUID.URL = URL; + return generateUUID; +} + +/***/ }), + +/***/ 5122: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = void 0; + +var _rng = _interopRequireDefault(__nccwpck_require__(807)); + +var _stringify = _interopRequireDefault(__nccwpck_require__(8950)); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function v4(options, buf, offset) { + options = options || {}; + + const rnds = options.random || (options.rng || _rng.default)(); // Per 4.4, set bits for version and `clock_seq_hi_and_reserved` + + + rnds[6] = rnds[6] & 0x0f | 0x40; + rnds[8] = rnds[8] & 0x3f | 0x80; // Copy bytes to buffer, if provided + + if (buf) { + offset = offset || 0; + + for (let i = 0; i < 16; ++i) { + buf[offset + i] = rnds[i]; + } + + return buf; + } + + return (0, _stringify.default)(rnds); +} + +var _default = v4; +exports["default"] = _default; + +/***/ }), + +/***/ 9120: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = void 0; + +var _v = _interopRequireDefault(__nccwpck_require__(5998)); + +var _sha = _interopRequireDefault(__nccwpck_require__(5274)); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +const v5 = (0, _v.default)('v5', 0x50, _sha.default); +var _default = v5; +exports["default"] = _default; + +/***/ }), + +/***/ 6900: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = void 0; + +var _regex = _interopRequireDefault(__nccwpck_require__(814)); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function validate(uuid) { + return typeof uuid === 'string' && _regex.default.test(uuid); +} + +var _default = validate; +exports["default"] = _default; + +/***/ }), + +/***/ 1595: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + + +Object.defineProperty(exports, "__esModule", ({ + value: true +})); +exports["default"] = void 0; + +var _validate = _interopRequireDefault(__nccwpck_require__(6900)); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function version(uuid) { + if (!(0, _validate.default)(uuid)) { + throw TypeError('Invalid UUID'); + } + + return parseInt(uuid.substr(14, 1), 16); +} + +var _default = version; +exports["default"] = _default; + +/***/ }), + +/***/ 9491: +/***/ ((module) => { + +"use strict"; +module.exports = require("assert"); + +/***/ }), + +/***/ 852: +/***/ ((module) => { + +"use strict"; +module.exports = require("async_hooks"); + +/***/ }), + +/***/ 4300: +/***/ ((module) => { + +"use strict"; +module.exports = require("buffer"); + +/***/ }), + +/***/ 6206: +/***/ ((module) => { + +"use strict"; +module.exports = require("console"); + +/***/ }), + +/***/ 6113: +/***/ ((module) => { + +"use strict"; +module.exports = require("crypto"); + +/***/ }), + +/***/ 7643: +/***/ ((module) => { + +"use strict"; +module.exports = require("diagnostics_channel"); + +/***/ }), + +/***/ 2361: +/***/ ((module) => { + +"use strict"; +module.exports = require("events"); + +/***/ }), + +/***/ 7147: +/***/ ((module) => { + +"use strict"; +module.exports = require("fs"); + +/***/ }), + +/***/ 3685: +/***/ ((module) => { + +"use strict"; +module.exports = require("http"); + +/***/ }), + +/***/ 5158: +/***/ ((module) => { + +"use strict"; +module.exports = require("http2"); + +/***/ }), + +/***/ 5687: +/***/ ((module) => { + +"use strict"; +module.exports = require("https"); + +/***/ }), + +/***/ 1808: +/***/ ((module) => { + +"use strict"; +module.exports = require("net"); + +/***/ }), + +/***/ 5673: +/***/ ((module) => { + +"use strict"; +module.exports = require("node:events"); + +/***/ }), + +/***/ 4492: +/***/ ((module) => { + +"use strict"; +module.exports = require("node:stream"); + +/***/ }), + +/***/ 7261: +/***/ ((module) => { + +"use strict"; +module.exports = require("node:util"); + +/***/ }), + +/***/ 2037: +/***/ ((module) => { + +"use strict"; +module.exports = require("os"); + +/***/ }), + +/***/ 1017: +/***/ ((module) => { + +"use strict"; +module.exports = require("path"); + +/***/ }), + +/***/ 4074: +/***/ ((module) => { + +"use strict"; +module.exports = require("perf_hooks"); + +/***/ }), + +/***/ 3477: +/***/ ((module) => { + +"use strict"; +module.exports = require("querystring"); + +/***/ }), + +/***/ 2781: +/***/ ((module) => { + +"use strict"; +module.exports = require("stream"); + +/***/ }), + +/***/ 5356: +/***/ ((module) => { + +"use strict"; +module.exports = require("stream/web"); + +/***/ }), + +/***/ 1576: +/***/ ((module) => { + +"use strict"; +module.exports = require("string_decoder"); + +/***/ }), + +/***/ 4404: +/***/ ((module) => { + +"use strict"; +module.exports = require("tls"); + +/***/ }), + +/***/ 6224: +/***/ ((module) => { + +"use strict"; +module.exports = require("tty"); + +/***/ }), + +/***/ 7310: +/***/ ((module) => { + +"use strict"; +module.exports = require("url"); + +/***/ }), + +/***/ 3837: +/***/ ((module) => { + +"use strict"; +module.exports = require("util"); + +/***/ }), + +/***/ 9830: +/***/ ((module) => { + +"use strict"; +module.exports = require("util/types"); + +/***/ }), + +/***/ 1267: +/***/ ((module) => { + +"use strict"; +module.exports = require("worker_threads"); + +/***/ }), + +/***/ 9796: +/***/ ((module) => { + +"use strict"; +module.exports = require("zlib"); + +/***/ }), + +/***/ 2960: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const WritableStream = (__nccwpck_require__(4492).Writable) +const inherits = (__nccwpck_require__(7261).inherits) + +const StreamSearch = __nccwpck_require__(1142) + +const PartStream = __nccwpck_require__(1620) +const HeaderParser = __nccwpck_require__(2032) + +const DASH = 45 +const B_ONEDASH = Buffer.from('-') +const B_CRLF = Buffer.from('\r\n') +const EMPTY_FN = function () {} + +function Dicer (cfg) { + if (!(this instanceof Dicer)) { return new Dicer(cfg) } + WritableStream.call(this, cfg) + + if (!cfg || (!cfg.headerFirst && typeof cfg.boundary !== 'string')) { throw new TypeError('Boundary required') } + + if (typeof cfg.boundary === 'string') { this.setBoundary(cfg.boundary) } else { this._bparser = undefined } + + this._headerFirst = cfg.headerFirst + + this._dashes = 0 + this._parts = 0 + this._finished = false + this._realFinish = false + this._isPreamble = true + this._justMatched = false + this._firstWrite = true + this._inHeader = true + this._part = undefined + this._cb = undefined + this._ignoreData = false + this._partOpts = { highWaterMark: cfg.partHwm } + this._pause = false + + const self = this + this._hparser = new HeaderParser(cfg) + this._hparser.on('header', function (header) { + self._inHeader = false + self._part.emit('header', header) + }) +} +inherits(Dicer, WritableStream) + +Dicer.prototype.emit = function (ev) { + if (ev === 'finish' && !this._realFinish) { + if (!this._finished) { + const self = this + process.nextTick(function () { + self.emit('error', new Error('Unexpected end of multipart data')) + if (self._part && !self._ignoreData) { + const type = (self._isPreamble ? 'Preamble' : 'Part') + self._part.emit('error', new Error(type + ' terminated early due to unexpected end of multipart data')) + self._part.push(null) + process.nextTick(function () { + self._realFinish = true + self.emit('finish') + self._realFinish = false + }) + return + } + self._realFinish = true + self.emit('finish') + self._realFinish = false + }) + } + } else { WritableStream.prototype.emit.apply(this, arguments) } +} + +Dicer.prototype._write = function (data, encoding, cb) { + // ignore unexpected data (e.g. extra trailer data after finished) + if (!this._hparser && !this._bparser) { return cb() } + + if (this._headerFirst && this._isPreamble) { + if (!this._part) { + this._part = new PartStream(this._partOpts) + if (this._events.preamble) { this.emit('preamble', this._part) } else { this._ignore() } + } + const r = this._hparser.push(data) + if (!this._inHeader && r !== undefined && r < data.length) { data = data.slice(r) } else { return cb() } + } + + // allows for "easier" testing + if (this._firstWrite) { + this._bparser.push(B_CRLF) + this._firstWrite = false + } + + this._bparser.push(data) + + if (this._pause) { this._cb = cb } else { cb() } +} + +Dicer.prototype.reset = function () { + this._part = undefined + this._bparser = undefined + this._hparser = undefined +} + +Dicer.prototype.setBoundary = function (boundary) { + const self = this + this._bparser = new StreamSearch('\r\n--' + boundary) + this._bparser.on('info', function (isMatch, data, start, end) { + self._oninfo(isMatch, data, start, end) + }) +} + +Dicer.prototype._ignore = function () { + if (this._part && !this._ignoreData) { + this._ignoreData = true + this._part.on('error', EMPTY_FN) + // we must perform some kind of read on the stream even though we are + // ignoring the data, otherwise node's Readable stream will not emit 'end' + // after pushing null to the stream + this._part.resume() + } +} + +Dicer.prototype._oninfo = function (isMatch, data, start, end) { + let buf; const self = this; let i = 0; let r; let shouldWriteMore = true + + if (!this._part && this._justMatched && data) { + while (this._dashes < 2 && (start + i) < end) { + if (data[start + i] === DASH) { + ++i + ++this._dashes + } else { + if (this._dashes) { buf = B_ONEDASH } + this._dashes = 0 + break + } + } + if (this._dashes === 2) { + if ((start + i) < end && this._events.trailer) { this.emit('trailer', data.slice(start + i, end)) } + this.reset() + this._finished = true + // no more parts will be added + if (self._parts === 0) { + self._realFinish = true + self.emit('finish') + self._realFinish = false + } + } + if (this._dashes) { return } + } + if (this._justMatched) { this._justMatched = false } + if (!this._part) { + this._part = new PartStream(this._partOpts) + this._part._read = function (n) { + self._unpause() + } + if (this._isPreamble && this._events.preamble) { this.emit('preamble', this._part) } else if (this._isPreamble !== true && this._events.part) { this.emit('part', this._part) } else { this._ignore() } + if (!this._isPreamble) { this._inHeader = true } + } + if (data && start < end && !this._ignoreData) { + if (this._isPreamble || !this._inHeader) { + if (buf) { shouldWriteMore = this._part.push(buf) } + shouldWriteMore = this._part.push(data.slice(start, end)) + if (!shouldWriteMore) { this._pause = true } + } else if (!this._isPreamble && this._inHeader) { + if (buf) { this._hparser.push(buf) } + r = this._hparser.push(data.slice(start, end)) + if (!this._inHeader && r !== undefined && r < end) { this._oninfo(false, data, start + r, end) } + } + } + if (isMatch) { + this._hparser.reset() + if (this._isPreamble) { this._isPreamble = false } else { + if (start !== end) { + ++this._parts + this._part.on('end', function () { + if (--self._parts === 0) { + if (self._finished) { + self._realFinish = true + self.emit('finish') + self._realFinish = false + } else { + self._unpause() + } + } + }) + } + } + this._part.push(null) + this._part = undefined + this._ignoreData = false + this._justMatched = true + this._dashes = 0 + } +} + +Dicer.prototype._unpause = function () { + if (!this._pause) { return } + + this._pause = false + if (this._cb) { + const cb = this._cb + this._cb = undefined + cb() + } +} + +module.exports = Dicer + + +/***/ }), + +/***/ 2032: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const EventEmitter = (__nccwpck_require__(5673).EventEmitter) +const inherits = (__nccwpck_require__(7261).inherits) +const getLimit = __nccwpck_require__(1467) + +const StreamSearch = __nccwpck_require__(1142) + +const B_DCRLF = Buffer.from('\r\n\r\n') +const RE_CRLF = /\r\n/g +const RE_HDR = /^([^:]+):[ \t]?([\x00-\xFF]+)?$/ // eslint-disable-line no-control-regex + +function HeaderParser (cfg) { + EventEmitter.call(this) + + cfg = cfg || {} + const self = this + this.nread = 0 + this.maxed = false + this.npairs = 0 + this.maxHeaderPairs = getLimit(cfg, 'maxHeaderPairs', 2000) + this.maxHeaderSize = getLimit(cfg, 'maxHeaderSize', 80 * 1024) + this.buffer = '' + this.header = {} + this.finished = false + this.ss = new StreamSearch(B_DCRLF) + this.ss.on('info', function (isMatch, data, start, end) { + if (data && !self.maxed) { + if (self.nread + end - start >= self.maxHeaderSize) { + end = self.maxHeaderSize - self.nread + start + self.nread = self.maxHeaderSize + self.maxed = true + } else { self.nread += (end - start) } + + self.buffer += data.toString('binary', start, end) + } + if (isMatch) { self._finish() } + }) +} +inherits(HeaderParser, EventEmitter) + +HeaderParser.prototype.push = function (data) { + const r = this.ss.push(data) + if (this.finished) { return r } +} + +HeaderParser.prototype.reset = function () { + this.finished = false + this.buffer = '' + this.header = {} + this.ss.reset() +} + +HeaderParser.prototype._finish = function () { + if (this.buffer) { this._parseHeader() } + this.ss.matches = this.ss.maxMatches + const header = this.header + this.header = {} + this.buffer = '' + this.finished = true + this.nread = this.npairs = 0 + this.maxed = false + this.emit('header', header) +} + +HeaderParser.prototype._parseHeader = function () { + if (this.npairs === this.maxHeaderPairs) { return } + + const lines = this.buffer.split(RE_CRLF) + const len = lines.length + let m, h + + for (var i = 0; i < len; ++i) { // eslint-disable-line no-var + if (lines[i].length === 0) { continue } + if (lines[i][0] === '\t' || lines[i][0] === ' ') { + // folded header content + // RFC2822 says to just remove the CRLF and not the whitespace following + // it, so we follow the RFC and include the leading whitespace ... + if (h) { + this.header[h][this.header[h].length - 1] += lines[i] + continue + } + } + + const posColon = lines[i].indexOf(':') + if ( + posColon === -1 || + posColon === 0 + ) { + return + } + m = RE_HDR.exec(lines[i]) + h = m[1].toLowerCase() + this.header[h] = this.header[h] || [] + this.header[h].push((m[2] || '')) + if (++this.npairs === this.maxHeaderPairs) { break } + } +} + +module.exports = HeaderParser + + +/***/ }), + +/***/ 1620: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const inherits = (__nccwpck_require__(7261).inherits) +const ReadableStream = (__nccwpck_require__(4492).Readable) + +function PartStream (opts) { + ReadableStream.call(this, opts) +} +inherits(PartStream, ReadableStream) + +PartStream.prototype._read = function (n) {} + +module.exports = PartStream + + +/***/ }), + +/***/ 1142: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +/** + * Copyright Brian White. All rights reserved. + * + * @see https://github.com/mscdex/streamsearch + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + * + * Based heavily on the Streaming Boyer-Moore-Horspool C++ implementation + * by Hongli Lai at: https://github.com/FooBarWidget/boyer-moore-horspool + */ +const EventEmitter = (__nccwpck_require__(5673).EventEmitter) +const inherits = (__nccwpck_require__(7261).inherits) + +function SBMH (needle) { + if (typeof needle === 'string') { + needle = Buffer.from(needle) + } + + if (!Buffer.isBuffer(needle)) { + throw new TypeError('The needle has to be a String or a Buffer.') + } + + const needleLength = needle.length + + if (needleLength === 0) { + throw new Error('The needle cannot be an empty String/Buffer.') + } + + if (needleLength > 256) { + throw new Error('The needle cannot have a length bigger than 256.') + } + + this.maxMatches = Infinity + this.matches = 0 + + this._occ = new Array(256) + .fill(needleLength) // Initialize occurrence table. + this._lookbehind_size = 0 + this._needle = needle + this._bufpos = 0 + + this._lookbehind = Buffer.alloc(needleLength) + + // Populate occurrence table with analysis of the needle, + // ignoring last letter. + for (var i = 0; i < needleLength - 1; ++i) { // eslint-disable-line no-var + this._occ[needle[i]] = needleLength - 1 - i + } +} +inherits(SBMH, EventEmitter) + +SBMH.prototype.reset = function () { + this._lookbehind_size = 0 + this.matches = 0 + this._bufpos = 0 +} + +SBMH.prototype.push = function (chunk, pos) { + if (!Buffer.isBuffer(chunk)) { + chunk = Buffer.from(chunk, 'binary') + } + const chlen = chunk.length + this._bufpos = pos || 0 + let r + while (r !== chlen && this.matches < this.maxMatches) { r = this._sbmh_feed(chunk) } + return r +} + +SBMH.prototype._sbmh_feed = function (data) { + const len = data.length + const needle = this._needle + const needleLength = needle.length + const lastNeedleChar = needle[needleLength - 1] + + // Positive: points to a position in `data` + // pos == 3 points to data[3] + // Negative: points to a position in the lookbehind buffer + // pos == -2 points to lookbehind[lookbehind_size - 2] + let pos = -this._lookbehind_size + let ch + + if (pos < 0) { + // Lookbehind buffer is not empty. Perform Boyer-Moore-Horspool + // search with character lookup code that considers both the + // lookbehind buffer and the current round's haystack data. + // + // Loop until + // there is a match. + // or until + // we've moved past the position that requires the + // lookbehind buffer. In this case we switch to the + // optimized loop. + // or until + // the character to look at lies outside the haystack. + while (pos < 0 && pos <= len - needleLength) { + ch = this._sbmh_lookup_char(data, pos + needleLength - 1) + + if ( + ch === lastNeedleChar && + this._sbmh_memcmp(data, pos, needleLength - 1) + ) { + this._lookbehind_size = 0 + ++this.matches + this.emit('info', true) + + return (this._bufpos = pos + needleLength) + } + pos += this._occ[ch] + } + + // No match. + + if (pos < 0) { + // There's too few data for Boyer-Moore-Horspool to run, + // so let's use a different algorithm to skip as much as + // we can. + // Forward pos until + // the trailing part of lookbehind + data + // looks like the beginning of the needle + // or until + // pos == 0 + while (pos < 0 && !this._sbmh_memcmp(data, pos, len - pos)) { ++pos } + } + + if (pos >= 0) { + // Discard lookbehind buffer. + this.emit('info', false, this._lookbehind, 0, this._lookbehind_size) + this._lookbehind_size = 0 + } else { + // Cut off part of the lookbehind buffer that has + // been processed and append the entire haystack + // into it. + const bytesToCutOff = this._lookbehind_size + pos + if (bytesToCutOff > 0) { + // The cut off data is guaranteed not to contain the needle. + this.emit('info', false, this._lookbehind, 0, bytesToCutOff) + } + + this._lookbehind.copy(this._lookbehind, 0, bytesToCutOff, + this._lookbehind_size - bytesToCutOff) + this._lookbehind_size -= bytesToCutOff + + data.copy(this._lookbehind, this._lookbehind_size) + this._lookbehind_size += len + + this._bufpos = len + return len + } + } + + pos += (pos >= 0) * this._bufpos + + // Lookbehind buffer is now empty. We only need to check if the + // needle is in the haystack. + if (data.indexOf(needle, pos) !== -1) { + pos = data.indexOf(needle, pos) + ++this.matches + if (pos > 0) { this.emit('info', true, data, this._bufpos, pos) } else { this.emit('info', true) } + + return (this._bufpos = pos + needleLength) + } else { + pos = len - needleLength + } + + // There was no match. If there's trailing haystack data that we cannot + // match yet using the Boyer-Moore-Horspool algorithm (because the trailing + // data is less than the needle size) then match using a modified + // algorithm that starts matching from the beginning instead of the end. + // Whatever trailing data is left after running this algorithm is added to + // the lookbehind buffer. + while ( + pos < len && + ( + data[pos] !== needle[0] || + ( + (Buffer.compare( + data.subarray(pos, pos + len - pos), + needle.subarray(0, len - pos) + ) !== 0) + ) + ) + ) { + ++pos + } + if (pos < len) { + data.copy(this._lookbehind, 0, pos, pos + (len - pos)) + this._lookbehind_size = len - pos + } + + // Everything until pos is guaranteed not to contain needle data. + if (pos > 0) { this.emit('info', false, data, this._bufpos, pos < len ? pos : len) } + + this._bufpos = len + return len +} + +SBMH.prototype._sbmh_lookup_char = function (data, pos) { + return (pos < 0) + ? this._lookbehind[this._lookbehind_size + pos] + : data[pos] +} + +SBMH.prototype._sbmh_memcmp = function (data, pos, len) { + for (var i = 0; i < len; ++i) { // eslint-disable-line no-var + if (this._sbmh_lookup_char(data, pos + i) !== this._needle[i]) { return false } + } + return true +} + +module.exports = SBMH + + +/***/ }), + +/***/ 727: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const WritableStream = (__nccwpck_require__(4492).Writable) +const { inherits } = __nccwpck_require__(7261) +const Dicer = __nccwpck_require__(2960) + +const MultipartParser = __nccwpck_require__(2183) +const UrlencodedParser = __nccwpck_require__(8306) +const parseParams = __nccwpck_require__(1854) + +function Busboy (opts) { + if (!(this instanceof Busboy)) { return new Busboy(opts) } + + if (typeof opts !== 'object') { + throw new TypeError('Busboy expected an options-Object.') + } + if (typeof opts.headers !== 'object') { + throw new TypeError('Busboy expected an options-Object with headers-attribute.') + } + if (typeof opts.headers['content-type'] !== 'string') { + throw new TypeError('Missing Content-Type-header.') + } + + const { + headers, + ...streamOptions + } = opts + + this.opts = { + autoDestroy: false, + ...streamOptions + } + WritableStream.call(this, this.opts) + + this._done = false + this._parser = this.getParserByHeaders(headers) + this._finished = false +} +inherits(Busboy, WritableStream) + +Busboy.prototype.emit = function (ev) { + if (ev === 'finish') { + if (!this._done) { + this._parser?.end() + return + } else if (this._finished) { + return + } + this._finished = true + } + WritableStream.prototype.emit.apply(this, arguments) +} + +Busboy.prototype.getParserByHeaders = function (headers) { + const parsed = parseParams(headers['content-type']) + + const cfg = { + defCharset: this.opts.defCharset, + fileHwm: this.opts.fileHwm, + headers, + highWaterMark: this.opts.highWaterMark, + isPartAFile: this.opts.isPartAFile, + limits: this.opts.limits, + parsedConType: parsed, + preservePath: this.opts.preservePath + } + + if (MultipartParser.detect.test(parsed[0])) { + return new MultipartParser(this, cfg) + } + if (UrlencodedParser.detect.test(parsed[0])) { + return new UrlencodedParser(this, cfg) + } + throw new Error('Unsupported Content-Type.') +} + +Busboy.prototype._write = function (chunk, encoding, cb) { + this._parser.write(chunk, cb) +} + +module.exports = Busboy +module.exports["default"] = Busboy +module.exports.Busboy = Busboy + +module.exports.Dicer = Dicer + + +/***/ }), + +/***/ 2183: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +// TODO: +// * support 1 nested multipart level +// (see second multipart example here: +// http://www.w3.org/TR/html401/interact/forms.html#didx-multipartform-data) +// * support limits.fieldNameSize +// -- this will require modifications to utils.parseParams + +const { Readable } = __nccwpck_require__(4492) +const { inherits } = __nccwpck_require__(7261) + +const Dicer = __nccwpck_require__(2960) + +const parseParams = __nccwpck_require__(1854) +const decodeText = __nccwpck_require__(4619) +const basename = __nccwpck_require__(8647) +const getLimit = __nccwpck_require__(1467) + +const RE_BOUNDARY = /^boundary$/i +const RE_FIELD = /^form-data$/i +const RE_CHARSET = /^charset$/i +const RE_FILENAME = /^filename$/i +const RE_NAME = /^name$/i + +Multipart.detect = /^multipart\/form-data/i +function Multipart (boy, cfg) { + let i + let len + const self = this + let boundary + const limits = cfg.limits + const isPartAFile = cfg.isPartAFile || ((fieldName, contentType, fileName) => (contentType === 'application/octet-stream' || fileName !== undefined)) + const parsedConType = cfg.parsedConType || [] + const defCharset = cfg.defCharset || 'utf8' + const preservePath = cfg.preservePath + const fileOpts = { highWaterMark: cfg.fileHwm } + + for (i = 0, len = parsedConType.length; i < len; ++i) { + if (Array.isArray(parsedConType[i]) && + RE_BOUNDARY.test(parsedConType[i][0])) { + boundary = parsedConType[i][1] + break + } + } + + function checkFinished () { + if (nends === 0 && finished && !boy._done) { + finished = false + self.end() + } + } + + if (typeof boundary !== 'string') { throw new Error('Multipart: Boundary not found') } + + const fieldSizeLimit = getLimit(limits, 'fieldSize', 1 * 1024 * 1024) + const fileSizeLimit = getLimit(limits, 'fileSize', Infinity) + const filesLimit = getLimit(limits, 'files', Infinity) + const fieldsLimit = getLimit(limits, 'fields', Infinity) + const partsLimit = getLimit(limits, 'parts', Infinity) + const headerPairsLimit = getLimit(limits, 'headerPairs', 2000) + const headerSizeLimit = getLimit(limits, 'headerSize', 80 * 1024) + + let nfiles = 0 + let nfields = 0 + let nends = 0 + let curFile + let curField + let finished = false + + this._needDrain = false + this._pause = false + this._cb = undefined + this._nparts = 0 + this._boy = boy + + const parserCfg = { + boundary, + maxHeaderPairs: headerPairsLimit, + maxHeaderSize: headerSizeLimit, + partHwm: fileOpts.highWaterMark, + highWaterMark: cfg.highWaterMark + } + + this.parser = new Dicer(parserCfg) + this.parser.on('drain', function () { + self._needDrain = false + if (self._cb && !self._pause) { + const cb = self._cb + self._cb = undefined + cb() + } + }).on('part', function onPart (part) { + if (++self._nparts > partsLimit) { + self.parser.removeListener('part', onPart) + self.parser.on('part', skipPart) + boy.hitPartsLimit = true + boy.emit('partsLimit') + return skipPart(part) + } + + // hack because streams2 _always_ doesn't emit 'end' until nextTick, so let + // us emit 'end' early since we know the part has ended if we are already + // seeing the next part + if (curField) { + const field = curField + field.emit('end') + field.removeAllListeners('end') + } + + part.on('header', function (header) { + let contype + let fieldname + let parsed + let charset + let encoding + let filename + let nsize = 0 + + if (header['content-type']) { + parsed = parseParams(header['content-type'][0]) + if (parsed[0]) { + contype = parsed[0].toLowerCase() + for (i = 0, len = parsed.length; i < len; ++i) { + if (RE_CHARSET.test(parsed[i][0])) { + charset = parsed[i][1].toLowerCase() + break + } + } + } + } + + if (contype === undefined) { contype = 'text/plain' } + if (charset === undefined) { charset = defCharset } + + if (header['content-disposition']) { + parsed = parseParams(header['content-disposition'][0]) + if (!RE_FIELD.test(parsed[0])) { return skipPart(part) } + for (i = 0, len = parsed.length; i < len; ++i) { + if (RE_NAME.test(parsed[i][0])) { + fieldname = parsed[i][1] + } else if (RE_FILENAME.test(parsed[i][0])) { + filename = parsed[i][1] + if (!preservePath) { filename = basename(filename) } + } + } + } else { return skipPart(part) } + + if (header['content-transfer-encoding']) { encoding = header['content-transfer-encoding'][0].toLowerCase() } else { encoding = '7bit' } + + let onData, + onEnd + + if (isPartAFile(fieldname, contype, filename)) { + // file/binary field + if (nfiles === filesLimit) { + if (!boy.hitFilesLimit) { + boy.hitFilesLimit = true + boy.emit('filesLimit') + } + return skipPart(part) + } + + ++nfiles + + if (!boy._events.file) { + self.parser._ignore() + return + } + + ++nends + const file = new FileStream(fileOpts) + curFile = file + file.on('end', function () { + --nends + self._pause = false + checkFinished() + if (self._cb && !self._needDrain) { + const cb = self._cb + self._cb = undefined + cb() + } + }) + file._read = function (n) { + if (!self._pause) { return } + self._pause = false + if (self._cb && !self._needDrain) { + const cb = self._cb + self._cb = undefined + cb() + } + } + boy.emit('file', fieldname, file, filename, encoding, contype) + + onData = function (data) { + if ((nsize += data.length) > fileSizeLimit) { + const extralen = fileSizeLimit - nsize + data.length + if (extralen > 0) { file.push(data.slice(0, extralen)) } + file.truncated = true + file.bytesRead = fileSizeLimit + part.removeAllListeners('data') + file.emit('limit') + return + } else if (!file.push(data)) { self._pause = true } + + file.bytesRead = nsize + } + + onEnd = function () { + curFile = undefined + file.push(null) + } + } else { + // non-file field + if (nfields === fieldsLimit) { + if (!boy.hitFieldsLimit) { + boy.hitFieldsLimit = true + boy.emit('fieldsLimit') + } + return skipPart(part) + } + + ++nfields + ++nends + let buffer = '' + let truncated = false + curField = part + + onData = function (data) { + if ((nsize += data.length) > fieldSizeLimit) { + const extralen = (fieldSizeLimit - (nsize - data.length)) + buffer += data.toString('binary', 0, extralen) + truncated = true + part.removeAllListeners('data') + } else { buffer += data.toString('binary') } + } + + onEnd = function () { + curField = undefined + if (buffer.length) { buffer = decodeText(buffer, 'binary', charset) } + boy.emit('field', fieldname, buffer, false, truncated, encoding, contype) + --nends + checkFinished() + } + } + + /* As of node@2efe4ab761666 (v0.10.29+/v0.11.14+), busboy had become + broken. Streams2/streams3 is a huge black box of confusion, but + somehow overriding the sync state seems to fix things again (and still + seems to work for previous node versions). + */ + part._readableState.sync = false + + part.on('data', onData) + part.on('end', onEnd) + }).on('error', function (err) { + if (curFile) { curFile.emit('error', err) } + }) + }).on('error', function (err) { + boy.emit('error', err) + }).on('finish', function () { + finished = true + checkFinished() + }) +} + +Multipart.prototype.write = function (chunk, cb) { + const r = this.parser.write(chunk) + if (r && !this._pause) { + cb() + } else { + this._needDrain = !r + this._cb = cb + } +} + +Multipart.prototype.end = function () { + const self = this + + if (self.parser.writable) { + self.parser.end() + } else if (!self._boy._done) { + process.nextTick(function () { + self._boy._done = true + self._boy.emit('finish') + }) + } +} + +function skipPart (part) { + part.resume() +} + +function FileStream (opts) { + Readable.call(this, opts) + + this.bytesRead = 0 + + this.truncated = false +} + +inherits(FileStream, Readable) + +FileStream.prototype._read = function (n) {} + +module.exports = Multipart + + +/***/ }), + +/***/ 8306: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + + +const Decoder = __nccwpck_require__(7100) +const decodeText = __nccwpck_require__(4619) +const getLimit = __nccwpck_require__(1467) + +const RE_CHARSET = /^charset$/i + +UrlEncoded.detect = /^application\/x-www-form-urlencoded/i +function UrlEncoded (boy, cfg) { + const limits = cfg.limits + const parsedConType = cfg.parsedConType + this.boy = boy + + this.fieldSizeLimit = getLimit(limits, 'fieldSize', 1 * 1024 * 1024) + this.fieldNameSizeLimit = getLimit(limits, 'fieldNameSize', 100) + this.fieldsLimit = getLimit(limits, 'fields', Infinity) + + let charset + for (var i = 0, len = parsedConType.length; i < len; ++i) { // eslint-disable-line no-var + if (Array.isArray(parsedConType[i]) && + RE_CHARSET.test(parsedConType[i][0])) { + charset = parsedConType[i][1].toLowerCase() + break + } + } + + if (charset === undefined) { charset = cfg.defCharset || 'utf8' } + + this.decoder = new Decoder() + this.charset = charset + this._fields = 0 + this._state = 'key' + this._checkingBytes = true + this._bytesKey = 0 + this._bytesVal = 0 + this._key = '' + this._val = '' + this._keyTrunc = false + this._valTrunc = false + this._hitLimit = false +} + +UrlEncoded.prototype.write = function (data, cb) { + if (this._fields === this.fieldsLimit) { + if (!this.boy.hitFieldsLimit) { + this.boy.hitFieldsLimit = true + this.boy.emit('fieldsLimit') + } + return cb() + } + + let idxeq; let idxamp; let i; let p = 0; const len = data.length + + while (p < len) { + if (this._state === 'key') { + idxeq = idxamp = undefined + for (i = p; i < len; ++i) { + if (!this._checkingBytes) { ++p } + if (data[i] === 0x3D/* = */) { + idxeq = i + break + } else if (data[i] === 0x26/* & */) { + idxamp = i + break + } + if (this._checkingBytes && this._bytesKey === this.fieldNameSizeLimit) { + this._hitLimit = true + break + } else if (this._checkingBytes) { ++this._bytesKey } + } + + if (idxeq !== undefined) { + // key with assignment + if (idxeq > p) { this._key += this.decoder.write(data.toString('binary', p, idxeq)) } + this._state = 'val' + + this._hitLimit = false + this._checkingBytes = true + this._val = '' + this._bytesVal = 0 + this._valTrunc = false + this.decoder.reset() + + p = idxeq + 1 + } else if (idxamp !== undefined) { + // key with no assignment + ++this._fields + let key; const keyTrunc = this._keyTrunc + if (idxamp > p) { key = (this._key += this.decoder.write(data.toString('binary', p, idxamp))) } else { key = this._key } + + this._hitLimit = false + this._checkingBytes = true + this._key = '' + this._bytesKey = 0 + this._keyTrunc = false + this.decoder.reset() + + if (key.length) { + this.boy.emit('field', decodeText(key, 'binary', this.charset), + '', + keyTrunc, + false) + } + + p = idxamp + 1 + if (this._fields === this.fieldsLimit) { return cb() } + } else if (this._hitLimit) { + // we may not have hit the actual limit if there are encoded bytes... + if (i > p) { this._key += this.decoder.write(data.toString('binary', p, i)) } + p = i + if ((this._bytesKey = this._key.length) === this.fieldNameSizeLimit) { + // yep, we actually did hit the limit + this._checkingBytes = false + this._keyTrunc = true + } + } else { + if (p < len) { this._key += this.decoder.write(data.toString('binary', p)) } + p = len + } + } else { + idxamp = undefined + for (i = p; i < len; ++i) { + if (!this._checkingBytes) { ++p } + if (data[i] === 0x26/* & */) { + idxamp = i + break + } + if (this._checkingBytes && this._bytesVal === this.fieldSizeLimit) { + this._hitLimit = true + break + } else if (this._checkingBytes) { ++this._bytesVal } + } + + if (idxamp !== undefined) { + ++this._fields + if (idxamp > p) { this._val += this.decoder.write(data.toString('binary', p, idxamp)) } + this.boy.emit('field', decodeText(this._key, 'binary', this.charset), + decodeText(this._val, 'binary', this.charset), + this._keyTrunc, + this._valTrunc) + this._state = 'key' + + this._hitLimit = false + this._checkingBytes = true + this._key = '' + this._bytesKey = 0 + this._keyTrunc = false + this.decoder.reset() + + p = idxamp + 1 + if (this._fields === this.fieldsLimit) { return cb() } + } else if (this._hitLimit) { + // we may not have hit the actual limit if there are encoded bytes... + if (i > p) { this._val += this.decoder.write(data.toString('binary', p, i)) } + p = i + if ((this._val === '' && this.fieldSizeLimit === 0) || + (this._bytesVal = this._val.length) === this.fieldSizeLimit) { + // yep, we actually did hit the limit + this._checkingBytes = false + this._valTrunc = true + } + } else { + if (p < len) { this._val += this.decoder.write(data.toString('binary', p)) } + p = len + } + } + } + cb() +} + +UrlEncoded.prototype.end = function () { + if (this.boy._done) { return } + + if (this._state === 'key' && this._key.length > 0) { + this.boy.emit('field', decodeText(this._key, 'binary', this.charset), + '', + this._keyTrunc, + false) + } else if (this._state === 'val') { + this.boy.emit('field', decodeText(this._key, 'binary', this.charset), + decodeText(this._val, 'binary', this.charset), + this._keyTrunc, + this._valTrunc) + } + this.boy._done = true + this.boy.emit('finish') +} + +module.exports = UrlEncoded + + +/***/ }), + +/***/ 7100: +/***/ ((module) => { + +"use strict"; + + +const RE_PLUS = /\+/g + +const HEX = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 +] + +function Decoder () { + this.buffer = undefined +} +Decoder.prototype.write = function (str) { + // Replace '+' with ' ' before decoding + str = str.replace(RE_PLUS, ' ') + let res = '' + let i = 0; let p = 0; const len = str.length + for (; i < len; ++i) { + if (this.buffer !== undefined) { + if (!HEX[str.charCodeAt(i)]) { + res += '%' + this.buffer + this.buffer = undefined + --i // retry character + } else { + this.buffer += str[i] + ++p + if (this.buffer.length === 2) { + res += String.fromCharCode(parseInt(this.buffer, 16)) + this.buffer = undefined + } + } + } else if (str[i] === '%') { + if (i > p) { + res += str.substring(p, i) + p = i + } + this.buffer = '' + ++p + } + } + if (p < len && this.buffer === undefined) { res += str.substring(p) } + return res +} +Decoder.prototype.reset = function () { + this.buffer = undefined +} + +module.exports = Decoder + + +/***/ }), + +/***/ 8647: +/***/ ((module) => { + +"use strict"; + + +module.exports = function basename (path) { + if (typeof path !== 'string') { return '' } + for (var i = path.length - 1; i >= 0; --i) { // eslint-disable-line no-var + switch (path.charCodeAt(i)) { + case 0x2F: // '/' + case 0x5C: // '\' + path = path.slice(i + 1) + return (path === '..' || path === '.' ? '' : path) + } + } + return (path === '..' || path === '.' ? '' : path) +} + + +/***/ }), + +/***/ 4619: +/***/ (function(module) { + +"use strict"; + + +// Node has always utf-8 +const utf8Decoder = new TextDecoder('utf-8') +const textDecoders = new Map([ + ['utf-8', utf8Decoder], + ['utf8', utf8Decoder] +]) + +function getDecoder (charset) { + let lc + while (true) { + switch (charset) { + case 'utf-8': + case 'utf8': + return decoders.utf8 + case 'latin1': + case 'ascii': // TODO: Make these a separate, strict decoder? + case 'us-ascii': + case 'iso-8859-1': + case 'iso8859-1': + case 'iso88591': + case 'iso_8859-1': + case 'windows-1252': + case 'iso_8859-1:1987': + case 'cp1252': + case 'x-cp1252': + return decoders.latin1 + case 'utf16le': + case 'utf-16le': + case 'ucs2': + case 'ucs-2': + return decoders.utf16le + case 'base64': + return decoders.base64 + default: + if (lc === undefined) { + lc = true + charset = charset.toLowerCase() + continue + } + return decoders.other.bind(charset) + } + } +} + +const decoders = { + utf8: (data, sourceEncoding) => { + if (data.length === 0) { + return '' + } + if (typeof data === 'string') { + data = Buffer.from(data, sourceEncoding) + } + return data.utf8Slice(0, data.length) + }, + + latin1: (data, sourceEncoding) => { + if (data.length === 0) { + return '' + } + if (typeof data === 'string') { + return data + } + return data.latin1Slice(0, data.length) + }, + + utf16le: (data, sourceEncoding) => { + if (data.length === 0) { + return '' + } + if (typeof data === 'string') { + data = Buffer.from(data, sourceEncoding) + } + return data.ucs2Slice(0, data.length) + }, + + base64: (data, sourceEncoding) => { + if (data.length === 0) { + return '' + } + if (typeof data === 'string') { + data = Buffer.from(data, sourceEncoding) + } + return data.base64Slice(0, data.length) + }, + + other: (data, sourceEncoding) => { + if (data.length === 0) { + return '' + } + if (typeof data === 'string') { + data = Buffer.from(data, sourceEncoding) + } + + if (textDecoders.has(this.toString())) { + try { + return textDecoders.get(this).decode(data) + } catch (e) { } + } + return typeof data === 'string' + ? data + : data.toString() + } +} + +function decodeText (text, sourceEncoding, destEncoding) { + if (text) { + return getDecoder(destEncoding)(text, sourceEncoding) + } + return text +} + +module.exports = decodeText + + +/***/ }), + +/***/ 1467: +/***/ ((module) => { + +"use strict"; + + +module.exports = function getLimit (limits, name, defaultLimit) { + if ( + !limits || + limits[name] === undefined || + limits[name] === null + ) { return defaultLimit } + + if ( + typeof limits[name] !== 'number' || + isNaN(limits[name]) + ) { throw new TypeError('Limit ' + name + ' is not a valid number') } + + return limits[name] +} + + +/***/ }), + +/***/ 1854: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; +/* eslint-disable object-property-newline */ + + +const decodeText = __nccwpck_require__(4619) + +const RE_ENCODED = /%[a-fA-F0-9][a-fA-F0-9]/g + +const EncodedLookup = { + '%00': '\x00', '%01': '\x01', '%02': '\x02', '%03': '\x03', '%04': '\x04', + '%05': '\x05', '%06': '\x06', '%07': '\x07', '%08': '\x08', '%09': '\x09', + '%0a': '\x0a', '%0A': '\x0a', '%0b': '\x0b', '%0B': '\x0b', '%0c': '\x0c', + '%0C': '\x0c', '%0d': '\x0d', '%0D': '\x0d', '%0e': '\x0e', '%0E': '\x0e', + '%0f': '\x0f', '%0F': '\x0f', '%10': '\x10', '%11': '\x11', '%12': '\x12', + '%13': '\x13', '%14': '\x14', '%15': '\x15', '%16': '\x16', '%17': '\x17', + '%18': '\x18', '%19': '\x19', '%1a': '\x1a', '%1A': '\x1a', '%1b': '\x1b', + '%1B': '\x1b', '%1c': '\x1c', '%1C': '\x1c', '%1d': '\x1d', '%1D': '\x1d', + '%1e': '\x1e', '%1E': '\x1e', '%1f': '\x1f', '%1F': '\x1f', '%20': '\x20', + '%21': '\x21', '%22': '\x22', '%23': '\x23', '%24': '\x24', '%25': '\x25', + '%26': '\x26', '%27': '\x27', '%28': '\x28', '%29': '\x29', '%2a': '\x2a', + '%2A': '\x2a', '%2b': '\x2b', '%2B': '\x2b', '%2c': '\x2c', '%2C': '\x2c', + '%2d': '\x2d', '%2D': '\x2d', '%2e': '\x2e', '%2E': '\x2e', '%2f': '\x2f', + '%2F': '\x2f', '%30': '\x30', '%31': '\x31', '%32': '\x32', '%33': '\x33', + '%34': '\x34', '%35': '\x35', '%36': '\x36', '%37': '\x37', '%38': '\x38', + '%39': '\x39', '%3a': '\x3a', '%3A': '\x3a', '%3b': '\x3b', '%3B': '\x3b', + '%3c': '\x3c', '%3C': '\x3c', '%3d': '\x3d', '%3D': '\x3d', '%3e': '\x3e', + '%3E': '\x3e', '%3f': '\x3f', '%3F': '\x3f', '%40': '\x40', '%41': '\x41', + '%42': '\x42', '%43': '\x43', '%44': '\x44', '%45': '\x45', '%46': '\x46', + '%47': '\x47', '%48': '\x48', '%49': '\x49', '%4a': '\x4a', '%4A': '\x4a', + '%4b': '\x4b', '%4B': '\x4b', '%4c': '\x4c', '%4C': '\x4c', '%4d': '\x4d', + '%4D': '\x4d', '%4e': '\x4e', '%4E': '\x4e', '%4f': '\x4f', '%4F': '\x4f', + '%50': '\x50', '%51': '\x51', '%52': '\x52', '%53': '\x53', '%54': '\x54', + '%55': '\x55', '%56': '\x56', '%57': '\x57', '%58': '\x58', '%59': '\x59', + '%5a': '\x5a', '%5A': '\x5a', '%5b': '\x5b', '%5B': '\x5b', '%5c': '\x5c', + '%5C': '\x5c', '%5d': '\x5d', '%5D': '\x5d', '%5e': '\x5e', '%5E': '\x5e', + '%5f': '\x5f', '%5F': '\x5f', '%60': '\x60', '%61': '\x61', '%62': '\x62', + '%63': '\x63', '%64': '\x64', '%65': '\x65', '%66': '\x66', '%67': '\x67', + '%68': '\x68', '%69': '\x69', '%6a': '\x6a', '%6A': '\x6a', '%6b': '\x6b', + '%6B': '\x6b', '%6c': '\x6c', '%6C': '\x6c', '%6d': '\x6d', '%6D': '\x6d', + '%6e': '\x6e', '%6E': '\x6e', '%6f': '\x6f', '%6F': '\x6f', '%70': '\x70', + '%71': '\x71', '%72': '\x72', '%73': '\x73', '%74': '\x74', '%75': '\x75', + '%76': '\x76', '%77': '\x77', '%78': '\x78', '%79': '\x79', '%7a': '\x7a', + '%7A': '\x7a', '%7b': '\x7b', '%7B': '\x7b', '%7c': '\x7c', '%7C': '\x7c', + '%7d': '\x7d', '%7D': '\x7d', '%7e': '\x7e', '%7E': '\x7e', '%7f': '\x7f', + '%7F': '\x7f', '%80': '\x80', '%81': '\x81', '%82': '\x82', '%83': '\x83', + '%84': '\x84', '%85': '\x85', '%86': '\x86', '%87': '\x87', '%88': '\x88', + '%89': '\x89', '%8a': '\x8a', '%8A': '\x8a', '%8b': '\x8b', '%8B': '\x8b', + '%8c': '\x8c', '%8C': '\x8c', '%8d': '\x8d', '%8D': '\x8d', '%8e': '\x8e', + '%8E': '\x8e', '%8f': '\x8f', '%8F': '\x8f', '%90': '\x90', '%91': '\x91', + '%92': '\x92', '%93': '\x93', '%94': '\x94', '%95': '\x95', '%96': '\x96', + '%97': '\x97', '%98': '\x98', '%99': '\x99', '%9a': '\x9a', '%9A': '\x9a', + '%9b': '\x9b', '%9B': '\x9b', '%9c': '\x9c', '%9C': '\x9c', '%9d': '\x9d', + '%9D': '\x9d', '%9e': '\x9e', '%9E': '\x9e', '%9f': '\x9f', '%9F': '\x9f', + '%a0': '\xa0', '%A0': '\xa0', '%a1': '\xa1', '%A1': '\xa1', '%a2': '\xa2', + '%A2': '\xa2', '%a3': '\xa3', '%A3': '\xa3', '%a4': '\xa4', '%A4': '\xa4', + '%a5': '\xa5', '%A5': '\xa5', '%a6': '\xa6', '%A6': '\xa6', '%a7': '\xa7', + '%A7': '\xa7', '%a8': '\xa8', '%A8': '\xa8', '%a9': '\xa9', '%A9': '\xa9', + '%aa': '\xaa', '%Aa': '\xaa', '%aA': '\xaa', '%AA': '\xaa', '%ab': '\xab', + '%Ab': '\xab', '%aB': '\xab', '%AB': '\xab', '%ac': '\xac', '%Ac': '\xac', + '%aC': '\xac', '%AC': '\xac', '%ad': '\xad', '%Ad': '\xad', '%aD': '\xad', + '%AD': '\xad', '%ae': '\xae', '%Ae': '\xae', '%aE': '\xae', '%AE': '\xae', + '%af': '\xaf', '%Af': '\xaf', '%aF': '\xaf', '%AF': '\xaf', '%b0': '\xb0', + '%B0': '\xb0', '%b1': '\xb1', '%B1': '\xb1', '%b2': '\xb2', '%B2': '\xb2', + '%b3': '\xb3', '%B3': '\xb3', '%b4': '\xb4', '%B4': '\xb4', '%b5': '\xb5', + '%B5': '\xb5', '%b6': '\xb6', '%B6': '\xb6', '%b7': '\xb7', '%B7': '\xb7', + '%b8': '\xb8', '%B8': '\xb8', '%b9': '\xb9', '%B9': '\xb9', '%ba': '\xba', + '%Ba': '\xba', '%bA': '\xba', '%BA': '\xba', '%bb': '\xbb', '%Bb': '\xbb', + '%bB': '\xbb', '%BB': '\xbb', '%bc': '\xbc', '%Bc': '\xbc', '%bC': '\xbc', + '%BC': '\xbc', '%bd': '\xbd', '%Bd': '\xbd', '%bD': '\xbd', '%BD': '\xbd', + '%be': '\xbe', '%Be': '\xbe', '%bE': '\xbe', '%BE': '\xbe', '%bf': '\xbf', + '%Bf': '\xbf', '%bF': '\xbf', '%BF': '\xbf', '%c0': '\xc0', '%C0': '\xc0', + '%c1': '\xc1', '%C1': '\xc1', '%c2': '\xc2', '%C2': '\xc2', '%c3': '\xc3', + '%C3': '\xc3', '%c4': '\xc4', '%C4': '\xc4', '%c5': '\xc5', '%C5': '\xc5', + '%c6': '\xc6', '%C6': '\xc6', '%c7': '\xc7', '%C7': '\xc7', '%c8': '\xc8', + '%C8': '\xc8', '%c9': '\xc9', '%C9': '\xc9', '%ca': '\xca', '%Ca': '\xca', + '%cA': '\xca', '%CA': '\xca', '%cb': '\xcb', '%Cb': '\xcb', '%cB': '\xcb', + '%CB': '\xcb', '%cc': '\xcc', '%Cc': '\xcc', '%cC': '\xcc', '%CC': '\xcc', + '%cd': '\xcd', '%Cd': '\xcd', '%cD': '\xcd', '%CD': '\xcd', '%ce': '\xce', + '%Ce': '\xce', '%cE': '\xce', '%CE': '\xce', '%cf': '\xcf', '%Cf': '\xcf', + '%cF': '\xcf', '%CF': '\xcf', '%d0': '\xd0', '%D0': '\xd0', '%d1': '\xd1', + '%D1': '\xd1', '%d2': '\xd2', '%D2': '\xd2', '%d3': '\xd3', '%D3': '\xd3', + '%d4': '\xd4', '%D4': '\xd4', '%d5': '\xd5', '%D5': '\xd5', '%d6': '\xd6', + '%D6': '\xd6', '%d7': '\xd7', '%D7': '\xd7', '%d8': '\xd8', '%D8': '\xd8', + '%d9': '\xd9', '%D9': '\xd9', '%da': '\xda', '%Da': '\xda', '%dA': '\xda', + '%DA': '\xda', '%db': '\xdb', '%Db': '\xdb', '%dB': '\xdb', '%DB': '\xdb', + '%dc': '\xdc', '%Dc': '\xdc', '%dC': '\xdc', '%DC': '\xdc', '%dd': '\xdd', + '%Dd': '\xdd', '%dD': '\xdd', '%DD': '\xdd', '%de': '\xde', '%De': '\xde', + '%dE': '\xde', '%DE': '\xde', '%df': '\xdf', '%Df': '\xdf', '%dF': '\xdf', + '%DF': '\xdf', '%e0': '\xe0', '%E0': '\xe0', '%e1': '\xe1', '%E1': '\xe1', + '%e2': '\xe2', '%E2': '\xe2', '%e3': '\xe3', '%E3': '\xe3', '%e4': '\xe4', + '%E4': '\xe4', '%e5': '\xe5', '%E5': '\xe5', '%e6': '\xe6', '%E6': '\xe6', + '%e7': '\xe7', '%E7': '\xe7', '%e8': '\xe8', '%E8': '\xe8', '%e9': '\xe9', + '%E9': '\xe9', '%ea': '\xea', '%Ea': '\xea', '%eA': '\xea', '%EA': '\xea', + '%eb': '\xeb', '%Eb': '\xeb', '%eB': '\xeb', '%EB': '\xeb', '%ec': '\xec', + '%Ec': '\xec', '%eC': '\xec', '%EC': '\xec', '%ed': '\xed', '%Ed': '\xed', + '%eD': '\xed', '%ED': '\xed', '%ee': '\xee', '%Ee': '\xee', '%eE': '\xee', + '%EE': '\xee', '%ef': '\xef', '%Ef': '\xef', '%eF': '\xef', '%EF': '\xef', + '%f0': '\xf0', '%F0': '\xf0', '%f1': '\xf1', '%F1': '\xf1', '%f2': '\xf2', + '%F2': '\xf2', '%f3': '\xf3', '%F3': '\xf3', '%f4': '\xf4', '%F4': '\xf4', + '%f5': '\xf5', '%F5': '\xf5', '%f6': '\xf6', '%F6': '\xf6', '%f7': '\xf7', + '%F7': '\xf7', '%f8': '\xf8', '%F8': '\xf8', '%f9': '\xf9', '%F9': '\xf9', + '%fa': '\xfa', '%Fa': '\xfa', '%fA': '\xfa', '%FA': '\xfa', '%fb': '\xfb', + '%Fb': '\xfb', '%fB': '\xfb', '%FB': '\xfb', '%fc': '\xfc', '%Fc': '\xfc', + '%fC': '\xfc', '%FC': '\xfc', '%fd': '\xfd', '%Fd': '\xfd', '%fD': '\xfd', + '%FD': '\xfd', '%fe': '\xfe', '%Fe': '\xfe', '%fE': '\xfe', '%FE': '\xfe', + '%ff': '\xff', '%Ff': '\xff', '%fF': '\xff', '%FF': '\xff' +} + +function encodedReplacer (match) { + return EncodedLookup[match] +} + +const STATE_KEY = 0 +const STATE_VALUE = 1 +const STATE_CHARSET = 2 +const STATE_LANG = 3 + +function parseParams (str) { + const res = [] + let state = STATE_KEY + let charset = '' + let inquote = false + let escaping = false + let p = 0 + let tmp = '' + const len = str.length + + for (var i = 0; i < len; ++i) { // eslint-disable-line no-var + const char = str[i] + if (char === '\\' && inquote) { + if (escaping) { escaping = false } else { + escaping = true + continue + } + } else if (char === '"') { + if (!escaping) { + if (inquote) { + inquote = false + state = STATE_KEY + } else { inquote = true } + continue + } else { escaping = false } + } else { + if (escaping && inquote) { tmp += '\\' } + escaping = false + if ((state === STATE_CHARSET || state === STATE_LANG) && char === "'") { + if (state === STATE_CHARSET) { + state = STATE_LANG + charset = tmp.substring(1) + } else { state = STATE_VALUE } + tmp = '' + continue + } else if (state === STATE_KEY && + (char === '*' || char === '=') && + res.length) { + state = char === '*' + ? STATE_CHARSET + : STATE_VALUE + res[p] = [tmp, undefined] + tmp = '' + continue + } else if (!inquote && char === ';') { + state = STATE_KEY + if (charset) { + if (tmp.length) { + tmp = decodeText(tmp.replace(RE_ENCODED, encodedReplacer), + 'binary', + charset) + } + charset = '' + } else if (tmp.length) { + tmp = decodeText(tmp, 'binary', 'utf8') + } + if (res[p] === undefined) { res[p] = tmp } else { res[p][1] = tmp } + tmp = '' + ++p + continue + } else if (!inquote && (char === ' ' || char === '\t')) { continue } + } + tmp += char + } + if (charset && tmp.length) { + tmp = decodeText(tmp.replace(RE_ENCODED, encodedReplacer), + 'binary', + charset) + } else if (tmp) { + tmp = decodeText(tmp, 'binary', 'utf8') + } + + if (res[p] === undefined) { + if (tmp) { res[p] = tmp } + } else { res[p][1] = tmp } + + return res +} + +module.exports = parseParams + + +/***/ }), + +/***/ 3765: +/***/ ((module) => { + +"use strict"; +module.exports = JSON.parse('{"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/3gpphal+json":{"source":"iana","compressible":true},"application/3gpphalforms+json":{"source":"iana","compressible":true},"application/a2l":{"source":"iana"},"application/ace+cbor":{"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/at+jwt":{"source":"iana"},"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/captive+json":{"source":"iana","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/city+json":{"source":"iana","compressible":true},"application/clr":{"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,"extensions":["cpl"]},"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/dash-patch+xml":{"source":"iana","compressible":true,"extensions":["mpp"]},"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":["es","ecma"]},"application/edi-consent":{"source":"iana"},"application/edi-x12":{"source":"iana","compressible":false},"application/edifact":{"source":"iana","compressible":false},"application/efi":{"source":"iana"},"application/elm+json":{"source":"iana","charset":"UTF-8","compressible":true},"application/elm+xml":{"source":"iana","compressible":true},"application/emergencycalldata.cap+xml":{"source":"iana","charset":"UTF-8","compressible":true},"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/express":{"source":"iana","extensions":["exp"]},"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/jscalendar+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":{"source":"iana","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,"extensions":["mpf"]},"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/missing-blocks+cbor-seq":{"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},"application/mrb-publish+xml":{"source":"iana","compressible":true},"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/oauth-authz-req+jwt":{"source":"iana"},"application/oblivious-dns-message":{"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/opc-nodeset+xml":{"source":"iana","compressible":true},"application/oscore":{"source":"iana"},"application/oxps":{"source":"iana","extensions":["oxps"]},"application/p21":{"source":"iana"},"application/p21+zip":{"source":"iana","compressible":false},"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","extensions":["asc"]},"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.cyn":{"source":"iana","charset":"7-BIT"},"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/sarif+json":{"source":"iana","compressible":true},"application/sarif-external-properties+json":{"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/spdx+json":{"source":"iana","compressible":true},"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/token-introspection+jwt":{"source":"iana"},"application/toml":{"compressible":true,"extensions":["toml"]},"application/trickle-ice-sdpfrag":{"source":"iana"},"application/trig":{"source":"iana","extensions":["trig"]},"application/ttml+xml":{"source":"iana","compressible":true,"extensions":["ttml"]},"application/tve-trigger":{"source":"iana"},"application/tzif":{"source":"iana"},"application/tzif-leap":{"source":"iana"},"application/ubjson":{"compressible":false,"extensions":["ubj"]},"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,"extensions":["td"]},"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.5gnas":{"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.gtpc":{"source":"iana"},"application/vnd.3gpp.interworking-data":{"source":"iana"},"application/vnd.3gpp.lpp":{"source":"iana"},"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.ngap":{"source":"iana"},"application/vnd.3gpp.pfcp":{"source":"iana"},"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.s1ap":{"source":"iana"},"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.cmoca-cmresource":{"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-cmtable":{"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.age":{"source":"iana","extensions":["age"]},"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.arrow.file":{"source":"iana"},"application/vnd.apache.arrow.stream":{"source":"iana"},"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":["key"]},"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.cryptomator.encrypted":{"source":"iana"},"application/vnd.cryptomator.vault":{"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.cyclonedx+json":{"source":"iana","compressible":true},"application/vnd.cyclonedx+xml":{"source":"iana","compressible":true},"application/vnd.d2l.coursepackage1p0+zip":{"source":"iana","compressible":false},"application/vnd.d3m-dataset":{"source":"iana"},"application/vnd.d3m-problem":{"source":"iana"},"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","extensions":["dbf"]},"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.eclipse.ditto+json":{"source":"iana","compressible":true},"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.eu.kasparian.car+json":{"source":"iana","compressible":true},"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.familysearch.gedcom+zip":{"source":"iana","compressible":false},"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.fujifilm.fb.docuworks":{"source":"iana"},"application/vnd.fujifilm.fb.docuworks.binder":{"source":"iana"},"application/vnd.fujifilm.fb.docuworks.container":{"source":"iana"},"application/vnd.fujifilm.fb.jfi+xml":{"source":"iana","compressible":true},"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.slides":{"source":"iana"},"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.hl7cda+xml":{"source":"iana","charset":"UTF-8","compressible":true},"application/vnd.hl7v2+xml":{"source":"iana","charset":"UTF-8","compressible":true},"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","extensions":["mvt"]},"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.maxar.archive.3tz+zip":{"source":"iana","compressible":false},"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.nacamar.ybrid+json":{"source":"iana","compressible":true},"application/vnd.ncd.control":{"source":"iana"},"application/vnd.ncd.reference":{"source":"iana"},"application/vnd.nearst.inv+json":{"source":"iana","compressible":true},"application/vnd.nebumind.line":{"source":"iana"},"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+cbor":{"source":"iana"},"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.opentimestamps.ots":{"source":"iana"},"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","extensions":["rar"]},"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.resilient.logic":{"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.seis+json":{"source":"iana","compressible":true},"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.sycle+xml":{"source":"iana","compressible":true},"application/vnd.syft+json":{"source":"iana","compressible":true},"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.veritone.aion+json":{"source":"iana","compressible":true},"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.dpp":{"source":"iana"},"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":{"source":"iana","compressible":true,"extensions":["wasm"]},"application/watcherinfo+xml":{"source":"iana","compressible":true,"extensions":["wif"]},"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-iwork-keynote-sffkey":{"extensions":["key"]},"application/x-iwork-numbers-sffnumbers":{"extensions":["numbers"]},"application/x-iwork-pages-sffpages":{"extensions":["pages"]},"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},"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":["xsl","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","extensions":["amr"]},"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","opus"]},"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/scip":{"source":"iana"},"audio/silk":{"source":"apache","extensions":["sil"]},"audio/smv":{"source":"iana"},"audio/smv-qcp":{"source":"iana"},"audio/smv0":{"source":"iana"},"audio/sofa":{"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/tsvcis":{"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","extensions":["avci"]},"image/avcs":{"source":"iana","extensions":["avcs"]},"image/avif":{"source":"iana","compressible":false,"extensions":["avif"]},"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/ktx2":{"source":"iana","extensions":["ktx2"]},"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","compressible":true,"extensions":["ico"]},"image/vnd.mix":{"source":"iana"},"image/vnd.mozilla.apng":{"source":"iana"},"image/vnd.ms-dds":{"compressible":true,"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.pco.b16":{"source":"iana","extensions":["b16"]},"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/e57":{"source":"iana"},"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/step":{"source":"iana"},"model/step+xml":{"source":"iana","compressible":true,"extensions":["stpx"]},"model/step+zip":{"source":"iana","compressible":false,"extensions":["stpz"]},"model/step-xml+zip":{"source":"iana","compressible":false,"extensions":["stpxz"]},"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.pytha.pyox":{"source":"iana"},"model/vnd.rosette.annotated-data-model":{"source":"iana"},"model/vnd.sap.vds":{"source":"iana","extensions":["vds"]},"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/cql":{"source":"iana"},"text/cql-expression":{"source":"iana"},"text/cql-identifier":{"source":"iana"},"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/fhirpath":{"source":"iana"},"text/flexfec":{"source":"iana"},"text/fwdred":{"source":"iana"},"text/gff3":{"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/shaclc":{"source":"iana"},"text/shex":{"source":"iana","extensions":["shex"]},"text/slim":{"extensions":["slim","slm"]},"text/spdx":{"source":"iana","extensions":["spdx"]},"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.familysearch.gedcom":{"source":"iana","extensions":["ged"]},"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.hans":{"source":"iana"},"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":{"compressible":true,"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/av1":{"source":"iana"},"video/bmpeg":{"source":"iana"},"video/bt656":{"source":"iana"},"video/celb":{"source":"iana"},"video/dv":{"source":"iana"},"video/encaprtp":{"source":"iana"},"video/ffv1":{"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","extensions":["m4s"]},"video/jpeg":{"source":"iana","extensions":["jpgv"]},"video/jpeg2000":{"source":"iana"},"video/jpm":{"source":"apache","extensions":["jpm","jpgm"]},"video/jxsv":{"source":"iana"},"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/scip":{"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/vp9":{"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}}'); + +/***/ }) + +/******/ }); +/************************************************************************/ +/******/ // The module cache +/******/ var __webpack_module_cache__ = {}; +/******/ +/******/ // The require function +/******/ function __nccwpck_require__(moduleId) { +/******/ // Check if module is in cache +/******/ var cachedModule = __webpack_module_cache__[moduleId]; +/******/ if (cachedModule !== undefined) { +/******/ return cachedModule.exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = __webpack_module_cache__[moduleId] = { +/******/ // no module.id needed +/******/ // no module.loaded needed +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ var threw = true; +/******/ try { +/******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __nccwpck_require__); +/******/ threw = false; +/******/ } finally { +/******/ if(threw) delete __webpack_module_cache__[moduleId]; +/******/ } +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/************************************************************************/ +/******/ /* webpack/runtime/compat */ +/******/ +/******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = __dirname + "/"; +/******/ +/************************************************************************/ +var __webpack_exports__ = {}; +// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk. +(() => { +/*! + * 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 = __nccwpck_require__(2186); +const formData = __nccwpck_require__(4334); +const Mailgun = __nccwpck_require__(4274); + +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(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; + }) + .catch((err) => { + core.setFailed(err.message); + }); +} + +function isNonEmptyString(value) { + return typeof value === 'string' && value !== ''; +} + +const config = loadConfig(); +validate(config); +sendEmail(config); + +})(); + +module.exports = __webpack_exports__; +/******/ })() +; \ 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..38be9f04fc --- /dev/null +++ b/.github/actions/send-email/index.js @@ -0,0 +1,82 @@ +/*! + * 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(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; + }) + .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..7c530a0d4f --- /dev/null +++ b/.github/actions/send-email/package-lock.json @@ -0,0 +1,195 @@ +{ + "name": "send-email", + "version": "1.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "send-email", + "version": "1.0.1", + "license": "Apache-2.0", + "dependencies": { + "@actions/core": "^1.10.1", + "mailgun.js": "^10.1.0" + }, + "devDependencies": { + "@vercel/ncc": "^0.38.1" + } + }, + "node_modules/@actions/core": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.1.tgz", + "integrity": "sha512-3lBR9EDAY+iYIpTnTIXmWcNbX3T2kCkAEQGIQx4NVQ0575nk2k3GRZDTPQG+vVtS2izSLmINlxXf0uLtnrTP+g==", + "dependencies": { + "@actions/http-client": "^2.0.1", + "uuid": "^8.3.2" + } + }, + "node_modules/@actions/http-client": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.0.tgz", + "integrity": "sha512-q+epW0trjVUUHboliPb4UF9g2msf+w61b32tAkFEwL/IwP0DQWgbCMM0Hbe3e3WXSKz5VcUXbzJQgy8Hkra/Lg==", + "dependencies": { + "tunnel": "^0.0.6", + "undici": "^5.25.4" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz", + "integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@vercel/ncc": { + "version": "0.38.1", + "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.38.1.tgz", + "integrity": "sha512-IBBb+iI2NLu4VQn3Vwldyi2QwaXt5+hTyh58ggAMoCGE6DJmPvwL3KPBWcJl1m9LYPChBLE980Jw+CS4Wokqxw==", + "dev": true, + "bin": { + "ncc": "dist/ncc/cli.js" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", + "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", + "dependencies": { + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/base-64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz", + "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/mailgun.js": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/mailgun.js/-/mailgun.js-10.1.0.tgz", + "integrity": "sha512-j2yP5HGT14xwobySPA+y5IZtKW+XxavNySmFt8A/ztXTCmmgd2OEJMX293femA579iXyphjfymXyTQ/DCP9aAg==", + "dependencies": { + "axios": "^1.6.0", + "base-64": "^1.0.0", + "url-join": "^4.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + } + } +} diff --git a/.github/actions/send-email/package.json b/.github/actions/send-email/package.json new file mode 100644 index 0000000000..69ee369e50 --- /dev/null +++ b/.github/actions/send-email/package.json @@ -0,0 +1,23 @@ +{ + "name": "send-email", + "version": "1.0.1", + "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.10.1", + "mailgun.js": "^10.1.0" + }, + "devDependencies": { + "@vercel/ncc": "^0.38.1" + } +} diff --git a/.github/actions/send-tweet/README.md b/.github/actions/send-tweet/README.md new file mode 100644 index 0000000000..eda484f608 --- /dev/null +++ b/.github/actions/send-tweet/README.md @@ -0,0 +1,49 @@ +# Send Tweet GitHub Action + +This is a minimalistic GitHub Action for posting Firebase release announcements +to Twitter. Simply specify the Twitter API keys along with the Tweet status to +be posted. + +## Inputs + +### `status` + +**Required** Text of the Tweet to send. + +### `consumer-key` + +**Required** Consumer API key from Twitter. + +### `consumer-secret` + +**Required** Consumer API secret key from Twitter. + +### `access-token` + +**Required** Twitter application access token. + +### `access-token-secret` + +**Required** Twitter application access token secret. + +## Example usage + +``` +- name: Send Tweet + uses: firebase/firebase-admin-node/.github/actions/send-tweet + with: + status: > + v1.2.3 of @Firebase Admin Node.js SDK is available. + Release notes at https://firebase.google.com. + consumer-key: ${{ secrets.TWITTER_CONSUMER_KEY }} + consumer-secret: ${{ secrets.TWITTER_CONSUMER_SECRET }} + access-token: ${{ secrets.TWITTER_ACCESS_TOKEN }} + access-token-secret: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} +``` + +## Implementation + +This Action uses the `twitter` NPM package to send Tweets. + +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-tweet/action.yml b/.github/actions/send-tweet/action.yml new file mode 100644 index 0000000000..bb45748d4b --- /dev/null +++ b/.github/actions/send-tweet/action.yml @@ -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. + +name: 'Send Tweet Action' +description: 'Send Tweets from GitHub Actions workflows.' +inputs: + status: + description: Status (Tweet) to be posted + required: true + consumer-key: + description: Consumer API key. + required: true + consumer-secret: + description: Consumer API secret key. + required: true + access-token: + description: Application access token. + required: true + access-token-secret: + description: Application access token secret. + required: true +runs: + using: 'node12' + main: 'dist/index.js' diff --git a/.github/actions/send-tweet/dist/index.js b/.github/actions/send-tweet/dist/index.js new file mode 100644 index 0000000000..e3bed9903a --- /dev/null +++ b/.github/actions/send-tweet/dist/index.js @@ -0,0 +1,33631 @@ +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(); +/******/ }) +/************************************************************************/ +/******/ ({ + +/***/ 13: +/***/ (function(module) { + +"use strict"; + + +var replace = String.prototype.replace; +var percentTwenties = /%20/g; + +module.exports = { + 'default': 'RFC3986', + formatters: { + RFC1738: function (value) { + return replace.call(value, percentTwenties, '+'); + }, + RFC3986: function (value) { + return value; + } + }, + RFC1738: 'RFC1738', + RFC3986: 'RFC3986' +}; + + +/***/ }), + +/***/ 16: +/***/ (function(module) { + +module.exports = require("tls"); + +/***/ }), + +/***/ 28: +/***/ (function(module) { + +"use strict"; + +module.exports = function generate_comment(it, $keyword, $ruleType) { + var out = ' '; + var $schema = it.schema[$keyword]; + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $comment = it.util.toQuotedString($schema); + if (it.opts.$comment === true) { + out += ' console.log(' + ($comment) + ');'; + } else if (typeof it.opts.$comment == 'function') { + out += ' self._opts.$comment(' + ($comment) + ', ' + (it.util.toQuotedString($errSchemaPath)) + ', validate.root.schema);'; + } + return out; +} + + +/***/ }), + +/***/ 35: +/***/ (function(module) { + +"use strict"; + +module.exports = function generate_propertyNames(it, $keyword, $ruleType) { + var out = ' '; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $data = 'data' + ($dataLvl || ''); + var $errs = 'errs__' + $lvl; + var $it = it.util.copy(it); + var $closingBraces = ''; + $it.level++; + var $nextValid = 'valid' + $it.level; + out += 'var ' + ($errs) + ' = errors;'; + if ((it.opts.strictKeywords ? typeof $schema == 'object' && Object.keys($schema).length > 0 : it.util.schemaHasRules($schema, it.RULES.all))) { + $it.schema = $schema; + $it.schemaPath = $schemaPath; + $it.errSchemaPath = $errSchemaPath; + var $key = 'key' + $lvl, + $idx = 'idx' + $lvl, + $i = 'i' + $lvl, + $invalidName = '\' + ' + $key + ' + \'', + $dataNxt = $it.dataLevel = it.dataLevel + 1, + $nextData = 'data' + $dataNxt, + $dataProperties = 'dataProperties' + $lvl, + $ownProperties = it.opts.ownProperties, + $currentBaseId = it.baseId; + if ($ownProperties) { + out += ' var ' + ($dataProperties) + ' = undefined; '; + } + if ($ownProperties) { + out += ' ' + ($dataProperties) + ' = ' + ($dataProperties) + ' || Object.keys(' + ($data) + '); for (var ' + ($idx) + '=0; ' + ($idx) + '<' + ($dataProperties) + '.length; ' + ($idx) + '++) { var ' + ($key) + ' = ' + ($dataProperties) + '[' + ($idx) + ']; '; + } else { + out += ' for (var ' + ($key) + ' in ' + ($data) + ') { '; + } + out += ' var startErrs' + ($lvl) + ' = errors; '; + var $passData = $key; + var $wasComposite = it.compositeRule; + it.compositeRule = $it.compositeRule = true; + var $code = it.validate($it); + $it.baseId = $currentBaseId; + if (it.util.varOccurences($code, $nextData) < 2) { + out += ' ' + (it.util.varReplace($code, $nextData, $passData)) + ' '; + } else { + out += ' var ' + ($nextData) + ' = ' + ($passData) + '; ' + ($code) + ' '; + } + it.compositeRule = $it.compositeRule = $wasComposite; + out += ' if (!' + ($nextValid) + ') { for (var ' + ($i) + '=startErrs' + ($lvl) + '; ' + ($i) + ' 299) { + return reject(new Error('HTTP Error: ' + response.statusCode + ' ' + response.statusMessage)); + } + + // no errors + resolve(data); + }); + }); + } + + // Callback version + this.request(options, function(error, response, data) { + // request error + if (error) { + return callback(error, data, response); + } + + // JSON parse error or empty strings + try { + // An empty string is a valid response + if (data === '') { + data = {}; + } + else { + data = JSON.parse(data); + } + } + catch(parseError) { + return callback( + new Error('JSON parseError with HTTP Status: ' + response.statusCode + ' ' + response.statusMessage), + data, + response + ); + } + + + // response object errors + // This should return an error object not an array of errors + if (data.errors !== undefined) { + return callback(data.errors, data, response); + } + + // status code errors + if(response.statusCode < 200 || response.statusCode > 299) { + return callback( + new Error('HTTP Error: ' + response.statusCode + ' ' + response.statusMessage), + data, + response + ); + } + // no errors + callback(null, data, response); + }); + +}; + +/** + * GET + */ +Twitter.prototype.get = function(url, params, callback) { + return this.__request('get', url, params, callback); +}; + +/** + * POST + */ +Twitter.prototype.post = function(url, params, callback) { + return this.__request('post', url, params, callback); +}; + +/** + * STREAM + */ +Twitter.prototype.stream = function(method, params, callback) { + if (typeof params === 'function') { + callback = params; + params = {}; + } + + var base = 'stream'; + + if (method === 'user' || method === 'site') { + base = method + '_' + base; + } + + var url = this.__buildEndpoint(method, base); + var request = this.request({url: url, qs: params}); + var stream = new Streamparser(); + + stream.destroy = function() { + // FIXME: should we emit end/close on explicit destroy? + if ( typeof request.abort === 'function' ) { + request.abort(); // node v0.4.0 + } + else { + request.socket.destroy(); + } + }; + + request.on('response', function(response) { + if(response.statusCode !== 200) { + stream.emit('error', new Error('Status Code: ' + response.statusCode)); + } + else { + stream.emit('response', response); + } + + response.on('data', function(chunk) { + stream.receive(chunk); + }); + + response.on('error', function(error) { + stream.emit('error', error); + }); + + response.on('end', function() { + stream.emit('end', response); + }); + }); + + request.on('error', function(error) { + stream.emit('error', error); + }); + request.end(); + + if (typeof callback === 'function') { + callback(stream); + } + else { + return stream; + } +}; + + +module.exports = Twitter; + + +/***/ }), + +/***/ 54: +/***/ (function(__unusedmodule, exports) { + +"use strict"; +/*! + * Copyright (c) 2015, Salesforce.com, Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of Salesforce.com nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * "A request-path path-matches a given cookie-path if at least one of the + * following conditions holds:" + */ +function pathMatch (reqPath, cookiePath) { + // "o The cookie-path and the request-path are identical." + if (cookiePath === reqPath) { + return true; + } + + var idx = reqPath.indexOf(cookiePath); + if (idx === 0) { + // "o The cookie-path is a prefix of the request-path, and the last + // character of the cookie-path is %x2F ("/")." + if (cookiePath.substr(-1) === "/") { + return true; + } + + // " o The cookie-path is a prefix of the request-path, and the first + // character of the request-path that is not included in the cookie- path + // is a %x2F ("/") character." + if (reqPath.substr(cookiePath.length, 1) === "/") { + return true; + } + } + + return false; +} + +exports.pathMatch = pathMatch; + + +/***/ }), + +/***/ 62: +/***/ (function(module, __unusedexports, __webpack_require__) { + +// Copyright 2011 Mark Cavage All rights reserved. + +// If you have no idea what ASN.1 or BER is, see this: +// ftp://ftp.rsa.com/pub/pkcs/ascii/layman.asc + +var Ber = __webpack_require__(249); + + + +// --- Exported API + +module.exports = { + + Ber: Ber, + + BerReader: Ber.Reader, + + BerWriter: Ber.Writer + +}; + + +/***/ }), + +/***/ 64: +/***/ (function(module, __unusedexports, __webpack_require__) { + +// Copyright 2012 Joyent, Inc. All rights reserved. + +var assert = __webpack_require__(477); +var crypto = __webpack_require__(417); +var http = __webpack_require__(605); +var util = __webpack_require__(669); +var sshpk = __webpack_require__(650); +var jsprim = __webpack_require__(348); +var utils = __webpack_require__(909); + +var sprintf = __webpack_require__(669).format; + +var HASH_ALGOS = utils.HASH_ALGOS; +var PK_ALGOS = utils.PK_ALGOS; +var InvalidAlgorithmError = utils.InvalidAlgorithmError; +var HttpSignatureError = utils.HttpSignatureError; +var validateAlgorithm = utils.validateAlgorithm; + +///--- Globals + +var AUTHZ_FMT = + 'Signature keyId="%s",algorithm="%s",headers="%s",signature="%s"'; + +///--- Specific Errors + +function MissingHeaderError(message) { + HttpSignatureError.call(this, message, MissingHeaderError); +} +util.inherits(MissingHeaderError, HttpSignatureError); + +function StrictParsingError(message) { + HttpSignatureError.call(this, message, StrictParsingError); +} +util.inherits(StrictParsingError, HttpSignatureError); + +/* See createSigner() */ +function RequestSigner(options) { + assert.object(options, 'options'); + + var alg = []; + if (options.algorithm !== undefined) { + assert.string(options.algorithm, 'options.algorithm'); + alg = validateAlgorithm(options.algorithm); + } + this.rs_alg = alg; + + /* + * RequestSigners come in two varieties: ones with an rs_signFunc, and ones + * with an rs_signer. + * + * rs_signFunc-based RequestSigners have to build up their entire signing + * string within the rs_lines array and give it to rs_signFunc as a single + * concat'd blob. rs_signer-based RequestSigners can add a line at a time to + * their signing state by using rs_signer.update(), thus only needing to + * buffer the hash function state and one line at a time. + */ + if (options.sign !== undefined) { + assert.func(options.sign, 'options.sign'); + this.rs_signFunc = options.sign; + + } else if (alg[0] === 'hmac' && options.key !== undefined) { + assert.string(options.keyId, 'options.keyId'); + this.rs_keyId = options.keyId; + + if (typeof (options.key) !== 'string' && !Buffer.isBuffer(options.key)) + throw (new TypeError('options.key for HMAC must be a string or Buffer')); + + /* + * Make an rs_signer for HMACs, not a rs_signFunc -- HMACs digest their + * data in chunks rather than requiring it all to be given in one go + * at the end, so they are more similar to signers than signFuncs. + */ + this.rs_signer = crypto.createHmac(alg[1].toUpperCase(), options.key); + this.rs_signer.sign = function () { + var digest = this.digest('base64'); + return ({ + hashAlgorithm: alg[1], + toString: function () { return (digest); } + }); + }; + + } else if (options.key !== undefined) { + var key = options.key; + if (typeof (key) === 'string' || Buffer.isBuffer(key)) + key = sshpk.parsePrivateKey(key); + + assert.ok(sshpk.PrivateKey.isPrivateKey(key, [1, 2]), + 'options.key must be a sshpk.PrivateKey'); + this.rs_key = key; + + assert.string(options.keyId, 'options.keyId'); + this.rs_keyId = options.keyId; + + if (!PK_ALGOS[key.type]) { + throw (new InvalidAlgorithmError(key.type.toUpperCase() + ' type ' + + 'keys are not supported')); + } + + if (alg[0] !== undefined && key.type !== alg[0]) { + throw (new InvalidAlgorithmError('options.key must be a ' + + alg[0].toUpperCase() + ' key, was given a ' + + key.type.toUpperCase() + ' key instead')); + } + + this.rs_signer = key.createSign(alg[1]); + + } else { + throw (new TypeError('options.sign (func) or options.key is required')); + } + + this.rs_headers = []; + this.rs_lines = []; +} + +/** + * Adds a header to be signed, with its value, into this signer. + * + * @param {String} header + * @param {String} value + * @return {String} value written + */ +RequestSigner.prototype.writeHeader = function (header, value) { + assert.string(header, 'header'); + header = header.toLowerCase(); + assert.string(value, 'value'); + + this.rs_headers.push(header); + + if (this.rs_signFunc) { + this.rs_lines.push(header + ': ' + value); + + } else { + var line = header + ': ' + value; + if (this.rs_headers.length > 0) + line = '\n' + line; + this.rs_signer.update(line); + } + + return (value); +}; + +/** + * Adds a default Date header, returning its value. + * + * @return {String} + */ +RequestSigner.prototype.writeDateHeader = function () { + return (this.writeHeader('date', jsprim.rfc1123(new Date()))); +}; + +/** + * Adds the request target line to be signed. + * + * @param {String} method, HTTP method (e.g. 'get', 'post', 'put') + * @param {String} path + */ +RequestSigner.prototype.writeTarget = function (method, path) { + assert.string(method, 'method'); + assert.string(path, 'path'); + method = method.toLowerCase(); + this.writeHeader('(request-target)', method + ' ' + path); +}; + +/** + * Calculate the value for the Authorization header on this request + * asynchronously. + * + * @param {Func} callback (err, authz) + */ +RequestSigner.prototype.sign = function (cb) { + assert.func(cb, 'callback'); + + if (this.rs_headers.length < 1) + throw (new Error('At least one header must be signed')); + + var alg, authz; + if (this.rs_signFunc) { + var data = this.rs_lines.join('\n'); + var self = this; + this.rs_signFunc(data, function (err, sig) { + if (err) { + cb(err); + return; + } + try { + assert.object(sig, 'signature'); + assert.string(sig.keyId, 'signature.keyId'); + assert.string(sig.algorithm, 'signature.algorithm'); + assert.string(sig.signature, 'signature.signature'); + alg = validateAlgorithm(sig.algorithm); + + authz = sprintf(AUTHZ_FMT, + sig.keyId, + sig.algorithm, + self.rs_headers.join(' '), + sig.signature); + } catch (e) { + cb(e); + return; + } + cb(null, authz); + }); + + } else { + try { + var sigObj = this.rs_signer.sign(); + } catch (e) { + cb(e); + return; + } + alg = (this.rs_alg[0] || this.rs_key.type) + '-' + sigObj.hashAlgorithm; + var signature = sigObj.toString(); + authz = sprintf(AUTHZ_FMT, + this.rs_keyId, + alg, + this.rs_headers.join(' '), + signature); + cb(null, authz); + } +}; + +///--- Exported API + +module.exports = { + /** + * Identifies whether a given object is a request signer or not. + * + * @param {Object} object, the object to identify + * @returns {Boolean} + */ + isSigner: function (obj) { + if (typeof (obj) === 'object' && obj instanceof RequestSigner) + return (true); + return (false); + }, + + /** + * Creates a request signer, used to asynchronously build a signature + * for a request (does not have to be an http.ClientRequest). + * + * @param {Object} options, either: + * - {String} keyId + * - {String|Buffer} key + * - {String} algorithm (optional, required for HMAC) + * or: + * - {Func} sign (data, cb) + * @return {RequestSigner} + */ + createSigner: function createSigner(options) { + return (new RequestSigner(options)); + }, + + /** + * Adds an 'Authorization' header to an http.ClientRequest object. + * + * Note that this API will add a Date header if it's not already set. Any + * other headers in the options.headers array MUST be present, or this + * will throw. + * + * You shouldn't need to check the return type; it's just there if you want + * to be pedantic. + * + * The optional flag indicates whether parsing should use strict enforcement + * of the version draft-cavage-http-signatures-04 of the spec or beyond. + * The default is to be loose and support + * older versions for compatibility. + * + * @param {Object} request an instance of http.ClientRequest. + * @param {Object} options signing parameters object: + * - {String} keyId required. + * - {String} key required (either a PEM or HMAC key). + * - {Array} headers optional; defaults to ['date']. + * - {String} algorithm optional (unless key is HMAC); + * default is the same as the sshpk default + * signing algorithm for the type of key given + * - {String} httpVersion optional; defaults to '1.1'. + * - {Boolean} strict optional; defaults to 'false'. + * @return {Boolean} true if Authorization (and optionally Date) were added. + * @throws {TypeError} on bad parameter types (input). + * @throws {InvalidAlgorithmError} if algorithm was bad or incompatible with + * the given key. + * @throws {sshpk.KeyParseError} if key was bad. + * @throws {MissingHeaderError} if a header to be signed was specified but + * was not present. + */ + signRequest: function signRequest(request, options) { + assert.object(request, 'request'); + assert.object(options, 'options'); + assert.optionalString(options.algorithm, 'options.algorithm'); + assert.string(options.keyId, 'options.keyId'); + assert.optionalArrayOfString(options.headers, 'options.headers'); + assert.optionalString(options.httpVersion, 'options.httpVersion'); + + if (!request.getHeader('Date')) + request.setHeader('Date', jsprim.rfc1123(new Date())); + if (!options.headers) + options.headers = ['date']; + if (!options.httpVersion) + options.httpVersion = '1.1'; + + var alg = []; + if (options.algorithm) { + options.algorithm = options.algorithm.toLowerCase(); + alg = validateAlgorithm(options.algorithm); + } + + var i; + var stringToSign = ''; + for (i = 0; i < options.headers.length; i++) { + if (typeof (options.headers[i]) !== 'string') + throw new TypeError('options.headers must be an array of Strings'); + + var h = options.headers[i].toLowerCase(); + + if (h === 'request-line') { + if (!options.strict) { + /** + * We allow headers from the older spec drafts if strict parsing isn't + * specified in options. + */ + stringToSign += + request.method + ' ' + request.path + ' HTTP/' + + options.httpVersion; + } else { + /* Strict parsing doesn't allow older draft headers. */ + throw (new StrictParsingError('request-line is not a valid header ' + + 'with strict parsing enabled.')); + } + } else if (h === '(request-target)') { + stringToSign += + '(request-target): ' + request.method.toLowerCase() + ' ' + + request.path; + } else { + var value = request.getHeader(h); + if (value === undefined || value === '') { + throw new MissingHeaderError(h + ' was not in the request'); + } + stringToSign += h + ': ' + value; + } + + if ((i + 1) < options.headers.length) + stringToSign += '\n'; + } + + /* This is just for unit tests. */ + if (request.hasOwnProperty('_stringToSign')) { + request._stringToSign = stringToSign; + } + + var signature; + if (alg[0] === 'hmac') { + if (typeof (options.key) !== 'string' && !Buffer.isBuffer(options.key)) + throw (new TypeError('options.key must be a string or Buffer')); + + var hmac = crypto.createHmac(alg[1].toUpperCase(), options.key); + hmac.update(stringToSign); + signature = hmac.digest('base64'); + + } else { + var key = options.key; + if (typeof (key) === 'string' || Buffer.isBuffer(key)) + key = sshpk.parsePrivateKey(options.key); + + assert.ok(sshpk.PrivateKey.isPrivateKey(key, [1, 2]), + 'options.key must be a sshpk.PrivateKey'); + + if (!PK_ALGOS[key.type]) { + throw (new InvalidAlgorithmError(key.type.toUpperCase() + ' type ' + + 'keys are not supported')); + } + + if (alg[0] !== undefined && key.type !== alg[0]) { + throw (new InvalidAlgorithmError('options.key must be a ' + + alg[0].toUpperCase() + ' key, was given a ' + + key.type.toUpperCase() + ' key instead')); + } + + var signer = key.createSign(alg[1]); + signer.update(stringToSign); + var sigObj = signer.sign(); + if (!HASH_ALGOS[sigObj.hashAlgorithm]) { + throw (new InvalidAlgorithmError(sigObj.hashAlgorithm.toUpperCase() + + ' is not a supported hash algorithm')); + } + options.algorithm = key.type + '-' + sigObj.hashAlgorithm; + signature = sigObj.toString(); + assert.notStrictEqual(signature, '', 'empty signature produced'); + } + + var authzHeaderName = options.authorizationHeaderName || 'Authorization'; + + request.setHeader(authzHeaderName, sprintf(AUTHZ_FMT, + options.keyId, + options.algorithm, + options.headers.join(' '), + signature)); + + return true; + } + +}; + + +/***/ }), + +/***/ 69: +/***/ (function(module) { + +// populates missing values +module.exports = function(dst, src) { + + Object.keys(src).forEach(function(prop) + { + dst[prop] = dst[prop] || src[prop]; + }); + + return dst; +}; + + +/***/ }), + +/***/ 71: +/***/ (function(module, __unusedexports, __webpack_require__) { + +"use strict"; + + +// glorious streaming json parser, built specifically for the twitter streaming api +// assumptions: +// 1) ninjas are mammals +// 2) tweets come in chunks of text, surrounded by {}'s, separated by line breaks +// 3) only one tweet per chunk +// +// p = new parser.instance() +// p.addListener('object', function...) +// p.receive(data) +// p.receive(data) +// ... + +var EventEmitter = __webpack_require__(614).EventEmitter; + +var Parser = module.exports = function Parser() { + // Make sure we call our parents constructor + EventEmitter.call(this); + this.buffer = ''; + return this; +}; + +// The parser emits events! +Parser.prototype = Object.create(EventEmitter.prototype); + +Parser.END = '\r\n'; +Parser.END_LENGTH = 2; + +Parser.prototype.receive = function receive(buffer) { + this.buffer += buffer.toString('utf8'); + var index, json; + + // We have END? + while ((index = this.buffer.indexOf(Parser.END)) > -1) { + json = this.buffer.slice(0, index); + this.buffer = this.buffer.slice(index + Parser.END_LENGTH); + if (json.length > 0) { + try { + json = JSON.parse(json); + // Event message + if (json.event !== undefined) { + // First emit specific event + this.emit(json.event, json); + // Now emit catch-all event + this.emit('event', json); + } + // Delete message + else if (json.delete !== undefined) { + this.emit('delete', json); + } + // Friends message (beginning of stream) + else if (json.friends !== undefined || json.friends_str !== undefined) { + this.emit('friends', json); + } + // Any other message + else { + this.emit('data', json); + } + } + catch (error) { + error.source = json; + this.emit('error', error); + } + } + else { + // Keep Alive + this.emit('ping'); + } + } +}; + + +/***/ }), + +/***/ 78: +/***/ (function(module, __unusedexports, __webpack_require__) { + +// Copyright 2015 Joyent, Inc. + +module.exports = { + read: read, + readSSHPrivate: readSSHPrivate, + write: write +}; + +var assert = __webpack_require__(477); +var asn1 = __webpack_require__(62); +var Buffer = __webpack_require__(215).Buffer; +var algs = __webpack_require__(98); +var utils = __webpack_require__(270); +var crypto = __webpack_require__(417); + +var Key = __webpack_require__(852); +var PrivateKey = __webpack_require__(502); +var pem = __webpack_require__(268); +var rfc4253 = __webpack_require__(538); +var SSHBuffer = __webpack_require__(940); +var errors = __webpack_require__(753); + +var bcrypt; + +function read(buf, options) { + return (pem.read(buf, options)); +} + +var MAGIC = 'openssh-key-v1'; + +function readSSHPrivate(type, buf, options) { + buf = new SSHBuffer({buffer: buf}); + + var magic = buf.readCString(); + assert.strictEqual(magic, MAGIC, 'bad magic string'); + + var cipher = buf.readString(); + var kdf = buf.readString(); + var kdfOpts = buf.readBuffer(); + + var nkeys = buf.readInt(); + if (nkeys !== 1) { + throw (new Error('OpenSSH-format key file contains ' + + 'multiple keys: this is unsupported.')); + } + + var pubKey = buf.readBuffer(); + + if (type === 'public') { + assert.ok(buf.atEnd(), 'excess bytes left after key'); + return (rfc4253.read(pubKey)); + } + + var privKeyBlob = buf.readBuffer(); + assert.ok(buf.atEnd(), 'excess bytes left after key'); + + var kdfOptsBuf = new SSHBuffer({ buffer: kdfOpts }); + switch (kdf) { + case 'none': + if (cipher !== 'none') { + throw (new Error('OpenSSH-format key uses KDF "none" ' + + 'but specifies a cipher other than "none"')); + } + break; + case 'bcrypt': + var salt = kdfOptsBuf.readBuffer(); + var rounds = kdfOptsBuf.readInt(); + var cinf = utils.opensshCipherInfo(cipher); + if (bcrypt === undefined) { + bcrypt = __webpack_require__(641); + } + + if (typeof (options.passphrase) === 'string') { + options.passphrase = Buffer.from(options.passphrase, + 'utf-8'); + } + if (!Buffer.isBuffer(options.passphrase)) { + throw (new errors.KeyEncryptedError( + options.filename, 'OpenSSH')); + } + + var pass = new Uint8Array(options.passphrase); + var salti = new Uint8Array(salt); + /* Use the pbkdf to derive both the key and the IV. */ + var out = new Uint8Array(cinf.keySize + cinf.blockSize); + var res = bcrypt.pbkdf(pass, pass.length, salti, salti.length, + out, out.length, rounds); + if (res !== 0) { + throw (new Error('bcrypt_pbkdf function returned ' + + 'failure, parameters invalid')); + } + out = Buffer.from(out); + var ckey = out.slice(0, cinf.keySize); + var iv = out.slice(cinf.keySize, cinf.keySize + cinf.blockSize); + var cipherStream = crypto.createDecipheriv(cinf.opensslName, + ckey, iv); + cipherStream.setAutoPadding(false); + var chunk, chunks = []; + cipherStream.once('error', function (e) { + if (e.toString().indexOf('bad decrypt') !== -1) { + throw (new Error('Incorrect passphrase ' + + 'supplied, could not decrypt key')); + } + throw (e); + }); + cipherStream.write(privKeyBlob); + cipherStream.end(); + while ((chunk = cipherStream.read()) !== null) + chunks.push(chunk); + privKeyBlob = Buffer.concat(chunks); + break; + default: + throw (new Error( + 'OpenSSH-format key uses unknown KDF "' + kdf + '"')); + } + + buf = new SSHBuffer({buffer: privKeyBlob}); + + var checkInt1 = buf.readInt(); + var checkInt2 = buf.readInt(); + if (checkInt1 !== checkInt2) { + throw (new Error('Incorrect passphrase supplied, could not ' + + 'decrypt key')); + } + + var ret = {}; + var key = rfc4253.readInternal(ret, 'private', buf.remainder()); + + buf.skip(ret.consumed); + + var comment = buf.readString(); + key.comment = comment; + + return (key); +} + +function write(key, options) { + var pubKey; + if (PrivateKey.isPrivateKey(key)) + pubKey = key.toPublic(); + else + pubKey = key; + + var cipher = 'none'; + var kdf = 'none'; + var kdfopts = Buffer.alloc(0); + var cinf = { blockSize: 8 }; + var passphrase; + if (options !== undefined) { + passphrase = options.passphrase; + if (typeof (passphrase) === 'string') + passphrase = Buffer.from(passphrase, 'utf-8'); + if (passphrase !== undefined) { + assert.buffer(passphrase, 'options.passphrase'); + assert.optionalString(options.cipher, 'options.cipher'); + cipher = options.cipher; + if (cipher === undefined) + cipher = 'aes128-ctr'; + cinf = utils.opensshCipherInfo(cipher); + kdf = 'bcrypt'; + } + } + + var privBuf; + if (PrivateKey.isPrivateKey(key)) { + privBuf = new SSHBuffer({}); + var checkInt = crypto.randomBytes(4).readUInt32BE(0); + privBuf.writeInt(checkInt); + privBuf.writeInt(checkInt); + privBuf.write(key.toBuffer('rfc4253')); + privBuf.writeString(key.comment || ''); + + var n = 1; + while (privBuf._offset % cinf.blockSize !== 0) + privBuf.writeChar(n++); + privBuf = privBuf.toBuffer(); + } + + switch (kdf) { + case 'none': + break; + case 'bcrypt': + var salt = crypto.randomBytes(16); + var rounds = 16; + var kdfssh = new SSHBuffer({}); + kdfssh.writeBuffer(salt); + kdfssh.writeInt(rounds); + kdfopts = kdfssh.toBuffer(); + + if (bcrypt === undefined) { + bcrypt = __webpack_require__(641); + } + var pass = new Uint8Array(passphrase); + var salti = new Uint8Array(salt); + /* Use the pbkdf to derive both the key and the IV. */ + var out = new Uint8Array(cinf.keySize + cinf.blockSize); + var res = bcrypt.pbkdf(pass, pass.length, salti, salti.length, + out, out.length, rounds); + if (res !== 0) { + throw (new Error('bcrypt_pbkdf function returned ' + + 'failure, parameters invalid')); + } + out = Buffer.from(out); + var ckey = out.slice(0, cinf.keySize); + var iv = out.slice(cinf.keySize, cinf.keySize + cinf.blockSize); + + var cipherStream = crypto.createCipheriv(cinf.opensslName, + ckey, iv); + cipherStream.setAutoPadding(false); + var chunk, chunks = []; + cipherStream.once('error', function (e) { + throw (e); + }); + cipherStream.write(privBuf); + cipherStream.end(); + while ((chunk = cipherStream.read()) !== null) + chunks.push(chunk); + privBuf = Buffer.concat(chunks); + break; + default: + throw (new Error('Unsupported kdf ' + kdf)); + } + + var buf = new SSHBuffer({}); + + buf.writeCString(MAGIC); + buf.writeString(cipher); /* cipher */ + buf.writeString(kdf); /* kdf */ + buf.writeBuffer(kdfopts); /* kdfoptions */ + + buf.writeInt(1); /* nkeys */ + buf.writeBuffer(pubKey.toBuffer('rfc4253')); + + if (privBuf) + buf.writeBuffer(privBuf); + + buf = buf.toBuffer(); + + var header; + if (PrivateKey.isPrivateKey(key)) + header = 'OPENSSH PRIVATE KEY'; + else + header = 'OPENSSH PUBLIC KEY'; + + var tmp = buf.toString('base64'); + var len = tmp.length + (tmp.length / 70) + + 18 + 16 + header.length*2 + 10; + buf = Buffer.alloc(len); + var o = 0; + o += buf.write('-----BEGIN ' + header + '-----\n', o); + for (var i = 0; i < tmp.length; ) { + var limit = i + 70; + if (limit > tmp.length) + limit = tmp.length; + o += buf.write(tmp.slice(i, limit), o); + buf[o++] = 10; + i = limit; + } + o += buf.write('-----END ' + header + '-----\n', o); + + return (buf.slice(0, o)); +} + + +/***/ }), + +/***/ 85: +/***/ (function(module) { + +"use strict"; + +module.exports = function generate__limitItems(it, $keyword, $ruleType) { + var out = ' '; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $errorKeyword; + var $data = 'data' + ($dataLvl || ''); + var $isData = it.opts.$data && $schema && $schema.$data, + $schemaValue; + if ($isData) { + out += ' var schema' + ($lvl) + ' = ' + (it.util.getData($schema.$data, $dataLvl, it.dataPathArr)) + '; '; + $schemaValue = 'schema' + $lvl; + } else { + $schemaValue = $schema; + } + var $op = $keyword == 'maxItems' ? '>' : '<'; + out += 'if ( '; + if ($isData) { + out += ' (' + ($schemaValue) + ' !== undefined && typeof ' + ($schemaValue) + ' != \'number\') || '; + } + out += ' ' + ($data) + '.length ' + ($op) + ' ' + ($schemaValue) + ') { '; + var $errorKeyword = $keyword; + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ($errorKeyword || '_limitItems') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { limit: ' + ($schemaValue) + ' } '; + if (it.opts.messages !== false) { + out += ' , message: \'should NOT have '; + if ($keyword == 'maxItems') { + out += 'more'; + } else { + out += 'fewer'; + } + out += ' than '; + if ($isData) { + out += '\' + ' + ($schemaValue) + ' + \''; + } else { + out += '' + ($schema); + } + out += ' items\' '; + } + if (it.opts.verbose) { + out += ' , schema: '; + if ($isData) { + out += 'validate.schema' + ($schemaPath); + } else { + out += '' + ($schema); + } + out += ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + out += '} '; + if ($breakOnError) { + out += ' else { '; + } + return out; +} + + +/***/ }), + +/***/ 87: +/***/ (function(module) { + +module.exports = require("os"); + +/***/ }), + +/***/ 91: +/***/ (function(module, __unusedexports, __webpack_require__) { + +var serialOrdered = __webpack_require__(892); + +// 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); +} + + +/***/ }), + +/***/ 98: +/***/ (function(module, __unusedexports, __webpack_require__) { + +// Copyright 2015 Joyent, Inc. + +var Buffer = __webpack_require__(215).Buffer; + +var algInfo = { + 'dsa': { + parts: ['p', 'q', 'g', 'y'], + sizePart: 'p' + }, + 'rsa': { + parts: ['e', 'n'], + sizePart: 'n' + }, + 'ecdsa': { + parts: ['curve', 'Q'], + sizePart: 'Q' + }, + 'ed25519': { + parts: ['A'], + sizePart: 'A' + } +}; +algInfo['curve25519'] = algInfo['ed25519']; + +var algPrivInfo = { + 'dsa': { + parts: ['p', 'q', 'g', 'y', 'x'] + }, + 'rsa': { + parts: ['n', 'e', 'd', 'iqmp', 'p', 'q'] + }, + 'ecdsa': { + parts: ['curve', 'Q', 'd'] + }, + 'ed25519': { + parts: ['A', 'k'] + } +}; +algPrivInfo['curve25519'] = algPrivInfo['ed25519']; + +var hashAlgs = { + 'md5': true, + 'sha1': true, + 'sha256': true, + 'sha384': true, + 'sha512': true +}; + +/* + * Taken from + * http://csrc.nist.gov/groups/ST/toolkit/documents/dss/NISTReCur.pdf + */ +var curves = { + 'nistp256': { + size: 256, + pkcs8oid: '1.2.840.10045.3.1.7', + p: Buffer.from(('00' + + 'ffffffff 00000001 00000000 00000000' + + '00000000 ffffffff ffffffff ffffffff'). + replace(/ /g, ''), 'hex'), + a: Buffer.from(('00' + + 'FFFFFFFF 00000001 00000000 00000000' + + '00000000 FFFFFFFF FFFFFFFF FFFFFFFC'). + replace(/ /g, ''), 'hex'), + b: Buffer.from(( + '5ac635d8 aa3a93e7 b3ebbd55 769886bc' + + '651d06b0 cc53b0f6 3bce3c3e 27d2604b'). + replace(/ /g, ''), 'hex'), + s: Buffer.from(('00' + + 'c49d3608 86e70493 6a6678e1 139d26b7' + + '819f7e90'). + replace(/ /g, ''), 'hex'), + n: Buffer.from(('00' + + 'ffffffff 00000000 ffffffff ffffffff' + + 'bce6faad a7179e84 f3b9cac2 fc632551'). + replace(/ /g, ''), 'hex'), + G: Buffer.from(('04' + + '6b17d1f2 e12c4247 f8bce6e5 63a440f2' + + '77037d81 2deb33a0 f4a13945 d898c296' + + '4fe342e2 fe1a7f9b 8ee7eb4a 7c0f9e16' + + '2bce3357 6b315ece cbb64068 37bf51f5'). + replace(/ /g, ''), 'hex') + }, + 'nistp384': { + size: 384, + pkcs8oid: '1.3.132.0.34', + p: Buffer.from(('00' + + 'ffffffff ffffffff ffffffff ffffffff' + + 'ffffffff ffffffff ffffffff fffffffe' + + 'ffffffff 00000000 00000000 ffffffff'). + replace(/ /g, ''), 'hex'), + a: Buffer.from(('00' + + 'FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF' + + 'FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE' + + 'FFFFFFFF 00000000 00000000 FFFFFFFC'). + replace(/ /g, ''), 'hex'), + b: Buffer.from(( + 'b3312fa7 e23ee7e4 988e056b e3f82d19' + + '181d9c6e fe814112 0314088f 5013875a' + + 'c656398d 8a2ed19d 2a85c8ed d3ec2aef'). + replace(/ /g, ''), 'hex'), + s: Buffer.from(('00' + + 'a335926a a319a27a 1d00896a 6773a482' + + '7acdac73'). + replace(/ /g, ''), 'hex'), + n: Buffer.from(('00' + + 'ffffffff ffffffff ffffffff ffffffff' + + 'ffffffff ffffffff c7634d81 f4372ddf' + + '581a0db2 48b0a77a ecec196a ccc52973'). + replace(/ /g, ''), 'hex'), + G: Buffer.from(('04' + + 'aa87ca22 be8b0537 8eb1c71e f320ad74' + + '6e1d3b62 8ba79b98 59f741e0 82542a38' + + '5502f25d bf55296c 3a545e38 72760ab7' + + '3617de4a 96262c6f 5d9e98bf 9292dc29' + + 'f8f41dbd 289a147c e9da3113 b5f0b8c0' + + '0a60b1ce 1d7e819d 7a431d7c 90ea0e5f'). + replace(/ /g, ''), 'hex') + }, + 'nistp521': { + size: 521, + pkcs8oid: '1.3.132.0.35', + p: Buffer.from(( + '01ffffff ffffffff ffffffff ffffffff' + + 'ffffffff ffffffff ffffffff ffffffff' + + 'ffffffff ffffffff ffffffff ffffffff' + + 'ffffffff ffffffff ffffffff ffffffff' + + 'ffff').replace(/ /g, ''), 'hex'), + a: Buffer.from(('01FF' + + 'FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF' + + 'FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF' + + 'FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF' + + 'FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFC'). + replace(/ /g, ''), 'hex'), + b: Buffer.from(('51' + + '953eb961 8e1c9a1f 929a21a0 b68540ee' + + 'a2da725b 99b315f3 b8b48991 8ef109e1' + + '56193951 ec7e937b 1652c0bd 3bb1bf07' + + '3573df88 3d2c34f1 ef451fd4 6b503f00'). + replace(/ /g, ''), 'hex'), + s: Buffer.from(('00' + + 'd09e8800 291cb853 96cc6717 393284aa' + + 'a0da64ba').replace(/ /g, ''), 'hex'), + n: Buffer.from(('01ff' + + 'ffffffff ffffffff ffffffff ffffffff' + + 'ffffffff ffffffff ffffffff fffffffa' + + '51868783 bf2f966b 7fcc0148 f709a5d0' + + '3bb5c9b8 899c47ae bb6fb71e 91386409'). + replace(/ /g, ''), 'hex'), + G: Buffer.from(('04' + + '00c6 858e06b7 0404e9cd 9e3ecb66 2395b442' + + '9c648139 053fb521 f828af60 6b4d3dba' + + 'a14b5e77 efe75928 fe1dc127 a2ffa8de' + + '3348b3c1 856a429b f97e7e31 c2e5bd66' + + '0118 39296a78 9a3bc004 5c8a5fb4 2c7d1bd9' + + '98f54449 579b4468 17afbd17 273e662c' + + '97ee7299 5ef42640 c550b901 3fad0761' + + '353c7086 a272c240 88be9476 9fd16650'). + replace(/ /g, ''), 'hex') + } +}; + +module.exports = { + info: algInfo, + privInfo: algPrivInfo, + hashAlgs: hashAlgs, + curves: curves +}; + + +/***/ }), + +/***/ 104: +/***/ (function(__unusedmodule, __unusedexports, __webpack_require__) { + +/*! + * 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. + */ + +const core = __webpack_require__(470); +const Twitter = __webpack_require__(50); + +function sendTweet() { + const twitter = new Twitter({ + consumer_key: core.getInput('consumer-key'), + consumer_secret: core.getInput('consumer-secret'), + access_token_key: core.getInput('access-token'), + access_token_secret: core.getInput('access-token-secret') + }); + + return twitter.post('/statuses/update', {status: core.getInput('status')}) + .then(() => { + return; + }) + .catch((err) => { + core.setFailed(err.message); + }); +} + +sendTweet(); + + +/***/ }), + +/***/ 107: +/***/ (function(module) { + +"use strict"; + +module.exports = function generate_allOf(it, $keyword, $ruleType) { + var out = ' '; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $it = it.util.copy(it); + var $closingBraces = ''; + $it.level++; + var $nextValid = 'valid' + $it.level; + var $currentBaseId = $it.baseId, + $allSchemasEmpty = true; + var arr1 = $schema; + if (arr1) { + var $sch, $i = -1, + l1 = arr1.length - 1; + while ($i < l1) { + $sch = arr1[$i += 1]; + if ((it.opts.strictKeywords ? typeof $sch == 'object' && Object.keys($sch).length > 0 : it.util.schemaHasRules($sch, it.RULES.all))) { + $allSchemasEmpty = false; + $it.schema = $sch; + $it.schemaPath = $schemaPath + '[' + $i + ']'; + $it.errSchemaPath = $errSchemaPath + '/' + $i; + out += ' ' + (it.validate($it)) + ' '; + $it.baseId = $currentBaseId; + if ($breakOnError) { + out += ' if (' + ($nextValid) + ') { '; + $closingBraces += '}'; + } + } + } + } + if ($breakOnError) { + if ($allSchemasEmpty) { + out += ' if (true) { '; + } else { + out += ' ' + ($closingBraces.slice(0, -1)) + ' '; + } + } + out = it.util.cleanUpCode(out); + return out; +} + + +/***/ }), + +/***/ 113: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +var crypto = __webpack_require__(417) + +function sha (key, body, algorithm) { + return crypto.createHmac(algorithm, key).update(body).digest('base64') +} + +function rsa (key, body) { + return crypto.createSign('RSA-SHA1').update(body).sign(key, 'base64') +} + +function rfc3986 (str) { + return encodeURIComponent(str) + .replace(/!/g,'%21') + .replace(/\*/g,'%2A') + .replace(/\(/g,'%28') + .replace(/\)/g,'%29') + .replace(/'/g,'%27') +} + +// Maps object to bi-dimensional array +// Converts { foo: 'A', bar: [ 'b', 'B' ]} to +// [ ['foo', 'A'], ['bar', 'b'], ['bar', 'B'] ] +function map (obj) { + var key, val, arr = [] + for (key in obj) { + val = obj[key] + if (Array.isArray(val)) + for (var i = 0; i < val.length; i++) + arr.push([key, val[i]]) + else if (typeof val === 'object') + for (var prop in val) + arr.push([key + '[' + prop + ']', val[prop]]) + else + arr.push([key, val]) + } + return arr +} + +// Compare function for sort +function compare (a, b) { + return a > b ? 1 : a < b ? -1 : 0 +} + +function generateBase (httpMethod, base_uri, params) { + // adapted from https://dev.twitter.com/docs/auth/oauth and + // https://dev.twitter.com/docs/auth/creating-signature + + // Parameter normalization + // http://tools.ietf.org/html/rfc5849#section-3.4.1.3.2 + var normalized = map(params) + // 1. First, the name and value of each parameter are encoded + .map(function (p) { + return [ rfc3986(p[0]), rfc3986(p[1] || '') ] + }) + // 2. The parameters are sorted by name, using ascending byte value + // ordering. If two or more parameters share the same name, they + // are sorted by their value. + .sort(function (a, b) { + return compare(a[0], b[0]) || compare(a[1], b[1]) + }) + // 3. The name of each parameter is concatenated to its corresponding + // value using an "=" character (ASCII code 61) as a separator, even + // if the value is empty. + .map(function (p) { return p.join('=') }) + // 4. The sorted name/value pairs are concatenated together into a + // single string by using an "&" character (ASCII code 38) as + // separator. + .join('&') + + var base = [ + rfc3986(httpMethod ? httpMethod.toUpperCase() : 'GET'), + rfc3986(base_uri), + rfc3986(normalized) + ].join('&') + + return base +} + +function hmacsign (httpMethod, base_uri, params, consumer_secret, token_secret) { + var base = generateBase(httpMethod, base_uri, params) + var key = [ + consumer_secret || '', + token_secret || '' + ].map(rfc3986).join('&') + + return sha(key, base, 'sha1') +} + +function hmacsign256 (httpMethod, base_uri, params, consumer_secret, token_secret) { + var base = generateBase(httpMethod, base_uri, params) + var key = [ + consumer_secret || '', + token_secret || '' + ].map(rfc3986).join('&') + + return sha(key, base, 'sha256') +} + +function rsasign (httpMethod, base_uri, params, private_key, token_secret) { + var base = generateBase(httpMethod, base_uri, params) + var key = private_key || '' + + return rsa(key, base) +} + +function plaintext (consumer_secret, token_secret) { + var key = [ + consumer_secret || '', + token_secret || '' + ].map(rfc3986).join('&') + + return key +} + +function sign (signMethod, httpMethod, base_uri, params, consumer_secret, token_secret) { + var method + var skipArgs = 1 + + switch (signMethod) { + case 'RSA-SHA1': + method = rsasign + break + case 'HMAC-SHA1': + method = hmacsign + break + case 'HMAC-SHA256': + method = hmacsign256 + break + case 'PLAINTEXT': + method = plaintext + skipArgs = 4 + break + default: + throw new Error('Signature method not supported: ' + signMethod) + } + + return method.apply(null, [].slice.call(arguments, skipArgs)) +} + +exports.hmacsign = hmacsign +exports.hmacsign256 = hmacsign256 +exports.rsasign = rsasign +exports.plaintext = plaintext +exports.sign = sign +exports.rfc3986 = rfc3986 +exports.generateBase = generateBase + +/***/ }), + +/***/ 139: +/***/ (function(module, __unusedexports, __webpack_require__) { + +// Unique ID creation requires a high quality random # generator. In node.js +// this is pretty straight-forward - we use the crypto API. + +var crypto = __webpack_require__(417); + +module.exports = function nodeRNG() { + return crypto.randomBytes(16); +}; + + +/***/ }), + +/***/ 147: +/***/ (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; +} + + +/***/ }), + +/***/ 149: +/***/ (function(module, exports, __webpack_require__) { + +/* eslint-disable node/no-deprecated-api */ +var buffer = __webpack_require__(293) +var Buffer = buffer.Buffer + +// alternative to using Object.keys for old browsers +function copyProps (src, dst) { + for (var key in src) { + dst[key] = src[key] + } +} +if (Buffer.from && Buffer.alloc && Buffer.allocUnsafe && Buffer.allocUnsafeSlow) { + module.exports = buffer +} else { + // Copy properties from require('buffer') + copyProps(buffer, exports) + exports.Buffer = SafeBuffer +} + +function SafeBuffer (arg, encodingOrOffset, length) { + return Buffer(arg, encodingOrOffset, length) +} + +SafeBuffer.prototype = Object.create(Buffer.prototype) + +// Copy static methods from Buffer +copyProps(Buffer, SafeBuffer) + +SafeBuffer.from = function (arg, encodingOrOffset, length) { + if (typeof arg === 'number') { + throw new TypeError('Argument must not be a number') + } + return Buffer(arg, encodingOrOffset, length) +} + +SafeBuffer.alloc = function (size, fill, encoding) { + if (typeof size !== 'number') { + throw new TypeError('Argument must be a number') + } + var buf = Buffer(size) + if (fill !== undefined) { + if (typeof encoding === 'string') { + buf.fill(fill, encoding) + } else { + buf.fill(fill) + } + } else { + buf.fill(0) + } + return buf +} + +SafeBuffer.allocUnsafe = function (size) { + if (typeof size !== 'number') { + throw new TypeError('Argument must be a number') + } + return Buffer(size) +} + +SafeBuffer.allocUnsafeSlow = function (size) { + if (typeof size !== 'number') { + throw new TypeError('Argument must be a number') + } + return buffer.SlowBuffer(size) +} + + +/***/ }), + +/***/ 152: +/***/ (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)); +}; + + +/***/ }), + +/***/ 154: +/***/ (function(module) { + +"use strict"; + +module.exports = function generate_contains(it, $keyword, $ruleType) { + var out = ' '; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $data = 'data' + ($dataLvl || ''); + var $valid = 'valid' + $lvl; + var $errs = 'errs__' + $lvl; + var $it = it.util.copy(it); + var $closingBraces = ''; + $it.level++; + var $nextValid = 'valid' + $it.level; + var $idx = 'i' + $lvl, + $dataNxt = $it.dataLevel = it.dataLevel + 1, + $nextData = 'data' + $dataNxt, + $currentBaseId = it.baseId, + $nonEmptySchema = (it.opts.strictKeywords ? typeof $schema == 'object' && Object.keys($schema).length > 0 : it.util.schemaHasRules($schema, it.RULES.all)); + out += 'var ' + ($errs) + ' = errors;var ' + ($valid) + ';'; + if ($nonEmptySchema) { + var $wasComposite = it.compositeRule; + it.compositeRule = $it.compositeRule = true; + $it.schema = $schema; + $it.schemaPath = $schemaPath; + $it.errSchemaPath = $errSchemaPath; + out += ' var ' + ($nextValid) + ' = false; for (var ' + ($idx) + ' = 0; ' + ($idx) + ' < ' + ($data) + '.length; ' + ($idx) + '++) { '; + $it.errorPath = it.util.getPathExpr(it.errorPath, $idx, it.opts.jsonPointers, true); + var $passData = $data + '[' + $idx + ']'; + $it.dataPathArr[$dataNxt] = $idx; + var $code = it.validate($it); + $it.baseId = $currentBaseId; + if (it.util.varOccurences($code, $nextData) < 2) { + out += ' ' + (it.util.varReplace($code, $nextData, $passData)) + ' '; + } else { + out += ' var ' + ($nextData) + ' = ' + ($passData) + '; ' + ($code) + ' '; + } + out += ' if (' + ($nextValid) + ') break; } '; + it.compositeRule = $it.compositeRule = $wasComposite; + out += ' ' + ($closingBraces) + ' if (!' + ($nextValid) + ') {'; + } else { + out += ' if (' + ($data) + '.length == 0) {'; + } + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('contains') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: {} '; + if (it.opts.messages !== false) { + out += ' , message: \'should contain a valid item\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + out += ' } else { '; + if ($nonEmptySchema) { + out += ' errors = ' + ($errs) + '; if (vErrors !== null) { if (' + ($errs) + ') vErrors.length = ' + ($errs) + '; else vErrors = null; } '; + } + if (it.opts.allErrors) { + out += ' } '; + } + out = it.util.cleanUpCode(out); + return out; +} + + +/***/ }), + +/***/ 157: +/***/ (function(module, __unusedexports, __webpack_require__) { + +var async = __webpack_require__(751) + , abort = __webpack_require__(566) + ; + +// 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; +} + + +/***/ }), + +/***/ 162: +/***/ (function(module) { + +module.exports = {"$id":"content.json#","$schema":"http://json-schema.org/draft-06/schema#","type":"object","required":["size","mimeType"],"properties":{"size":{"type":"integer"},"compression":{"type":"integer"},"mimeType":{"type":"string"},"text":{"type":"string"},"encoding":{"type":"string"},"comment":{"type":"string"}}}; + +/***/ }), + +/***/ 181: +/***/ (function(module) { + +module.exports = {"$id":"pageTimings.json#","$schema":"http://json-schema.org/draft-06/schema#","type":"object","properties":{"onContentLoad":{"type":"number","min":-1},"onLoad":{"type":"number","min":-1},"comment":{"type":"string"}}}; + +/***/ }), + +/***/ 191: +/***/ (function(module) { + +module.exports = require("querystring"); + +/***/ }), + +/***/ 196: +/***/ (function(module, __unusedexports, __webpack_require__) { + +(function(nacl) { +'use strict'; + +// Ported in 2014 by Dmitry Chestnykh and Devi Mandiri. +// Public domain. +// +// Implementation derived from TweetNaCl version 20140427. +// See for details: http://tweetnacl.cr.yp.to/ + +var gf = function(init) { + var i, r = new Float64Array(16); + if (init) for (i = 0; i < init.length; i++) r[i] = init[i]; + return r; +}; + +// Pluggable, initialized in high-level API below. +var randombytes = function(/* x, n */) { throw new Error('no PRNG'); }; + +var _0 = new Uint8Array(16); +var _9 = new Uint8Array(32); _9[0] = 9; + +var gf0 = gf(), + gf1 = gf([1]), + _121665 = gf([0xdb41, 1]), + D = gf([0x78a3, 0x1359, 0x4dca, 0x75eb, 0xd8ab, 0x4141, 0x0a4d, 0x0070, 0xe898, 0x7779, 0x4079, 0x8cc7, 0xfe73, 0x2b6f, 0x6cee, 0x5203]), + D2 = gf([0xf159, 0x26b2, 0x9b94, 0xebd6, 0xb156, 0x8283, 0x149a, 0x00e0, 0xd130, 0xeef3, 0x80f2, 0x198e, 0xfce7, 0x56df, 0xd9dc, 0x2406]), + X = gf([0xd51a, 0x8f25, 0x2d60, 0xc956, 0xa7b2, 0x9525, 0xc760, 0x692c, 0xdc5c, 0xfdd6, 0xe231, 0xc0a4, 0x53fe, 0xcd6e, 0x36d3, 0x2169]), + Y = gf([0x6658, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666]), + I = gf([0xa0b0, 0x4a0e, 0x1b27, 0xc4ee, 0xe478, 0xad2f, 0x1806, 0x2f43, 0xd7a7, 0x3dfb, 0x0099, 0x2b4d, 0xdf0b, 0x4fc1, 0x2480, 0x2b83]); + +function ts64(x, i, h, l) { + x[i] = (h >> 24) & 0xff; + x[i+1] = (h >> 16) & 0xff; + x[i+2] = (h >> 8) & 0xff; + x[i+3] = h & 0xff; + x[i+4] = (l >> 24) & 0xff; + x[i+5] = (l >> 16) & 0xff; + x[i+6] = (l >> 8) & 0xff; + x[i+7] = l & 0xff; +} + +function vn(x, xi, y, yi, n) { + var i,d = 0; + for (i = 0; i < n; i++) d |= x[xi+i]^y[yi+i]; + return (1 & ((d - 1) >>> 8)) - 1; +} + +function crypto_verify_16(x, xi, y, yi) { + return vn(x,xi,y,yi,16); +} + +function crypto_verify_32(x, xi, y, yi) { + return vn(x,xi,y,yi,32); +} + +function core_salsa20(o, p, k, c) { + var j0 = c[ 0] & 0xff | (c[ 1] & 0xff)<<8 | (c[ 2] & 0xff)<<16 | (c[ 3] & 0xff)<<24, + j1 = k[ 0] & 0xff | (k[ 1] & 0xff)<<8 | (k[ 2] & 0xff)<<16 | (k[ 3] & 0xff)<<24, + j2 = k[ 4] & 0xff | (k[ 5] & 0xff)<<8 | (k[ 6] & 0xff)<<16 | (k[ 7] & 0xff)<<24, + j3 = k[ 8] & 0xff | (k[ 9] & 0xff)<<8 | (k[10] & 0xff)<<16 | (k[11] & 0xff)<<24, + j4 = k[12] & 0xff | (k[13] & 0xff)<<8 | (k[14] & 0xff)<<16 | (k[15] & 0xff)<<24, + j5 = c[ 4] & 0xff | (c[ 5] & 0xff)<<8 | (c[ 6] & 0xff)<<16 | (c[ 7] & 0xff)<<24, + j6 = p[ 0] & 0xff | (p[ 1] & 0xff)<<8 | (p[ 2] & 0xff)<<16 | (p[ 3] & 0xff)<<24, + j7 = p[ 4] & 0xff | (p[ 5] & 0xff)<<8 | (p[ 6] & 0xff)<<16 | (p[ 7] & 0xff)<<24, + j8 = p[ 8] & 0xff | (p[ 9] & 0xff)<<8 | (p[10] & 0xff)<<16 | (p[11] & 0xff)<<24, + j9 = p[12] & 0xff | (p[13] & 0xff)<<8 | (p[14] & 0xff)<<16 | (p[15] & 0xff)<<24, + j10 = c[ 8] & 0xff | (c[ 9] & 0xff)<<8 | (c[10] & 0xff)<<16 | (c[11] & 0xff)<<24, + j11 = k[16] & 0xff | (k[17] & 0xff)<<8 | (k[18] & 0xff)<<16 | (k[19] & 0xff)<<24, + j12 = k[20] & 0xff | (k[21] & 0xff)<<8 | (k[22] & 0xff)<<16 | (k[23] & 0xff)<<24, + j13 = k[24] & 0xff | (k[25] & 0xff)<<8 | (k[26] & 0xff)<<16 | (k[27] & 0xff)<<24, + j14 = k[28] & 0xff | (k[29] & 0xff)<<8 | (k[30] & 0xff)<<16 | (k[31] & 0xff)<<24, + j15 = c[12] & 0xff | (c[13] & 0xff)<<8 | (c[14] & 0xff)<<16 | (c[15] & 0xff)<<24; + + var x0 = j0, x1 = j1, x2 = j2, x3 = j3, x4 = j4, x5 = j5, x6 = j6, x7 = j7, + x8 = j8, x9 = j9, x10 = j10, x11 = j11, x12 = j12, x13 = j13, x14 = j14, + x15 = j15, u; + + for (var i = 0; i < 20; i += 2) { + u = x0 + x12 | 0; + x4 ^= u<<7 | u>>>(32-7); + u = x4 + x0 | 0; + x8 ^= u<<9 | u>>>(32-9); + u = x8 + x4 | 0; + x12 ^= u<<13 | u>>>(32-13); + u = x12 + x8 | 0; + x0 ^= u<<18 | u>>>(32-18); + + u = x5 + x1 | 0; + x9 ^= u<<7 | u>>>(32-7); + u = x9 + x5 | 0; + x13 ^= u<<9 | u>>>(32-9); + u = x13 + x9 | 0; + x1 ^= u<<13 | u>>>(32-13); + u = x1 + x13 | 0; + x5 ^= u<<18 | u>>>(32-18); + + u = x10 + x6 | 0; + x14 ^= u<<7 | u>>>(32-7); + u = x14 + x10 | 0; + x2 ^= u<<9 | u>>>(32-9); + u = x2 + x14 | 0; + x6 ^= u<<13 | u>>>(32-13); + u = x6 + x2 | 0; + x10 ^= u<<18 | u>>>(32-18); + + u = x15 + x11 | 0; + x3 ^= u<<7 | u>>>(32-7); + u = x3 + x15 | 0; + x7 ^= u<<9 | u>>>(32-9); + u = x7 + x3 | 0; + x11 ^= u<<13 | u>>>(32-13); + u = x11 + x7 | 0; + x15 ^= u<<18 | u>>>(32-18); + + u = x0 + x3 | 0; + x1 ^= u<<7 | u>>>(32-7); + u = x1 + x0 | 0; + x2 ^= u<<9 | u>>>(32-9); + u = x2 + x1 | 0; + x3 ^= u<<13 | u>>>(32-13); + u = x3 + x2 | 0; + x0 ^= u<<18 | u>>>(32-18); + + u = x5 + x4 | 0; + x6 ^= u<<7 | u>>>(32-7); + u = x6 + x5 | 0; + x7 ^= u<<9 | u>>>(32-9); + u = x7 + x6 | 0; + x4 ^= u<<13 | u>>>(32-13); + u = x4 + x7 | 0; + x5 ^= u<<18 | u>>>(32-18); + + u = x10 + x9 | 0; + x11 ^= u<<7 | u>>>(32-7); + u = x11 + x10 | 0; + x8 ^= u<<9 | u>>>(32-9); + u = x8 + x11 | 0; + x9 ^= u<<13 | u>>>(32-13); + u = x9 + x8 | 0; + x10 ^= u<<18 | u>>>(32-18); + + u = x15 + x14 | 0; + x12 ^= u<<7 | u>>>(32-7); + u = x12 + x15 | 0; + x13 ^= u<<9 | u>>>(32-9); + u = x13 + x12 | 0; + x14 ^= u<<13 | u>>>(32-13); + u = x14 + x13 | 0; + x15 ^= u<<18 | u>>>(32-18); + } + x0 = x0 + j0 | 0; + x1 = x1 + j1 | 0; + x2 = x2 + j2 | 0; + x3 = x3 + j3 | 0; + x4 = x4 + j4 | 0; + x5 = x5 + j5 | 0; + x6 = x6 + j6 | 0; + x7 = x7 + j7 | 0; + x8 = x8 + j8 | 0; + x9 = x9 + j9 | 0; + x10 = x10 + j10 | 0; + x11 = x11 + j11 | 0; + x12 = x12 + j12 | 0; + x13 = x13 + j13 | 0; + x14 = x14 + j14 | 0; + x15 = x15 + j15 | 0; + + o[ 0] = x0 >>> 0 & 0xff; + o[ 1] = x0 >>> 8 & 0xff; + o[ 2] = x0 >>> 16 & 0xff; + o[ 3] = x0 >>> 24 & 0xff; + + o[ 4] = x1 >>> 0 & 0xff; + o[ 5] = x1 >>> 8 & 0xff; + o[ 6] = x1 >>> 16 & 0xff; + o[ 7] = x1 >>> 24 & 0xff; + + o[ 8] = x2 >>> 0 & 0xff; + o[ 9] = x2 >>> 8 & 0xff; + o[10] = x2 >>> 16 & 0xff; + o[11] = x2 >>> 24 & 0xff; + + o[12] = x3 >>> 0 & 0xff; + o[13] = x3 >>> 8 & 0xff; + o[14] = x3 >>> 16 & 0xff; + o[15] = x3 >>> 24 & 0xff; + + o[16] = x4 >>> 0 & 0xff; + o[17] = x4 >>> 8 & 0xff; + o[18] = x4 >>> 16 & 0xff; + o[19] = x4 >>> 24 & 0xff; + + o[20] = x5 >>> 0 & 0xff; + o[21] = x5 >>> 8 & 0xff; + o[22] = x5 >>> 16 & 0xff; + o[23] = x5 >>> 24 & 0xff; + + o[24] = x6 >>> 0 & 0xff; + o[25] = x6 >>> 8 & 0xff; + o[26] = x6 >>> 16 & 0xff; + o[27] = x6 >>> 24 & 0xff; + + o[28] = x7 >>> 0 & 0xff; + o[29] = x7 >>> 8 & 0xff; + o[30] = x7 >>> 16 & 0xff; + o[31] = x7 >>> 24 & 0xff; + + o[32] = x8 >>> 0 & 0xff; + o[33] = x8 >>> 8 & 0xff; + o[34] = x8 >>> 16 & 0xff; + o[35] = x8 >>> 24 & 0xff; + + o[36] = x9 >>> 0 & 0xff; + o[37] = x9 >>> 8 & 0xff; + o[38] = x9 >>> 16 & 0xff; + o[39] = x9 >>> 24 & 0xff; + + o[40] = x10 >>> 0 & 0xff; + o[41] = x10 >>> 8 & 0xff; + o[42] = x10 >>> 16 & 0xff; + o[43] = x10 >>> 24 & 0xff; + + o[44] = x11 >>> 0 & 0xff; + o[45] = x11 >>> 8 & 0xff; + o[46] = x11 >>> 16 & 0xff; + o[47] = x11 >>> 24 & 0xff; + + o[48] = x12 >>> 0 & 0xff; + o[49] = x12 >>> 8 & 0xff; + o[50] = x12 >>> 16 & 0xff; + o[51] = x12 >>> 24 & 0xff; + + o[52] = x13 >>> 0 & 0xff; + o[53] = x13 >>> 8 & 0xff; + o[54] = x13 >>> 16 & 0xff; + o[55] = x13 >>> 24 & 0xff; + + o[56] = x14 >>> 0 & 0xff; + o[57] = x14 >>> 8 & 0xff; + o[58] = x14 >>> 16 & 0xff; + o[59] = x14 >>> 24 & 0xff; + + o[60] = x15 >>> 0 & 0xff; + o[61] = x15 >>> 8 & 0xff; + o[62] = x15 >>> 16 & 0xff; + o[63] = x15 >>> 24 & 0xff; +} + +function core_hsalsa20(o,p,k,c) { + var j0 = c[ 0] & 0xff | (c[ 1] & 0xff)<<8 | (c[ 2] & 0xff)<<16 | (c[ 3] & 0xff)<<24, + j1 = k[ 0] & 0xff | (k[ 1] & 0xff)<<8 | (k[ 2] & 0xff)<<16 | (k[ 3] & 0xff)<<24, + j2 = k[ 4] & 0xff | (k[ 5] & 0xff)<<8 | (k[ 6] & 0xff)<<16 | (k[ 7] & 0xff)<<24, + j3 = k[ 8] & 0xff | (k[ 9] & 0xff)<<8 | (k[10] & 0xff)<<16 | (k[11] & 0xff)<<24, + j4 = k[12] & 0xff | (k[13] & 0xff)<<8 | (k[14] & 0xff)<<16 | (k[15] & 0xff)<<24, + j5 = c[ 4] & 0xff | (c[ 5] & 0xff)<<8 | (c[ 6] & 0xff)<<16 | (c[ 7] & 0xff)<<24, + j6 = p[ 0] & 0xff | (p[ 1] & 0xff)<<8 | (p[ 2] & 0xff)<<16 | (p[ 3] & 0xff)<<24, + j7 = p[ 4] & 0xff | (p[ 5] & 0xff)<<8 | (p[ 6] & 0xff)<<16 | (p[ 7] & 0xff)<<24, + j8 = p[ 8] & 0xff | (p[ 9] & 0xff)<<8 | (p[10] & 0xff)<<16 | (p[11] & 0xff)<<24, + j9 = p[12] & 0xff | (p[13] & 0xff)<<8 | (p[14] & 0xff)<<16 | (p[15] & 0xff)<<24, + j10 = c[ 8] & 0xff | (c[ 9] & 0xff)<<8 | (c[10] & 0xff)<<16 | (c[11] & 0xff)<<24, + j11 = k[16] & 0xff | (k[17] & 0xff)<<8 | (k[18] & 0xff)<<16 | (k[19] & 0xff)<<24, + j12 = k[20] & 0xff | (k[21] & 0xff)<<8 | (k[22] & 0xff)<<16 | (k[23] & 0xff)<<24, + j13 = k[24] & 0xff | (k[25] & 0xff)<<8 | (k[26] & 0xff)<<16 | (k[27] & 0xff)<<24, + j14 = k[28] & 0xff | (k[29] & 0xff)<<8 | (k[30] & 0xff)<<16 | (k[31] & 0xff)<<24, + j15 = c[12] & 0xff | (c[13] & 0xff)<<8 | (c[14] & 0xff)<<16 | (c[15] & 0xff)<<24; + + var x0 = j0, x1 = j1, x2 = j2, x3 = j3, x4 = j4, x5 = j5, x6 = j6, x7 = j7, + x8 = j8, x9 = j9, x10 = j10, x11 = j11, x12 = j12, x13 = j13, x14 = j14, + x15 = j15, u; + + for (var i = 0; i < 20; i += 2) { + u = x0 + x12 | 0; + x4 ^= u<<7 | u>>>(32-7); + u = x4 + x0 | 0; + x8 ^= u<<9 | u>>>(32-9); + u = x8 + x4 | 0; + x12 ^= u<<13 | u>>>(32-13); + u = x12 + x8 | 0; + x0 ^= u<<18 | u>>>(32-18); + + u = x5 + x1 | 0; + x9 ^= u<<7 | u>>>(32-7); + u = x9 + x5 | 0; + x13 ^= u<<9 | u>>>(32-9); + u = x13 + x9 | 0; + x1 ^= u<<13 | u>>>(32-13); + u = x1 + x13 | 0; + x5 ^= u<<18 | u>>>(32-18); + + u = x10 + x6 | 0; + x14 ^= u<<7 | u>>>(32-7); + u = x14 + x10 | 0; + x2 ^= u<<9 | u>>>(32-9); + u = x2 + x14 | 0; + x6 ^= u<<13 | u>>>(32-13); + u = x6 + x2 | 0; + x10 ^= u<<18 | u>>>(32-18); + + u = x15 + x11 | 0; + x3 ^= u<<7 | u>>>(32-7); + u = x3 + x15 | 0; + x7 ^= u<<9 | u>>>(32-9); + u = x7 + x3 | 0; + x11 ^= u<<13 | u>>>(32-13); + u = x11 + x7 | 0; + x15 ^= u<<18 | u>>>(32-18); + + u = x0 + x3 | 0; + x1 ^= u<<7 | u>>>(32-7); + u = x1 + x0 | 0; + x2 ^= u<<9 | u>>>(32-9); + u = x2 + x1 | 0; + x3 ^= u<<13 | u>>>(32-13); + u = x3 + x2 | 0; + x0 ^= u<<18 | u>>>(32-18); + + u = x5 + x4 | 0; + x6 ^= u<<7 | u>>>(32-7); + u = x6 + x5 | 0; + x7 ^= u<<9 | u>>>(32-9); + u = x7 + x6 | 0; + x4 ^= u<<13 | u>>>(32-13); + u = x4 + x7 | 0; + x5 ^= u<<18 | u>>>(32-18); + + u = x10 + x9 | 0; + x11 ^= u<<7 | u>>>(32-7); + u = x11 + x10 | 0; + x8 ^= u<<9 | u>>>(32-9); + u = x8 + x11 | 0; + x9 ^= u<<13 | u>>>(32-13); + u = x9 + x8 | 0; + x10 ^= u<<18 | u>>>(32-18); + + u = x15 + x14 | 0; + x12 ^= u<<7 | u>>>(32-7); + u = x12 + x15 | 0; + x13 ^= u<<9 | u>>>(32-9); + u = x13 + x12 | 0; + x14 ^= u<<13 | u>>>(32-13); + u = x14 + x13 | 0; + x15 ^= u<<18 | u>>>(32-18); + } + + o[ 0] = x0 >>> 0 & 0xff; + o[ 1] = x0 >>> 8 & 0xff; + o[ 2] = x0 >>> 16 & 0xff; + o[ 3] = x0 >>> 24 & 0xff; + + o[ 4] = x5 >>> 0 & 0xff; + o[ 5] = x5 >>> 8 & 0xff; + o[ 6] = x5 >>> 16 & 0xff; + o[ 7] = x5 >>> 24 & 0xff; + + o[ 8] = x10 >>> 0 & 0xff; + o[ 9] = x10 >>> 8 & 0xff; + o[10] = x10 >>> 16 & 0xff; + o[11] = x10 >>> 24 & 0xff; + + o[12] = x15 >>> 0 & 0xff; + o[13] = x15 >>> 8 & 0xff; + o[14] = x15 >>> 16 & 0xff; + o[15] = x15 >>> 24 & 0xff; + + o[16] = x6 >>> 0 & 0xff; + o[17] = x6 >>> 8 & 0xff; + o[18] = x6 >>> 16 & 0xff; + o[19] = x6 >>> 24 & 0xff; + + o[20] = x7 >>> 0 & 0xff; + o[21] = x7 >>> 8 & 0xff; + o[22] = x7 >>> 16 & 0xff; + o[23] = x7 >>> 24 & 0xff; + + o[24] = x8 >>> 0 & 0xff; + o[25] = x8 >>> 8 & 0xff; + o[26] = x8 >>> 16 & 0xff; + o[27] = x8 >>> 24 & 0xff; + + o[28] = x9 >>> 0 & 0xff; + o[29] = x9 >>> 8 & 0xff; + o[30] = x9 >>> 16 & 0xff; + o[31] = x9 >>> 24 & 0xff; +} + +function crypto_core_salsa20(out,inp,k,c) { + core_salsa20(out,inp,k,c); +} + +function crypto_core_hsalsa20(out,inp,k,c) { + core_hsalsa20(out,inp,k,c); +} + +var sigma = new Uint8Array([101, 120, 112, 97, 110, 100, 32, 51, 50, 45, 98, 121, 116, 101, 32, 107]); + // "expand 32-byte k" + +function crypto_stream_salsa20_xor(c,cpos,m,mpos,b,n,k) { + var z = new Uint8Array(16), x = new Uint8Array(64); + var u, i; + for (i = 0; i < 16; i++) z[i] = 0; + for (i = 0; i < 8; i++) z[i] = n[i]; + while (b >= 64) { + crypto_core_salsa20(x,z,k,sigma); + for (i = 0; i < 64; i++) c[cpos+i] = m[mpos+i] ^ x[i]; + u = 1; + for (i = 8; i < 16; i++) { + u = u + (z[i] & 0xff) | 0; + z[i] = u & 0xff; + u >>>= 8; + } + b -= 64; + cpos += 64; + mpos += 64; + } + if (b > 0) { + crypto_core_salsa20(x,z,k,sigma); + for (i = 0; i < b; i++) c[cpos+i] = m[mpos+i] ^ x[i]; + } + return 0; +} + +function crypto_stream_salsa20(c,cpos,b,n,k) { + var z = new Uint8Array(16), x = new Uint8Array(64); + var u, i; + for (i = 0; i < 16; i++) z[i] = 0; + for (i = 0; i < 8; i++) z[i] = n[i]; + while (b >= 64) { + crypto_core_salsa20(x,z,k,sigma); + for (i = 0; i < 64; i++) c[cpos+i] = x[i]; + u = 1; + for (i = 8; i < 16; i++) { + u = u + (z[i] & 0xff) | 0; + z[i] = u & 0xff; + u >>>= 8; + } + b -= 64; + cpos += 64; + } + if (b > 0) { + crypto_core_salsa20(x,z,k,sigma); + for (i = 0; i < b; i++) c[cpos+i] = x[i]; + } + return 0; +} + +function crypto_stream(c,cpos,d,n,k) { + var s = new Uint8Array(32); + crypto_core_hsalsa20(s,n,k,sigma); + var sn = new Uint8Array(8); + for (var i = 0; i < 8; i++) sn[i] = n[i+16]; + return crypto_stream_salsa20(c,cpos,d,sn,s); +} + +function crypto_stream_xor(c,cpos,m,mpos,d,n,k) { + var s = new Uint8Array(32); + crypto_core_hsalsa20(s,n,k,sigma); + var sn = new Uint8Array(8); + for (var i = 0; i < 8; i++) sn[i] = n[i+16]; + return crypto_stream_salsa20_xor(c,cpos,m,mpos,d,sn,s); +} + +/* +* Port of Andrew Moon's Poly1305-donna-16. Public domain. +* https://github.com/floodyberry/poly1305-donna +*/ + +var poly1305 = function(key) { + this.buffer = new Uint8Array(16); + this.r = new Uint16Array(10); + this.h = new Uint16Array(10); + this.pad = new Uint16Array(8); + this.leftover = 0; + this.fin = 0; + + var t0, t1, t2, t3, t4, t5, t6, t7; + + t0 = key[ 0] & 0xff | (key[ 1] & 0xff) << 8; this.r[0] = ( t0 ) & 0x1fff; + t1 = key[ 2] & 0xff | (key[ 3] & 0xff) << 8; this.r[1] = ((t0 >>> 13) | (t1 << 3)) & 0x1fff; + t2 = key[ 4] & 0xff | (key[ 5] & 0xff) << 8; this.r[2] = ((t1 >>> 10) | (t2 << 6)) & 0x1f03; + t3 = key[ 6] & 0xff | (key[ 7] & 0xff) << 8; this.r[3] = ((t2 >>> 7) | (t3 << 9)) & 0x1fff; + t4 = key[ 8] & 0xff | (key[ 9] & 0xff) << 8; this.r[4] = ((t3 >>> 4) | (t4 << 12)) & 0x00ff; + this.r[5] = ((t4 >>> 1)) & 0x1ffe; + t5 = key[10] & 0xff | (key[11] & 0xff) << 8; this.r[6] = ((t4 >>> 14) | (t5 << 2)) & 0x1fff; + t6 = key[12] & 0xff | (key[13] & 0xff) << 8; this.r[7] = ((t5 >>> 11) | (t6 << 5)) & 0x1f81; + t7 = key[14] & 0xff | (key[15] & 0xff) << 8; this.r[8] = ((t6 >>> 8) | (t7 << 8)) & 0x1fff; + this.r[9] = ((t7 >>> 5)) & 0x007f; + + this.pad[0] = key[16] & 0xff | (key[17] & 0xff) << 8; + this.pad[1] = key[18] & 0xff | (key[19] & 0xff) << 8; + this.pad[2] = key[20] & 0xff | (key[21] & 0xff) << 8; + this.pad[3] = key[22] & 0xff | (key[23] & 0xff) << 8; + this.pad[4] = key[24] & 0xff | (key[25] & 0xff) << 8; + this.pad[5] = key[26] & 0xff | (key[27] & 0xff) << 8; + this.pad[6] = key[28] & 0xff | (key[29] & 0xff) << 8; + this.pad[7] = key[30] & 0xff | (key[31] & 0xff) << 8; +}; + +poly1305.prototype.blocks = function(m, mpos, bytes) { + var hibit = this.fin ? 0 : (1 << 11); + var t0, t1, t2, t3, t4, t5, t6, t7, c; + var d0, d1, d2, d3, d4, d5, d6, d7, d8, d9; + + var h0 = this.h[0], + h1 = this.h[1], + h2 = this.h[2], + h3 = this.h[3], + h4 = this.h[4], + h5 = this.h[5], + h6 = this.h[6], + h7 = this.h[7], + h8 = this.h[8], + h9 = this.h[9]; + + var r0 = this.r[0], + r1 = this.r[1], + r2 = this.r[2], + r3 = this.r[3], + r4 = this.r[4], + r5 = this.r[5], + r6 = this.r[6], + r7 = this.r[7], + r8 = this.r[8], + r9 = this.r[9]; + + while (bytes >= 16) { + t0 = m[mpos+ 0] & 0xff | (m[mpos+ 1] & 0xff) << 8; h0 += ( t0 ) & 0x1fff; + t1 = m[mpos+ 2] & 0xff | (m[mpos+ 3] & 0xff) << 8; h1 += ((t0 >>> 13) | (t1 << 3)) & 0x1fff; + t2 = m[mpos+ 4] & 0xff | (m[mpos+ 5] & 0xff) << 8; h2 += ((t1 >>> 10) | (t2 << 6)) & 0x1fff; + t3 = m[mpos+ 6] & 0xff | (m[mpos+ 7] & 0xff) << 8; h3 += ((t2 >>> 7) | (t3 << 9)) & 0x1fff; + t4 = m[mpos+ 8] & 0xff | (m[mpos+ 9] & 0xff) << 8; h4 += ((t3 >>> 4) | (t4 << 12)) & 0x1fff; + h5 += ((t4 >>> 1)) & 0x1fff; + t5 = m[mpos+10] & 0xff | (m[mpos+11] & 0xff) << 8; h6 += ((t4 >>> 14) | (t5 << 2)) & 0x1fff; + t6 = m[mpos+12] & 0xff | (m[mpos+13] & 0xff) << 8; h7 += ((t5 >>> 11) | (t6 << 5)) & 0x1fff; + t7 = m[mpos+14] & 0xff | (m[mpos+15] & 0xff) << 8; h8 += ((t6 >>> 8) | (t7 << 8)) & 0x1fff; + h9 += ((t7 >>> 5)) | hibit; + + c = 0; + + d0 = c; + d0 += h0 * r0; + d0 += h1 * (5 * r9); + d0 += h2 * (5 * r8); + d0 += h3 * (5 * r7); + d0 += h4 * (5 * r6); + c = (d0 >>> 13); d0 &= 0x1fff; + d0 += h5 * (5 * r5); + d0 += h6 * (5 * r4); + d0 += h7 * (5 * r3); + d0 += h8 * (5 * r2); + d0 += h9 * (5 * r1); + c += (d0 >>> 13); d0 &= 0x1fff; + + d1 = c; + d1 += h0 * r1; + d1 += h1 * r0; + d1 += h2 * (5 * r9); + d1 += h3 * (5 * r8); + d1 += h4 * (5 * r7); + c = (d1 >>> 13); d1 &= 0x1fff; + d1 += h5 * (5 * r6); + d1 += h6 * (5 * r5); + d1 += h7 * (5 * r4); + d1 += h8 * (5 * r3); + d1 += h9 * (5 * r2); + c += (d1 >>> 13); d1 &= 0x1fff; + + d2 = c; + d2 += h0 * r2; + d2 += h1 * r1; + d2 += h2 * r0; + d2 += h3 * (5 * r9); + d2 += h4 * (5 * r8); + c = (d2 >>> 13); d2 &= 0x1fff; + d2 += h5 * (5 * r7); + d2 += h6 * (5 * r6); + d2 += h7 * (5 * r5); + d2 += h8 * (5 * r4); + d2 += h9 * (5 * r3); + c += (d2 >>> 13); d2 &= 0x1fff; + + d3 = c; + d3 += h0 * r3; + d3 += h1 * r2; + d3 += h2 * r1; + d3 += h3 * r0; + d3 += h4 * (5 * r9); + c = (d3 >>> 13); d3 &= 0x1fff; + d3 += h5 * (5 * r8); + d3 += h6 * (5 * r7); + d3 += h7 * (5 * r6); + d3 += h8 * (5 * r5); + d3 += h9 * (5 * r4); + c += (d3 >>> 13); d3 &= 0x1fff; + + d4 = c; + d4 += h0 * r4; + d4 += h1 * r3; + d4 += h2 * r2; + d4 += h3 * r1; + d4 += h4 * r0; + c = (d4 >>> 13); d4 &= 0x1fff; + d4 += h5 * (5 * r9); + d4 += h6 * (5 * r8); + d4 += h7 * (5 * r7); + d4 += h8 * (5 * r6); + d4 += h9 * (5 * r5); + c += (d4 >>> 13); d4 &= 0x1fff; + + d5 = c; + d5 += h0 * r5; + d5 += h1 * r4; + d5 += h2 * r3; + d5 += h3 * r2; + d5 += h4 * r1; + c = (d5 >>> 13); d5 &= 0x1fff; + d5 += h5 * r0; + d5 += h6 * (5 * r9); + d5 += h7 * (5 * r8); + d5 += h8 * (5 * r7); + d5 += h9 * (5 * r6); + c += (d5 >>> 13); d5 &= 0x1fff; + + d6 = c; + d6 += h0 * r6; + d6 += h1 * r5; + d6 += h2 * r4; + d6 += h3 * r3; + d6 += h4 * r2; + c = (d6 >>> 13); d6 &= 0x1fff; + d6 += h5 * r1; + d6 += h6 * r0; + d6 += h7 * (5 * r9); + d6 += h8 * (5 * r8); + d6 += h9 * (5 * r7); + c += (d6 >>> 13); d6 &= 0x1fff; + + d7 = c; + d7 += h0 * r7; + d7 += h1 * r6; + d7 += h2 * r5; + d7 += h3 * r4; + d7 += h4 * r3; + c = (d7 >>> 13); d7 &= 0x1fff; + d7 += h5 * r2; + d7 += h6 * r1; + d7 += h7 * r0; + d7 += h8 * (5 * r9); + d7 += h9 * (5 * r8); + c += (d7 >>> 13); d7 &= 0x1fff; + + d8 = c; + d8 += h0 * r8; + d8 += h1 * r7; + d8 += h2 * r6; + d8 += h3 * r5; + d8 += h4 * r4; + c = (d8 >>> 13); d8 &= 0x1fff; + d8 += h5 * r3; + d8 += h6 * r2; + d8 += h7 * r1; + d8 += h8 * r0; + d8 += h9 * (5 * r9); + c += (d8 >>> 13); d8 &= 0x1fff; + + d9 = c; + d9 += h0 * r9; + d9 += h1 * r8; + d9 += h2 * r7; + d9 += h3 * r6; + d9 += h4 * r5; + c = (d9 >>> 13); d9 &= 0x1fff; + d9 += h5 * r4; + d9 += h6 * r3; + d9 += h7 * r2; + d9 += h8 * r1; + d9 += h9 * r0; + c += (d9 >>> 13); d9 &= 0x1fff; + + c = (((c << 2) + c)) | 0; + c = (c + d0) | 0; + d0 = c & 0x1fff; + c = (c >>> 13); + d1 += c; + + h0 = d0; + h1 = d1; + h2 = d2; + h3 = d3; + h4 = d4; + h5 = d5; + h6 = d6; + h7 = d7; + h8 = d8; + h9 = d9; + + mpos += 16; + bytes -= 16; + } + this.h[0] = h0; + this.h[1] = h1; + this.h[2] = h2; + this.h[3] = h3; + this.h[4] = h4; + this.h[5] = h5; + this.h[6] = h6; + this.h[7] = h7; + this.h[8] = h8; + this.h[9] = h9; +}; + +poly1305.prototype.finish = function(mac, macpos) { + var g = new Uint16Array(10); + var c, mask, f, i; + + if (this.leftover) { + i = this.leftover; + this.buffer[i++] = 1; + for (; i < 16; i++) this.buffer[i] = 0; + this.fin = 1; + this.blocks(this.buffer, 0, 16); + } + + c = this.h[1] >>> 13; + this.h[1] &= 0x1fff; + for (i = 2; i < 10; i++) { + this.h[i] += c; + c = this.h[i] >>> 13; + this.h[i] &= 0x1fff; + } + this.h[0] += (c * 5); + c = this.h[0] >>> 13; + this.h[0] &= 0x1fff; + this.h[1] += c; + c = this.h[1] >>> 13; + this.h[1] &= 0x1fff; + this.h[2] += c; + + g[0] = this.h[0] + 5; + c = g[0] >>> 13; + g[0] &= 0x1fff; + for (i = 1; i < 10; i++) { + g[i] = this.h[i] + c; + c = g[i] >>> 13; + g[i] &= 0x1fff; + } + g[9] -= (1 << 13); + + mask = (c ^ 1) - 1; + for (i = 0; i < 10; i++) g[i] &= mask; + mask = ~mask; + for (i = 0; i < 10; i++) this.h[i] = (this.h[i] & mask) | g[i]; + + this.h[0] = ((this.h[0] ) | (this.h[1] << 13) ) & 0xffff; + this.h[1] = ((this.h[1] >>> 3) | (this.h[2] << 10) ) & 0xffff; + this.h[2] = ((this.h[2] >>> 6) | (this.h[3] << 7) ) & 0xffff; + this.h[3] = ((this.h[3] >>> 9) | (this.h[4] << 4) ) & 0xffff; + this.h[4] = ((this.h[4] >>> 12) | (this.h[5] << 1) | (this.h[6] << 14)) & 0xffff; + this.h[5] = ((this.h[6] >>> 2) | (this.h[7] << 11) ) & 0xffff; + this.h[6] = ((this.h[7] >>> 5) | (this.h[8] << 8) ) & 0xffff; + this.h[7] = ((this.h[8] >>> 8) | (this.h[9] << 5) ) & 0xffff; + + f = this.h[0] + this.pad[0]; + this.h[0] = f & 0xffff; + for (i = 1; i < 8; i++) { + f = (((this.h[i] + this.pad[i]) | 0) + (f >>> 16)) | 0; + this.h[i] = f & 0xffff; + } + + mac[macpos+ 0] = (this.h[0] >>> 0) & 0xff; + mac[macpos+ 1] = (this.h[0] >>> 8) & 0xff; + mac[macpos+ 2] = (this.h[1] >>> 0) & 0xff; + mac[macpos+ 3] = (this.h[1] >>> 8) & 0xff; + mac[macpos+ 4] = (this.h[2] >>> 0) & 0xff; + mac[macpos+ 5] = (this.h[2] >>> 8) & 0xff; + mac[macpos+ 6] = (this.h[3] >>> 0) & 0xff; + mac[macpos+ 7] = (this.h[3] >>> 8) & 0xff; + mac[macpos+ 8] = (this.h[4] >>> 0) & 0xff; + mac[macpos+ 9] = (this.h[4] >>> 8) & 0xff; + mac[macpos+10] = (this.h[5] >>> 0) & 0xff; + mac[macpos+11] = (this.h[5] >>> 8) & 0xff; + mac[macpos+12] = (this.h[6] >>> 0) & 0xff; + mac[macpos+13] = (this.h[6] >>> 8) & 0xff; + mac[macpos+14] = (this.h[7] >>> 0) & 0xff; + mac[macpos+15] = (this.h[7] >>> 8) & 0xff; +}; + +poly1305.prototype.update = function(m, mpos, bytes) { + var i, want; + + if (this.leftover) { + want = (16 - this.leftover); + if (want > bytes) + want = bytes; + for (i = 0; i < want; i++) + this.buffer[this.leftover + i] = m[mpos+i]; + bytes -= want; + mpos += want; + this.leftover += want; + if (this.leftover < 16) + return; + this.blocks(this.buffer, 0, 16); + this.leftover = 0; + } + + if (bytes >= 16) { + want = bytes - (bytes % 16); + this.blocks(m, mpos, want); + mpos += want; + bytes -= want; + } + + if (bytes) { + for (i = 0; i < bytes; i++) + this.buffer[this.leftover + i] = m[mpos+i]; + this.leftover += bytes; + } +}; + +function crypto_onetimeauth(out, outpos, m, mpos, n, k) { + var s = new poly1305(k); + s.update(m, mpos, n); + s.finish(out, outpos); + return 0; +} + +function crypto_onetimeauth_verify(h, hpos, m, mpos, n, k) { + var x = new Uint8Array(16); + crypto_onetimeauth(x,0,m,mpos,n,k); + return crypto_verify_16(h,hpos,x,0); +} + +function crypto_secretbox(c,m,d,n,k) { + var i; + if (d < 32) return -1; + crypto_stream_xor(c,0,m,0,d,n,k); + crypto_onetimeauth(c, 16, c, 32, d - 32, c); + for (i = 0; i < 16; i++) c[i] = 0; + return 0; +} + +function crypto_secretbox_open(m,c,d,n,k) { + var i; + var x = new Uint8Array(32); + if (d < 32) return -1; + crypto_stream(x,0,32,n,k); + if (crypto_onetimeauth_verify(c, 16,c, 32,d - 32,x) !== 0) return -1; + crypto_stream_xor(m,0,c,0,d,n,k); + for (i = 0; i < 32; i++) m[i] = 0; + return 0; +} + +function set25519(r, a) { + var i; + for (i = 0; i < 16; i++) r[i] = a[i]|0; +} + +function car25519(o) { + var i, v, c = 1; + for (i = 0; i < 16; i++) { + v = o[i] + c + 65535; + c = Math.floor(v / 65536); + o[i] = v - c * 65536; + } + o[0] += c-1 + 37 * (c-1); +} + +function sel25519(p, q, b) { + var t, c = ~(b-1); + for (var i = 0; i < 16; i++) { + t = c & (p[i] ^ q[i]); + p[i] ^= t; + q[i] ^= t; + } +} + +function pack25519(o, n) { + var i, j, b; + var m = gf(), t = gf(); + for (i = 0; i < 16; i++) t[i] = n[i]; + car25519(t); + car25519(t); + car25519(t); + for (j = 0; j < 2; j++) { + m[0] = t[0] - 0xffed; + for (i = 1; i < 15; i++) { + m[i] = t[i] - 0xffff - ((m[i-1]>>16) & 1); + m[i-1] &= 0xffff; + } + m[15] = t[15] - 0x7fff - ((m[14]>>16) & 1); + b = (m[15]>>16) & 1; + m[14] &= 0xffff; + sel25519(t, m, 1-b); + } + for (i = 0; i < 16; i++) { + o[2*i] = t[i] & 0xff; + o[2*i+1] = t[i]>>8; + } +} + +function neq25519(a, b) { + var c = new Uint8Array(32), d = new Uint8Array(32); + pack25519(c, a); + pack25519(d, b); + return crypto_verify_32(c, 0, d, 0); +} + +function par25519(a) { + var d = new Uint8Array(32); + pack25519(d, a); + return d[0] & 1; +} + +function unpack25519(o, n) { + var i; + for (i = 0; i < 16; i++) o[i] = n[2*i] + (n[2*i+1] << 8); + o[15] &= 0x7fff; +} + +function A(o, a, b) { + for (var i = 0; i < 16; i++) o[i] = a[i] + b[i]; +} + +function Z(o, a, b) { + for (var i = 0; i < 16; i++) o[i] = a[i] - b[i]; +} + +function M(o, a, b) { + var v, c, + t0 = 0, t1 = 0, t2 = 0, t3 = 0, t4 = 0, t5 = 0, t6 = 0, t7 = 0, + t8 = 0, t9 = 0, t10 = 0, t11 = 0, t12 = 0, t13 = 0, t14 = 0, t15 = 0, + t16 = 0, t17 = 0, t18 = 0, t19 = 0, t20 = 0, t21 = 0, t22 = 0, t23 = 0, + t24 = 0, t25 = 0, t26 = 0, t27 = 0, t28 = 0, t29 = 0, t30 = 0, + b0 = b[0], + b1 = b[1], + b2 = b[2], + b3 = b[3], + b4 = b[4], + b5 = b[5], + b6 = b[6], + b7 = b[7], + b8 = b[8], + b9 = b[9], + b10 = b[10], + b11 = b[11], + b12 = b[12], + b13 = b[13], + b14 = b[14], + b15 = b[15]; + + v = a[0]; + t0 += v * b0; + t1 += v * b1; + t2 += v * b2; + t3 += v * b3; + t4 += v * b4; + t5 += v * b5; + t6 += v * b6; + t7 += v * b7; + t8 += v * b8; + t9 += v * b9; + t10 += v * b10; + t11 += v * b11; + t12 += v * b12; + t13 += v * b13; + t14 += v * b14; + t15 += v * b15; + v = a[1]; + t1 += v * b0; + t2 += v * b1; + t3 += v * b2; + t4 += v * b3; + t5 += v * b4; + t6 += v * b5; + t7 += v * b6; + t8 += v * b7; + t9 += v * b8; + t10 += v * b9; + t11 += v * b10; + t12 += v * b11; + t13 += v * b12; + t14 += v * b13; + t15 += v * b14; + t16 += v * b15; + v = a[2]; + t2 += v * b0; + t3 += v * b1; + t4 += v * b2; + t5 += v * b3; + t6 += v * b4; + t7 += v * b5; + t8 += v * b6; + t9 += v * b7; + t10 += v * b8; + t11 += v * b9; + t12 += v * b10; + t13 += v * b11; + t14 += v * b12; + t15 += v * b13; + t16 += v * b14; + t17 += v * b15; + v = a[3]; + t3 += v * b0; + t4 += v * b1; + t5 += v * b2; + t6 += v * b3; + t7 += v * b4; + t8 += v * b5; + t9 += v * b6; + t10 += v * b7; + t11 += v * b8; + t12 += v * b9; + t13 += v * b10; + t14 += v * b11; + t15 += v * b12; + t16 += v * b13; + t17 += v * b14; + t18 += v * b15; + v = a[4]; + t4 += v * b0; + t5 += v * b1; + t6 += v * b2; + t7 += v * b3; + t8 += v * b4; + t9 += v * b5; + t10 += v * b6; + t11 += v * b7; + t12 += v * b8; + t13 += v * b9; + t14 += v * b10; + t15 += v * b11; + t16 += v * b12; + t17 += v * b13; + t18 += v * b14; + t19 += v * b15; + v = a[5]; + t5 += v * b0; + t6 += v * b1; + t7 += v * b2; + t8 += v * b3; + t9 += v * b4; + t10 += v * b5; + t11 += v * b6; + t12 += v * b7; + t13 += v * b8; + t14 += v * b9; + t15 += v * b10; + t16 += v * b11; + t17 += v * b12; + t18 += v * b13; + t19 += v * b14; + t20 += v * b15; + v = a[6]; + t6 += v * b0; + t7 += v * b1; + t8 += v * b2; + t9 += v * b3; + t10 += v * b4; + t11 += v * b5; + t12 += v * b6; + t13 += v * b7; + t14 += v * b8; + t15 += v * b9; + t16 += v * b10; + t17 += v * b11; + t18 += v * b12; + t19 += v * b13; + t20 += v * b14; + t21 += v * b15; + v = a[7]; + t7 += v * b0; + t8 += v * b1; + t9 += v * b2; + t10 += v * b3; + t11 += v * b4; + t12 += v * b5; + t13 += v * b6; + t14 += v * b7; + t15 += v * b8; + t16 += v * b9; + t17 += v * b10; + t18 += v * b11; + t19 += v * b12; + t20 += v * b13; + t21 += v * b14; + t22 += v * b15; + v = a[8]; + t8 += v * b0; + t9 += v * b1; + t10 += v * b2; + t11 += v * b3; + t12 += v * b4; + t13 += v * b5; + t14 += v * b6; + t15 += v * b7; + t16 += v * b8; + t17 += v * b9; + t18 += v * b10; + t19 += v * b11; + t20 += v * b12; + t21 += v * b13; + t22 += v * b14; + t23 += v * b15; + v = a[9]; + t9 += v * b0; + t10 += v * b1; + t11 += v * b2; + t12 += v * b3; + t13 += v * b4; + t14 += v * b5; + t15 += v * b6; + t16 += v * b7; + t17 += v * b8; + t18 += v * b9; + t19 += v * b10; + t20 += v * b11; + t21 += v * b12; + t22 += v * b13; + t23 += v * b14; + t24 += v * b15; + v = a[10]; + t10 += v * b0; + t11 += v * b1; + t12 += v * b2; + t13 += v * b3; + t14 += v * b4; + t15 += v * b5; + t16 += v * b6; + t17 += v * b7; + t18 += v * b8; + t19 += v * b9; + t20 += v * b10; + t21 += v * b11; + t22 += v * b12; + t23 += v * b13; + t24 += v * b14; + t25 += v * b15; + v = a[11]; + t11 += v * b0; + t12 += v * b1; + t13 += v * b2; + t14 += v * b3; + t15 += v * b4; + t16 += v * b5; + t17 += v * b6; + t18 += v * b7; + t19 += v * b8; + t20 += v * b9; + t21 += v * b10; + t22 += v * b11; + t23 += v * b12; + t24 += v * b13; + t25 += v * b14; + t26 += v * b15; + v = a[12]; + t12 += v * b0; + t13 += v * b1; + t14 += v * b2; + t15 += v * b3; + t16 += v * b4; + t17 += v * b5; + t18 += v * b6; + t19 += v * b7; + t20 += v * b8; + t21 += v * b9; + t22 += v * b10; + t23 += v * b11; + t24 += v * b12; + t25 += v * b13; + t26 += v * b14; + t27 += v * b15; + v = a[13]; + t13 += v * b0; + t14 += v * b1; + t15 += v * b2; + t16 += v * b3; + t17 += v * b4; + t18 += v * b5; + t19 += v * b6; + t20 += v * b7; + t21 += v * b8; + t22 += v * b9; + t23 += v * b10; + t24 += v * b11; + t25 += v * b12; + t26 += v * b13; + t27 += v * b14; + t28 += v * b15; + v = a[14]; + t14 += v * b0; + t15 += v * b1; + t16 += v * b2; + t17 += v * b3; + t18 += v * b4; + t19 += v * b5; + t20 += v * b6; + t21 += v * b7; + t22 += v * b8; + t23 += v * b9; + t24 += v * b10; + t25 += v * b11; + t26 += v * b12; + t27 += v * b13; + t28 += v * b14; + t29 += v * b15; + v = a[15]; + t15 += v * b0; + t16 += v * b1; + t17 += v * b2; + t18 += v * b3; + t19 += v * b4; + t20 += v * b5; + t21 += v * b6; + t22 += v * b7; + t23 += v * b8; + t24 += v * b9; + t25 += v * b10; + t26 += v * b11; + t27 += v * b12; + t28 += v * b13; + t29 += v * b14; + t30 += v * b15; + + t0 += 38 * t16; + t1 += 38 * t17; + t2 += 38 * t18; + t3 += 38 * t19; + t4 += 38 * t20; + t5 += 38 * t21; + t6 += 38 * t22; + t7 += 38 * t23; + t8 += 38 * t24; + t9 += 38 * t25; + t10 += 38 * t26; + t11 += 38 * t27; + t12 += 38 * t28; + t13 += 38 * t29; + t14 += 38 * t30; + // t15 left as is + + // first car + c = 1; + v = t0 + c + 65535; c = Math.floor(v / 65536); t0 = v - c * 65536; + v = t1 + c + 65535; c = Math.floor(v / 65536); t1 = v - c * 65536; + v = t2 + c + 65535; c = Math.floor(v / 65536); t2 = v - c * 65536; + v = t3 + c + 65535; c = Math.floor(v / 65536); t3 = v - c * 65536; + v = t4 + c + 65535; c = Math.floor(v / 65536); t4 = v - c * 65536; + v = t5 + c + 65535; c = Math.floor(v / 65536); t5 = v - c * 65536; + v = t6 + c + 65535; c = Math.floor(v / 65536); t6 = v - c * 65536; + v = t7 + c + 65535; c = Math.floor(v / 65536); t7 = v - c * 65536; + v = t8 + c + 65535; c = Math.floor(v / 65536); t8 = v - c * 65536; + v = t9 + c + 65535; c = Math.floor(v / 65536); t9 = v - c * 65536; + v = t10 + c + 65535; c = Math.floor(v / 65536); t10 = v - c * 65536; + v = t11 + c + 65535; c = Math.floor(v / 65536); t11 = v - c * 65536; + v = t12 + c + 65535; c = Math.floor(v / 65536); t12 = v - c * 65536; + v = t13 + c + 65535; c = Math.floor(v / 65536); t13 = v - c * 65536; + v = t14 + c + 65535; c = Math.floor(v / 65536); t14 = v - c * 65536; + v = t15 + c + 65535; c = Math.floor(v / 65536); t15 = v - c * 65536; + t0 += c-1 + 37 * (c-1); + + // second car + c = 1; + v = t0 + c + 65535; c = Math.floor(v / 65536); t0 = v - c * 65536; + v = t1 + c + 65535; c = Math.floor(v / 65536); t1 = v - c * 65536; + v = t2 + c + 65535; c = Math.floor(v / 65536); t2 = v - c * 65536; + v = t3 + c + 65535; c = Math.floor(v / 65536); t3 = v - c * 65536; + v = t4 + c + 65535; c = Math.floor(v / 65536); t4 = v - c * 65536; + v = t5 + c + 65535; c = Math.floor(v / 65536); t5 = v - c * 65536; + v = t6 + c + 65535; c = Math.floor(v / 65536); t6 = v - c * 65536; + v = t7 + c + 65535; c = Math.floor(v / 65536); t7 = v - c * 65536; + v = t8 + c + 65535; c = Math.floor(v / 65536); t8 = v - c * 65536; + v = t9 + c + 65535; c = Math.floor(v / 65536); t9 = v - c * 65536; + v = t10 + c + 65535; c = Math.floor(v / 65536); t10 = v - c * 65536; + v = t11 + c + 65535; c = Math.floor(v / 65536); t11 = v - c * 65536; + v = t12 + c + 65535; c = Math.floor(v / 65536); t12 = v - c * 65536; + v = t13 + c + 65535; c = Math.floor(v / 65536); t13 = v - c * 65536; + v = t14 + c + 65535; c = Math.floor(v / 65536); t14 = v - c * 65536; + v = t15 + c + 65535; c = Math.floor(v / 65536); t15 = v - c * 65536; + t0 += c-1 + 37 * (c-1); + + o[ 0] = t0; + o[ 1] = t1; + o[ 2] = t2; + o[ 3] = t3; + o[ 4] = t4; + o[ 5] = t5; + o[ 6] = t6; + o[ 7] = t7; + o[ 8] = t8; + o[ 9] = t9; + o[10] = t10; + o[11] = t11; + o[12] = t12; + o[13] = t13; + o[14] = t14; + o[15] = t15; +} + +function S(o, a) { + M(o, a, a); +} + +function inv25519(o, i) { + var c = gf(); + var a; + for (a = 0; a < 16; a++) c[a] = i[a]; + for (a = 253; a >= 0; a--) { + S(c, c); + if(a !== 2 && a !== 4) M(c, c, i); + } + for (a = 0; a < 16; a++) o[a] = c[a]; +} + +function pow2523(o, i) { + var c = gf(); + var a; + for (a = 0; a < 16; a++) c[a] = i[a]; + for (a = 250; a >= 0; a--) { + S(c, c); + if(a !== 1) M(c, c, i); + } + for (a = 0; a < 16; a++) o[a] = c[a]; +} + +function crypto_scalarmult(q, n, p) { + var z = new Uint8Array(32); + var x = new Float64Array(80), r, i; + var a = gf(), b = gf(), c = gf(), + d = gf(), e = gf(), f = gf(); + for (i = 0; i < 31; i++) z[i] = n[i]; + z[31]=(n[31]&127)|64; + z[0]&=248; + unpack25519(x,p); + for (i = 0; i < 16; i++) { + b[i]=x[i]; + d[i]=a[i]=c[i]=0; + } + a[0]=d[0]=1; + for (i=254; i>=0; --i) { + r=(z[i>>>3]>>>(i&7))&1; + sel25519(a,b,r); + sel25519(c,d,r); + A(e,a,c); + Z(a,a,c); + A(c,b,d); + Z(b,b,d); + S(d,e); + S(f,a); + M(a,c,a); + M(c,b,e); + A(e,a,c); + Z(a,a,c); + S(b,a); + Z(c,d,f); + M(a,c,_121665); + A(a,a,d); + M(c,c,a); + M(a,d,f); + M(d,b,x); + S(b,e); + sel25519(a,b,r); + sel25519(c,d,r); + } + for (i = 0; i < 16; i++) { + x[i+16]=a[i]; + x[i+32]=c[i]; + x[i+48]=b[i]; + x[i+64]=d[i]; + } + var x32 = x.subarray(32); + var x16 = x.subarray(16); + inv25519(x32,x32); + M(x16,x16,x32); + pack25519(q,x16); + return 0; +} + +function crypto_scalarmult_base(q, n) { + return crypto_scalarmult(q, n, _9); +} + +function crypto_box_keypair(y, x) { + randombytes(x, 32); + return crypto_scalarmult_base(y, x); +} + +function crypto_box_beforenm(k, y, x) { + var s = new Uint8Array(32); + crypto_scalarmult(s, x, y); + return crypto_core_hsalsa20(k, _0, s, sigma); +} + +var crypto_box_afternm = crypto_secretbox; +var crypto_box_open_afternm = crypto_secretbox_open; + +function crypto_box(c, m, d, n, y, x) { + var k = new Uint8Array(32); + crypto_box_beforenm(k, y, x); + return crypto_box_afternm(c, m, d, n, k); +} + +function crypto_box_open(m, c, d, n, y, x) { + var k = new Uint8Array(32); + crypto_box_beforenm(k, y, x); + return crypto_box_open_afternm(m, c, d, n, k); +} + +var K = [ + 0x428a2f98, 0xd728ae22, 0x71374491, 0x23ef65cd, + 0xb5c0fbcf, 0xec4d3b2f, 0xe9b5dba5, 0x8189dbbc, + 0x3956c25b, 0xf348b538, 0x59f111f1, 0xb605d019, + 0x923f82a4, 0xaf194f9b, 0xab1c5ed5, 0xda6d8118, + 0xd807aa98, 0xa3030242, 0x12835b01, 0x45706fbe, + 0x243185be, 0x4ee4b28c, 0x550c7dc3, 0xd5ffb4e2, + 0x72be5d74, 0xf27b896f, 0x80deb1fe, 0x3b1696b1, + 0x9bdc06a7, 0x25c71235, 0xc19bf174, 0xcf692694, + 0xe49b69c1, 0x9ef14ad2, 0xefbe4786, 0x384f25e3, + 0x0fc19dc6, 0x8b8cd5b5, 0x240ca1cc, 0x77ac9c65, + 0x2de92c6f, 0x592b0275, 0x4a7484aa, 0x6ea6e483, + 0x5cb0a9dc, 0xbd41fbd4, 0x76f988da, 0x831153b5, + 0x983e5152, 0xee66dfab, 0xa831c66d, 0x2db43210, + 0xb00327c8, 0x98fb213f, 0xbf597fc7, 0xbeef0ee4, + 0xc6e00bf3, 0x3da88fc2, 0xd5a79147, 0x930aa725, + 0x06ca6351, 0xe003826f, 0x14292967, 0x0a0e6e70, + 0x27b70a85, 0x46d22ffc, 0x2e1b2138, 0x5c26c926, + 0x4d2c6dfc, 0x5ac42aed, 0x53380d13, 0x9d95b3df, + 0x650a7354, 0x8baf63de, 0x766a0abb, 0x3c77b2a8, + 0x81c2c92e, 0x47edaee6, 0x92722c85, 0x1482353b, + 0xa2bfe8a1, 0x4cf10364, 0xa81a664b, 0xbc423001, + 0xc24b8b70, 0xd0f89791, 0xc76c51a3, 0x0654be30, + 0xd192e819, 0xd6ef5218, 0xd6990624, 0x5565a910, + 0xf40e3585, 0x5771202a, 0x106aa070, 0x32bbd1b8, + 0x19a4c116, 0xb8d2d0c8, 0x1e376c08, 0x5141ab53, + 0x2748774c, 0xdf8eeb99, 0x34b0bcb5, 0xe19b48a8, + 0x391c0cb3, 0xc5c95a63, 0x4ed8aa4a, 0xe3418acb, + 0x5b9cca4f, 0x7763e373, 0x682e6ff3, 0xd6b2b8a3, + 0x748f82ee, 0x5defb2fc, 0x78a5636f, 0x43172f60, + 0x84c87814, 0xa1f0ab72, 0x8cc70208, 0x1a6439ec, + 0x90befffa, 0x23631e28, 0xa4506ceb, 0xde82bde9, + 0xbef9a3f7, 0xb2c67915, 0xc67178f2, 0xe372532b, + 0xca273ece, 0xea26619c, 0xd186b8c7, 0x21c0c207, + 0xeada7dd6, 0xcde0eb1e, 0xf57d4f7f, 0xee6ed178, + 0x06f067aa, 0x72176fba, 0x0a637dc5, 0xa2c898a6, + 0x113f9804, 0xbef90dae, 0x1b710b35, 0x131c471b, + 0x28db77f5, 0x23047d84, 0x32caab7b, 0x40c72493, + 0x3c9ebe0a, 0x15c9bebc, 0x431d67c4, 0x9c100d4c, + 0x4cc5d4be, 0xcb3e42b6, 0x597f299c, 0xfc657e2a, + 0x5fcb6fab, 0x3ad6faec, 0x6c44198c, 0x4a475817 +]; + +function crypto_hashblocks_hl(hh, hl, m, n) { + var wh = new Int32Array(16), wl = new Int32Array(16), + bh0, bh1, bh2, bh3, bh4, bh5, bh6, bh7, + bl0, bl1, bl2, bl3, bl4, bl5, bl6, bl7, + th, tl, i, j, h, l, a, b, c, d; + + var ah0 = hh[0], + ah1 = hh[1], + ah2 = hh[2], + ah3 = hh[3], + ah4 = hh[4], + ah5 = hh[5], + ah6 = hh[6], + ah7 = hh[7], + + al0 = hl[0], + al1 = hl[1], + al2 = hl[2], + al3 = hl[3], + al4 = hl[4], + al5 = hl[5], + al6 = hl[6], + al7 = hl[7]; + + var pos = 0; + while (n >= 128) { + for (i = 0; i < 16; i++) { + j = 8 * i + pos; + wh[i] = (m[j+0] << 24) | (m[j+1] << 16) | (m[j+2] << 8) | m[j+3]; + wl[i] = (m[j+4] << 24) | (m[j+5] << 16) | (m[j+6] << 8) | m[j+7]; + } + for (i = 0; i < 80; i++) { + bh0 = ah0; + bh1 = ah1; + bh2 = ah2; + bh3 = ah3; + bh4 = ah4; + bh5 = ah5; + bh6 = ah6; + bh7 = ah7; + + bl0 = al0; + bl1 = al1; + bl2 = al2; + bl3 = al3; + bl4 = al4; + bl5 = al5; + bl6 = al6; + bl7 = al7; + + // add + h = ah7; + l = al7; + + a = l & 0xffff; b = l >>> 16; + c = h & 0xffff; d = h >>> 16; + + // Sigma1 + h = ((ah4 >>> 14) | (al4 << (32-14))) ^ ((ah4 >>> 18) | (al4 << (32-18))) ^ ((al4 >>> (41-32)) | (ah4 << (32-(41-32)))); + l = ((al4 >>> 14) | (ah4 << (32-14))) ^ ((al4 >>> 18) | (ah4 << (32-18))) ^ ((ah4 >>> (41-32)) | (al4 << (32-(41-32)))); + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + // Ch + h = (ah4 & ah5) ^ (~ah4 & ah6); + l = (al4 & al5) ^ (~al4 & al6); + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + // K + h = K[i*2]; + l = K[i*2+1]; + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + // w + h = wh[i%16]; + l = wl[i%16]; + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + + th = c & 0xffff | d << 16; + tl = a & 0xffff | b << 16; + + // add + h = th; + l = tl; + + a = l & 0xffff; b = l >>> 16; + c = h & 0xffff; d = h >>> 16; + + // Sigma0 + h = ((ah0 >>> 28) | (al0 << (32-28))) ^ ((al0 >>> (34-32)) | (ah0 << (32-(34-32)))) ^ ((al0 >>> (39-32)) | (ah0 << (32-(39-32)))); + l = ((al0 >>> 28) | (ah0 << (32-28))) ^ ((ah0 >>> (34-32)) | (al0 << (32-(34-32)))) ^ ((ah0 >>> (39-32)) | (al0 << (32-(39-32)))); + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + // Maj + h = (ah0 & ah1) ^ (ah0 & ah2) ^ (ah1 & ah2); + l = (al0 & al1) ^ (al0 & al2) ^ (al1 & al2); + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + + bh7 = (c & 0xffff) | (d << 16); + bl7 = (a & 0xffff) | (b << 16); + + // add + h = bh3; + l = bl3; + + a = l & 0xffff; b = l >>> 16; + c = h & 0xffff; d = h >>> 16; + + h = th; + l = tl; + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + + bh3 = (c & 0xffff) | (d << 16); + bl3 = (a & 0xffff) | (b << 16); + + ah1 = bh0; + ah2 = bh1; + ah3 = bh2; + ah4 = bh3; + ah5 = bh4; + ah6 = bh5; + ah7 = bh6; + ah0 = bh7; + + al1 = bl0; + al2 = bl1; + al3 = bl2; + al4 = bl3; + al5 = bl4; + al6 = bl5; + al7 = bl6; + al0 = bl7; + + if (i%16 === 15) { + for (j = 0; j < 16; j++) { + // add + h = wh[j]; + l = wl[j]; + + a = l & 0xffff; b = l >>> 16; + c = h & 0xffff; d = h >>> 16; + + h = wh[(j+9)%16]; + l = wl[(j+9)%16]; + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + // sigma0 + th = wh[(j+1)%16]; + tl = wl[(j+1)%16]; + h = ((th >>> 1) | (tl << (32-1))) ^ ((th >>> 8) | (tl << (32-8))) ^ (th >>> 7); + l = ((tl >>> 1) | (th << (32-1))) ^ ((tl >>> 8) | (th << (32-8))) ^ ((tl >>> 7) | (th << (32-7))); + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + // sigma1 + th = wh[(j+14)%16]; + tl = wl[(j+14)%16]; + h = ((th >>> 19) | (tl << (32-19))) ^ ((tl >>> (61-32)) | (th << (32-(61-32)))) ^ (th >>> 6); + l = ((tl >>> 19) | (th << (32-19))) ^ ((th >>> (61-32)) | (tl << (32-(61-32)))) ^ ((tl >>> 6) | (th << (32-6))); + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + + wh[j] = (c & 0xffff) | (d << 16); + wl[j] = (a & 0xffff) | (b << 16); + } + } + } + + // add + h = ah0; + l = al0; + + a = l & 0xffff; b = l >>> 16; + c = h & 0xffff; d = h >>> 16; + + h = hh[0]; + l = hl[0]; + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + + hh[0] = ah0 = (c & 0xffff) | (d << 16); + hl[0] = al0 = (a & 0xffff) | (b << 16); + + h = ah1; + l = al1; + + a = l & 0xffff; b = l >>> 16; + c = h & 0xffff; d = h >>> 16; + + h = hh[1]; + l = hl[1]; + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + + hh[1] = ah1 = (c & 0xffff) | (d << 16); + hl[1] = al1 = (a & 0xffff) | (b << 16); + + h = ah2; + l = al2; + + a = l & 0xffff; b = l >>> 16; + c = h & 0xffff; d = h >>> 16; + + h = hh[2]; + l = hl[2]; + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + + hh[2] = ah2 = (c & 0xffff) | (d << 16); + hl[2] = al2 = (a & 0xffff) | (b << 16); + + h = ah3; + l = al3; + + a = l & 0xffff; b = l >>> 16; + c = h & 0xffff; d = h >>> 16; + + h = hh[3]; + l = hl[3]; + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + + hh[3] = ah3 = (c & 0xffff) | (d << 16); + hl[3] = al3 = (a & 0xffff) | (b << 16); + + h = ah4; + l = al4; + + a = l & 0xffff; b = l >>> 16; + c = h & 0xffff; d = h >>> 16; + + h = hh[4]; + l = hl[4]; + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + + hh[4] = ah4 = (c & 0xffff) | (d << 16); + hl[4] = al4 = (a & 0xffff) | (b << 16); + + h = ah5; + l = al5; + + a = l & 0xffff; b = l >>> 16; + c = h & 0xffff; d = h >>> 16; + + h = hh[5]; + l = hl[5]; + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + + hh[5] = ah5 = (c & 0xffff) | (d << 16); + hl[5] = al5 = (a & 0xffff) | (b << 16); + + h = ah6; + l = al6; + + a = l & 0xffff; b = l >>> 16; + c = h & 0xffff; d = h >>> 16; + + h = hh[6]; + l = hl[6]; + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + + hh[6] = ah6 = (c & 0xffff) | (d << 16); + hl[6] = al6 = (a & 0xffff) | (b << 16); + + h = ah7; + l = al7; + + a = l & 0xffff; b = l >>> 16; + c = h & 0xffff; d = h >>> 16; + + h = hh[7]; + l = hl[7]; + + a += l & 0xffff; b += l >>> 16; + c += h & 0xffff; d += h >>> 16; + + b += a >>> 16; + c += b >>> 16; + d += c >>> 16; + + hh[7] = ah7 = (c & 0xffff) | (d << 16); + hl[7] = al7 = (a & 0xffff) | (b << 16); + + pos += 128; + n -= 128; + } + + return n; +} + +function crypto_hash(out, m, n) { + var hh = new Int32Array(8), + hl = new Int32Array(8), + x = new Uint8Array(256), + i, b = n; + + hh[0] = 0x6a09e667; + hh[1] = 0xbb67ae85; + hh[2] = 0x3c6ef372; + hh[3] = 0xa54ff53a; + hh[4] = 0x510e527f; + hh[5] = 0x9b05688c; + hh[6] = 0x1f83d9ab; + hh[7] = 0x5be0cd19; + + hl[0] = 0xf3bcc908; + hl[1] = 0x84caa73b; + hl[2] = 0xfe94f82b; + hl[3] = 0x5f1d36f1; + hl[4] = 0xade682d1; + hl[5] = 0x2b3e6c1f; + hl[6] = 0xfb41bd6b; + hl[7] = 0x137e2179; + + crypto_hashblocks_hl(hh, hl, m, n); + n %= 128; + + for (i = 0; i < n; i++) x[i] = m[b-n+i]; + x[n] = 128; + + n = 256-128*(n<112?1:0); + x[n-9] = 0; + ts64(x, n-8, (b / 0x20000000) | 0, b << 3); + crypto_hashblocks_hl(hh, hl, x, n); + + for (i = 0; i < 8; i++) ts64(out, 8*i, hh[i], hl[i]); + + return 0; +} + +function add(p, q) { + var a = gf(), b = gf(), c = gf(), + d = gf(), e = gf(), f = gf(), + g = gf(), h = gf(), t = gf(); + + Z(a, p[1], p[0]); + Z(t, q[1], q[0]); + M(a, a, t); + A(b, p[0], p[1]); + A(t, q[0], q[1]); + M(b, b, t); + M(c, p[3], q[3]); + M(c, c, D2); + M(d, p[2], q[2]); + A(d, d, d); + Z(e, b, a); + Z(f, d, c); + A(g, d, c); + A(h, b, a); + + M(p[0], e, f); + M(p[1], h, g); + M(p[2], g, f); + M(p[3], e, h); +} + +function cswap(p, q, b) { + var i; + for (i = 0; i < 4; i++) { + sel25519(p[i], q[i], b); + } +} + +function pack(r, p) { + var tx = gf(), ty = gf(), zi = gf(); + inv25519(zi, p[2]); + M(tx, p[0], zi); + M(ty, p[1], zi); + pack25519(r, ty); + r[31] ^= par25519(tx) << 7; +} + +function scalarmult(p, q, s) { + var b, i; + set25519(p[0], gf0); + set25519(p[1], gf1); + set25519(p[2], gf1); + set25519(p[3], gf0); + for (i = 255; i >= 0; --i) { + b = (s[(i/8)|0] >> (i&7)) & 1; + cswap(p, q, b); + add(q, p); + add(p, p); + cswap(p, q, b); + } +} + +function scalarbase(p, s) { + var q = [gf(), gf(), gf(), gf()]; + set25519(q[0], X); + set25519(q[1], Y); + set25519(q[2], gf1); + M(q[3], X, Y); + scalarmult(p, q, s); +} + +function crypto_sign_keypair(pk, sk, seeded) { + var d = new Uint8Array(64); + var p = [gf(), gf(), gf(), gf()]; + var i; + + if (!seeded) randombytes(sk, 32); + crypto_hash(d, sk, 32); + d[0] &= 248; + d[31] &= 127; + d[31] |= 64; + + scalarbase(p, d); + pack(pk, p); + + for (i = 0; i < 32; i++) sk[i+32] = pk[i]; + return 0; +} + +var L = new Float64Array([0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x10]); + +function modL(r, x) { + var carry, i, j, k; + for (i = 63; i >= 32; --i) { + carry = 0; + for (j = i - 32, k = i - 12; j < k; ++j) { + x[j] += carry - 16 * x[i] * L[j - (i - 32)]; + carry = (x[j] + 128) >> 8; + x[j] -= carry * 256; + } + x[j] += carry; + x[i] = 0; + } + carry = 0; + for (j = 0; j < 32; j++) { + x[j] += carry - (x[31] >> 4) * L[j]; + carry = x[j] >> 8; + x[j] &= 255; + } + for (j = 0; j < 32; j++) x[j] -= carry * L[j]; + for (i = 0; i < 32; i++) { + x[i+1] += x[i] >> 8; + r[i] = x[i] & 255; + } +} + +function reduce(r) { + var x = new Float64Array(64), i; + for (i = 0; i < 64; i++) x[i] = r[i]; + for (i = 0; i < 64; i++) r[i] = 0; + modL(r, x); +} + +// Note: difference from C - smlen returned, not passed as argument. +function crypto_sign(sm, m, n, sk) { + var d = new Uint8Array(64), h = new Uint8Array(64), r = new Uint8Array(64); + var i, j, x = new Float64Array(64); + var p = [gf(), gf(), gf(), gf()]; + + crypto_hash(d, sk, 32); + d[0] &= 248; + d[31] &= 127; + d[31] |= 64; + + var smlen = n + 64; + for (i = 0; i < n; i++) sm[64 + i] = m[i]; + for (i = 0; i < 32; i++) sm[32 + i] = d[32 + i]; + + crypto_hash(r, sm.subarray(32), n+32); + reduce(r); + scalarbase(p, r); + pack(sm, p); + + for (i = 32; i < 64; i++) sm[i] = sk[i]; + crypto_hash(h, sm, n + 64); + reduce(h); + + for (i = 0; i < 64; i++) x[i] = 0; + for (i = 0; i < 32; i++) x[i] = r[i]; + for (i = 0; i < 32; i++) { + for (j = 0; j < 32; j++) { + x[i+j] += h[i] * d[j]; + } + } + + modL(sm.subarray(32), x); + return smlen; +} + +function unpackneg(r, p) { + var t = gf(), chk = gf(), num = gf(), + den = gf(), den2 = gf(), den4 = gf(), + den6 = gf(); + + set25519(r[2], gf1); + unpack25519(r[1], p); + S(num, r[1]); + M(den, num, D); + Z(num, num, r[2]); + A(den, r[2], den); + + S(den2, den); + S(den4, den2); + M(den6, den4, den2); + M(t, den6, num); + M(t, t, den); + + pow2523(t, t); + M(t, t, num); + M(t, t, den); + M(t, t, den); + M(r[0], t, den); + + S(chk, r[0]); + M(chk, chk, den); + if (neq25519(chk, num)) M(r[0], r[0], I); + + S(chk, r[0]); + M(chk, chk, den); + if (neq25519(chk, num)) return -1; + + if (par25519(r[0]) === (p[31]>>7)) Z(r[0], gf0, r[0]); + + M(r[3], r[0], r[1]); + return 0; +} + +function crypto_sign_open(m, sm, n, pk) { + var i, mlen; + var t = new Uint8Array(32), h = new Uint8Array(64); + var p = [gf(), gf(), gf(), gf()], + q = [gf(), gf(), gf(), gf()]; + + mlen = -1; + if (n < 64) return -1; + + if (unpackneg(q, pk)) return -1; + + for (i = 0; i < n; i++) m[i] = sm[i]; + for (i = 0; i < 32; i++) m[i+32] = pk[i]; + crypto_hash(h, m, n); + reduce(h); + scalarmult(p, q, h); + + scalarbase(q, sm.subarray(32)); + add(p, q); + pack(t, p); + + n -= 64; + if (crypto_verify_32(sm, 0, t, 0)) { + for (i = 0; i < n; i++) m[i] = 0; + return -1; + } + + for (i = 0; i < n; i++) m[i] = sm[i + 64]; + mlen = n; + return mlen; +} + +var crypto_secretbox_KEYBYTES = 32, + crypto_secretbox_NONCEBYTES = 24, + crypto_secretbox_ZEROBYTES = 32, + crypto_secretbox_BOXZEROBYTES = 16, + crypto_scalarmult_BYTES = 32, + crypto_scalarmult_SCALARBYTES = 32, + crypto_box_PUBLICKEYBYTES = 32, + crypto_box_SECRETKEYBYTES = 32, + crypto_box_BEFORENMBYTES = 32, + crypto_box_NONCEBYTES = crypto_secretbox_NONCEBYTES, + crypto_box_ZEROBYTES = crypto_secretbox_ZEROBYTES, + crypto_box_BOXZEROBYTES = crypto_secretbox_BOXZEROBYTES, + crypto_sign_BYTES = 64, + crypto_sign_PUBLICKEYBYTES = 32, + crypto_sign_SECRETKEYBYTES = 64, + crypto_sign_SEEDBYTES = 32, + crypto_hash_BYTES = 64; + +nacl.lowlevel = { + crypto_core_hsalsa20: crypto_core_hsalsa20, + crypto_stream_xor: crypto_stream_xor, + crypto_stream: crypto_stream, + crypto_stream_salsa20_xor: crypto_stream_salsa20_xor, + crypto_stream_salsa20: crypto_stream_salsa20, + crypto_onetimeauth: crypto_onetimeauth, + crypto_onetimeauth_verify: crypto_onetimeauth_verify, + crypto_verify_16: crypto_verify_16, + crypto_verify_32: crypto_verify_32, + crypto_secretbox: crypto_secretbox, + crypto_secretbox_open: crypto_secretbox_open, + crypto_scalarmult: crypto_scalarmult, + crypto_scalarmult_base: crypto_scalarmult_base, + crypto_box_beforenm: crypto_box_beforenm, + crypto_box_afternm: crypto_box_afternm, + crypto_box: crypto_box, + crypto_box_open: crypto_box_open, + crypto_box_keypair: crypto_box_keypair, + crypto_hash: crypto_hash, + crypto_sign: crypto_sign, + crypto_sign_keypair: crypto_sign_keypair, + crypto_sign_open: crypto_sign_open, + + crypto_secretbox_KEYBYTES: crypto_secretbox_KEYBYTES, + crypto_secretbox_NONCEBYTES: crypto_secretbox_NONCEBYTES, + crypto_secretbox_ZEROBYTES: crypto_secretbox_ZEROBYTES, + crypto_secretbox_BOXZEROBYTES: crypto_secretbox_BOXZEROBYTES, + crypto_scalarmult_BYTES: crypto_scalarmult_BYTES, + crypto_scalarmult_SCALARBYTES: crypto_scalarmult_SCALARBYTES, + crypto_box_PUBLICKEYBYTES: crypto_box_PUBLICKEYBYTES, + crypto_box_SECRETKEYBYTES: crypto_box_SECRETKEYBYTES, + crypto_box_BEFORENMBYTES: crypto_box_BEFORENMBYTES, + crypto_box_NONCEBYTES: crypto_box_NONCEBYTES, + crypto_box_ZEROBYTES: crypto_box_ZEROBYTES, + crypto_box_BOXZEROBYTES: crypto_box_BOXZEROBYTES, + crypto_sign_BYTES: crypto_sign_BYTES, + crypto_sign_PUBLICKEYBYTES: crypto_sign_PUBLICKEYBYTES, + crypto_sign_SECRETKEYBYTES: crypto_sign_SECRETKEYBYTES, + crypto_sign_SEEDBYTES: crypto_sign_SEEDBYTES, + crypto_hash_BYTES: crypto_hash_BYTES +}; + +/* High-level API */ + +function checkLengths(k, n) { + if (k.length !== crypto_secretbox_KEYBYTES) throw new Error('bad key size'); + if (n.length !== crypto_secretbox_NONCEBYTES) throw new Error('bad nonce size'); +} + +function checkBoxLengths(pk, sk) { + if (pk.length !== crypto_box_PUBLICKEYBYTES) throw new Error('bad public key size'); + if (sk.length !== crypto_box_SECRETKEYBYTES) throw new Error('bad secret key size'); +} + +function checkArrayTypes() { + var t, i; + for (i = 0; i < arguments.length; i++) { + if ((t = Object.prototype.toString.call(arguments[i])) !== '[object Uint8Array]') + throw new TypeError('unexpected type ' + t + ', use Uint8Array'); + } +} + +function cleanup(arr) { + for (var i = 0; i < arr.length; i++) arr[i] = 0; +} + +// TODO: Completely remove this in v0.15. +if (!nacl.util) { + nacl.util = {}; + nacl.util.decodeUTF8 = nacl.util.encodeUTF8 = nacl.util.encodeBase64 = nacl.util.decodeBase64 = function() { + throw new Error('nacl.util moved into separate package: https://github.com/dchest/tweetnacl-util-js'); + }; +} + +nacl.randomBytes = function(n) { + var b = new Uint8Array(n); + randombytes(b, n); + return b; +}; + +nacl.secretbox = function(msg, nonce, key) { + checkArrayTypes(msg, nonce, key); + checkLengths(key, nonce); + var m = new Uint8Array(crypto_secretbox_ZEROBYTES + msg.length); + var c = new Uint8Array(m.length); + for (var i = 0; i < msg.length; i++) m[i+crypto_secretbox_ZEROBYTES] = msg[i]; + crypto_secretbox(c, m, m.length, nonce, key); + return c.subarray(crypto_secretbox_BOXZEROBYTES); +}; + +nacl.secretbox.open = function(box, nonce, key) { + checkArrayTypes(box, nonce, key); + checkLengths(key, nonce); + var c = new Uint8Array(crypto_secretbox_BOXZEROBYTES + box.length); + var m = new Uint8Array(c.length); + for (var i = 0; i < box.length; i++) c[i+crypto_secretbox_BOXZEROBYTES] = box[i]; + if (c.length < 32) return false; + if (crypto_secretbox_open(m, c, c.length, nonce, key) !== 0) return false; + return m.subarray(crypto_secretbox_ZEROBYTES); +}; + +nacl.secretbox.keyLength = crypto_secretbox_KEYBYTES; +nacl.secretbox.nonceLength = crypto_secretbox_NONCEBYTES; +nacl.secretbox.overheadLength = crypto_secretbox_BOXZEROBYTES; + +nacl.scalarMult = function(n, p) { + checkArrayTypes(n, p); + if (n.length !== crypto_scalarmult_SCALARBYTES) throw new Error('bad n size'); + if (p.length !== crypto_scalarmult_BYTES) throw new Error('bad p size'); + var q = new Uint8Array(crypto_scalarmult_BYTES); + crypto_scalarmult(q, n, p); + return q; +}; + +nacl.scalarMult.base = function(n) { + checkArrayTypes(n); + if (n.length !== crypto_scalarmult_SCALARBYTES) throw new Error('bad n size'); + var q = new Uint8Array(crypto_scalarmult_BYTES); + crypto_scalarmult_base(q, n); + return q; +}; + +nacl.scalarMult.scalarLength = crypto_scalarmult_SCALARBYTES; +nacl.scalarMult.groupElementLength = crypto_scalarmult_BYTES; + +nacl.box = function(msg, nonce, publicKey, secretKey) { + var k = nacl.box.before(publicKey, secretKey); + return nacl.secretbox(msg, nonce, k); +}; + +nacl.box.before = function(publicKey, secretKey) { + checkArrayTypes(publicKey, secretKey); + checkBoxLengths(publicKey, secretKey); + var k = new Uint8Array(crypto_box_BEFORENMBYTES); + crypto_box_beforenm(k, publicKey, secretKey); + return k; +}; + +nacl.box.after = nacl.secretbox; + +nacl.box.open = function(msg, nonce, publicKey, secretKey) { + var k = nacl.box.before(publicKey, secretKey); + return nacl.secretbox.open(msg, nonce, k); +}; + +nacl.box.open.after = nacl.secretbox.open; + +nacl.box.keyPair = function() { + var pk = new Uint8Array(crypto_box_PUBLICKEYBYTES); + var sk = new Uint8Array(crypto_box_SECRETKEYBYTES); + crypto_box_keypair(pk, sk); + return {publicKey: pk, secretKey: sk}; +}; + +nacl.box.keyPair.fromSecretKey = function(secretKey) { + checkArrayTypes(secretKey); + if (secretKey.length !== crypto_box_SECRETKEYBYTES) + throw new Error('bad secret key size'); + var pk = new Uint8Array(crypto_box_PUBLICKEYBYTES); + crypto_scalarmult_base(pk, secretKey); + return {publicKey: pk, secretKey: new Uint8Array(secretKey)}; +}; + +nacl.box.publicKeyLength = crypto_box_PUBLICKEYBYTES; +nacl.box.secretKeyLength = crypto_box_SECRETKEYBYTES; +nacl.box.sharedKeyLength = crypto_box_BEFORENMBYTES; +nacl.box.nonceLength = crypto_box_NONCEBYTES; +nacl.box.overheadLength = nacl.secretbox.overheadLength; + +nacl.sign = function(msg, secretKey) { + checkArrayTypes(msg, secretKey); + if (secretKey.length !== crypto_sign_SECRETKEYBYTES) + throw new Error('bad secret key size'); + var signedMsg = new Uint8Array(crypto_sign_BYTES+msg.length); + crypto_sign(signedMsg, msg, msg.length, secretKey); + return signedMsg; +}; + +nacl.sign.open = function(signedMsg, publicKey) { + if (arguments.length !== 2) + throw new Error('nacl.sign.open accepts 2 arguments; did you mean to use nacl.sign.detached.verify?'); + checkArrayTypes(signedMsg, publicKey); + if (publicKey.length !== crypto_sign_PUBLICKEYBYTES) + throw new Error('bad public key size'); + var tmp = new Uint8Array(signedMsg.length); + var mlen = crypto_sign_open(tmp, signedMsg, signedMsg.length, publicKey); + if (mlen < 0) return null; + var m = new Uint8Array(mlen); + for (var i = 0; i < m.length; i++) m[i] = tmp[i]; + return m; +}; + +nacl.sign.detached = function(msg, secretKey) { + var signedMsg = nacl.sign(msg, secretKey); + var sig = new Uint8Array(crypto_sign_BYTES); + for (var i = 0; i < sig.length; i++) sig[i] = signedMsg[i]; + return sig; +}; + +nacl.sign.detached.verify = function(msg, sig, publicKey) { + checkArrayTypes(msg, sig, publicKey); + if (sig.length !== crypto_sign_BYTES) + throw new Error('bad signature size'); + if (publicKey.length !== crypto_sign_PUBLICKEYBYTES) + throw new Error('bad public key size'); + var sm = new Uint8Array(crypto_sign_BYTES + msg.length); + var m = new Uint8Array(crypto_sign_BYTES + msg.length); + var i; + for (i = 0; i < crypto_sign_BYTES; i++) sm[i] = sig[i]; + for (i = 0; i < msg.length; i++) sm[i+crypto_sign_BYTES] = msg[i]; + return (crypto_sign_open(m, sm, sm.length, publicKey) >= 0); +}; + +nacl.sign.keyPair = function() { + var pk = new Uint8Array(crypto_sign_PUBLICKEYBYTES); + var sk = new Uint8Array(crypto_sign_SECRETKEYBYTES); + crypto_sign_keypair(pk, sk); + return {publicKey: pk, secretKey: sk}; +}; + +nacl.sign.keyPair.fromSecretKey = function(secretKey) { + checkArrayTypes(secretKey); + if (secretKey.length !== crypto_sign_SECRETKEYBYTES) + throw new Error('bad secret key size'); + var pk = new Uint8Array(crypto_sign_PUBLICKEYBYTES); + for (var i = 0; i < pk.length; i++) pk[i] = secretKey[32+i]; + return {publicKey: pk, secretKey: new Uint8Array(secretKey)}; +}; + +nacl.sign.keyPair.fromSeed = function(seed) { + checkArrayTypes(seed); + if (seed.length !== crypto_sign_SEEDBYTES) + throw new Error('bad seed size'); + var pk = new Uint8Array(crypto_sign_PUBLICKEYBYTES); + var sk = new Uint8Array(crypto_sign_SECRETKEYBYTES); + for (var i = 0; i < 32; i++) sk[i] = seed[i]; + crypto_sign_keypair(pk, sk, true); + return {publicKey: pk, secretKey: sk}; +}; + +nacl.sign.publicKeyLength = crypto_sign_PUBLICKEYBYTES; +nacl.sign.secretKeyLength = crypto_sign_SECRETKEYBYTES; +nacl.sign.seedLength = crypto_sign_SEEDBYTES; +nacl.sign.signatureLength = crypto_sign_BYTES; + +nacl.hash = function(msg) { + checkArrayTypes(msg); + var h = new Uint8Array(crypto_hash_BYTES); + crypto_hash(h, msg, msg.length); + return h; +}; + +nacl.hash.hashLength = crypto_hash_BYTES; + +nacl.verify = function(x, y) { + checkArrayTypes(x, y); + // Zero length arguments are considered not equal. + if (x.length === 0 || y.length === 0) return false; + if (x.length !== y.length) return false; + return (vn(x, 0, y, 0, x.length) === 0) ? true : false; +}; + +nacl.setPRNG = function(fn) { + randombytes = fn; +}; + +(function() { + // Initialize PRNG if environment provides CSPRNG. + // If not, methods calling randombytes will throw. + var crypto = typeof self !== 'undefined' ? (self.crypto || self.msCrypto) : null; + if (crypto && crypto.getRandomValues) { + // Browsers. + var QUOTA = 65536; + nacl.setPRNG(function(x, n) { + var i, v = new Uint8Array(n); + for (i = 0; i < n; i += QUOTA) { + crypto.getRandomValues(v.subarray(i, i + Math.min(n - i, QUOTA))); + } + for (i = 0; i < n; i++) x[i] = v[i]; + cleanup(v); + }); + } else if (true) { + // Node.js. + crypto = __webpack_require__(417); + if (crypto && crypto.randomBytes) { + nacl.setPRNG(function(x, n) { + var i, v = crypto.randomBytes(n); + for (i = 0; i < n; i++) x[i] = v[i]; + cleanup(v); + }); + } + } +})(); + +})( true && module.exports ? module.exports : (self.nacl = self.nacl || {})); + + +/***/ }), + +/***/ 203: +/***/ (function(module) { + +"use strict"; +/*! + * @description Recursive object extending + * @author Viacheslav Lotsmanov + * @license MIT + * + * The MIT License (MIT) + * + * Copyright (c) 2013-2018 Viacheslav Lotsmanov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + + + +function isSpecificValue(val) { + return ( + val instanceof Buffer + || val instanceof Date + || val instanceof RegExp + ) ? true : false; +} + +function cloneSpecificValue(val) { + if (val instanceof Buffer) { + var x = Buffer.alloc + ? Buffer.alloc(val.length) + : new Buffer(val.length); + val.copy(x); + return x; + } else if (val instanceof Date) { + return new Date(val.getTime()); + } else if (val instanceof RegExp) { + return new RegExp(val); + } else { + throw new Error('Unexpected situation'); + } +} + +/** + * Recursive cloning array. + */ +function deepCloneArray(arr) { + var clone = []; + arr.forEach(function (item, index) { + if (typeof item === 'object' && item !== null) { + if (Array.isArray(item)) { + clone[index] = deepCloneArray(item); + } else if (isSpecificValue(item)) { + clone[index] = cloneSpecificValue(item); + } else { + clone[index] = deepExtend({}, item); + } + } else { + clone[index] = item; + } + }); + return clone; +} + +function safeGetProperty(object, property) { + return property === '__proto__' ? undefined : object[property]; +} + +/** + * Extening object that entered in first argument. + * + * Returns extended object or false if have no target object or incorrect type. + * + * If you wish to clone source object (without modify it), just use empty new + * object as first argument, like this: + * deepExtend({}, yourObj_1, [yourObj_N]); + */ +var deepExtend = module.exports = function (/*obj_1, [obj_2], [obj_N]*/) { + if (arguments.length < 1 || typeof arguments[0] !== 'object') { + return false; + } + + if (arguments.length < 2) { + return arguments[0]; + } + + var target = arguments[0]; + + // convert arguments to array and cut off target object + var args = Array.prototype.slice.call(arguments, 1); + + var val, src, clone; + + args.forEach(function (obj) { + // skip argument if isn't an object, is null, or is an array + if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) { + return; + } + + Object.keys(obj).forEach(function (key) { + src = safeGetProperty(target, key); // source value + val = safeGetProperty(obj, key); // new value + + // recursion prevention + if (val === target) { + return; + + /** + * if new value isn't object then just overwrite by new value + * instead of extending. + */ + } else if (typeof val !== 'object' || val === null) { + target[key] = val; + return; + + // just clone arrays (and recursive clone objects inside) + } else if (Array.isArray(val)) { + target[key] = deepCloneArray(val); + return; + + // custom cloning and overwrite for specific objects + } else if (isSpecificValue(val)) { + target[key] = cloneSpecificValue(val); + return; + + // overwrite by new value if source isn't object or array + } else if (typeof src !== 'object' || src === null || Array.isArray(src)) { + target[key] = deepExtend({}, val); + return; + + // source value and new value is objects both, extending... + } else { + target[key] = deepExtend(src, val); + return; + } + }); + }); + + return target; +}; + + +/***/ }), + +/***/ 211: +/***/ (function(module) { + +module.exports = require("https"); + +/***/ }), + +/***/ 213: +/***/ (function(module) { + +module.exports = require("punycode"); + +/***/ }), + +/***/ 215: +/***/ (function(module, __unusedexports, __webpack_require__) { + +"use strict"; +/* eslint-disable node/no-deprecated-api */ + + + +var buffer = __webpack_require__(293) +var Buffer = buffer.Buffer + +var safer = {} + +var key + +for (key in buffer) { + if (!buffer.hasOwnProperty(key)) continue + if (key === 'SlowBuffer' || key === 'Buffer') continue + safer[key] = buffer[key] +} + +var Safer = safer.Buffer = {} +for (key in Buffer) { + if (!Buffer.hasOwnProperty(key)) continue + if (key === 'allocUnsafe' || key === 'allocUnsafeSlow') continue + Safer[key] = Buffer[key] +} + +safer.Buffer.prototype = Buffer.prototype + +if (!Safer.from || Safer.from === Uint8Array.from) { + Safer.from = function (value, encodingOrOffset, length) { + if (typeof value === 'number') { + throw new TypeError('The "value" argument must not be of type number. Received type ' + typeof value) + } + if (value && typeof value.length === 'undefined') { + throw new TypeError('The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type ' + typeof value) + } + return Buffer(value, encodingOrOffset, length) + } +} + +if (!Safer.alloc) { + Safer.alloc = function (size, fill, encoding) { + if (typeof size !== 'number') { + throw new TypeError('The "size" argument must be of type number. Received type ' + typeof size) + } + if (size < 0 || size >= 2 * (1 << 30)) { + throw new RangeError('The value "' + size + '" is invalid for option "size"') + } + var buf = Buffer(size) + if (!fill || fill.length === 0) { + buf.fill(0) + } else if (typeof encoding === 'string') { + buf.fill(fill, encoding) + } else { + buf.fill(fill) + } + return buf + } +} + +if (!safer.kStringMaxLength) { + try { + safer.kStringMaxLength = process.binding('buffer').kStringMaxLength + } catch (e) { + // we can't determine kStringMaxLength in environments where process.binding + // is unsupported, so let's not set it + } +} + +if (!safer.constants) { + safer.constants = { + MAX_LENGTH: safer.kMaxLength + } + if (safer.kStringMaxLength) { + safer.constants.MAX_STRING_LENGTH = safer.kStringMaxLength + } +} + +module.exports = safer + + +/***/ }), + +/***/ 222: +/***/ (function(module) { + +module.exports = {"$id":"browser.json#","$schema":"http://json-schema.org/draft-06/schema#","type":"object","required":["name","version"],"properties":{"name":{"type":"string"},"version":{"type":"string"},"comment":{"type":"string"}}}; + +/***/ }), + +/***/ 226: +/***/ (function(module) { + +module.exports = {"$id":"response.json#","$schema":"http://json-schema.org/draft-06/schema#","type":"object","required":["status","statusText","httpVersion","cookies","headers","content","redirectURL","headersSize","bodySize"],"properties":{"status":{"type":"integer"},"statusText":{"type":"string"},"httpVersion":{"type":"string"},"cookies":{"type":"array","items":{"$ref":"cookie.json#"}},"headers":{"type":"array","items":{"$ref":"header.json#"}},"content":{"$ref":"content.json#"},"redirectURL":{"type":"string"},"headersSize":{"type":"integer"},"bodySize":{"type":"integer"},"comment":{"type":"string"}}}; + +/***/ }), + +/***/ 233: +/***/ (function(module) { + +"use strict"; + +module.exports = function generate_dependencies(it, $keyword, $ruleType) { + var out = ' '; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $data = 'data' + ($dataLvl || ''); + var $errs = 'errs__' + $lvl; + var $it = it.util.copy(it); + var $closingBraces = ''; + $it.level++; + var $nextValid = 'valid' + $it.level; + var $schemaDeps = {}, + $propertyDeps = {}, + $ownProperties = it.opts.ownProperties; + for ($property in $schema) { + var $sch = $schema[$property]; + var $deps = Array.isArray($sch) ? $propertyDeps : $schemaDeps; + $deps[$property] = $sch; + } + out += 'var ' + ($errs) + ' = errors;'; + var $currentErrorPath = it.errorPath; + out += 'var missing' + ($lvl) + ';'; + for (var $property in $propertyDeps) { + $deps = $propertyDeps[$property]; + if ($deps.length) { + out += ' if ( ' + ($data) + (it.util.getProperty($property)) + ' !== undefined '; + if ($ownProperties) { + out += ' && Object.prototype.hasOwnProperty.call(' + ($data) + ', \'' + (it.util.escapeQuotes($property)) + '\') '; + } + if ($breakOnError) { + out += ' && ( '; + var arr1 = $deps; + if (arr1) { + var $propertyKey, $i = -1, + l1 = arr1.length - 1; + while ($i < l1) { + $propertyKey = arr1[$i += 1]; + if ($i) { + out += ' || '; + } + var $prop = it.util.getProperty($propertyKey), + $useData = $data + $prop; + out += ' ( ( ' + ($useData) + ' === undefined '; + if ($ownProperties) { + out += ' || ! Object.prototype.hasOwnProperty.call(' + ($data) + ', \'' + (it.util.escapeQuotes($propertyKey)) + '\') '; + } + out += ') && (missing' + ($lvl) + ' = ' + (it.util.toQuotedString(it.opts.jsonPointers ? $propertyKey : $prop)) + ') ) '; + } + } + out += ')) { '; + var $propertyPath = 'missing' + $lvl, + $missingProperty = '\' + ' + $propertyPath + ' + \''; + if (it.opts._errorDataPathProperty) { + it.errorPath = it.opts.jsonPointers ? it.util.getPathExpr($currentErrorPath, $propertyPath, true) : $currentErrorPath + ' + ' + $propertyPath; + } + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('dependencies') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { property: \'' + (it.util.escapeQuotes($property)) + '\', missingProperty: \'' + ($missingProperty) + '\', depsCount: ' + ($deps.length) + ', deps: \'' + (it.util.escapeQuotes($deps.length == 1 ? $deps[0] : $deps.join(", "))) + '\' } '; + if (it.opts.messages !== false) { + out += ' , message: \'should have '; + if ($deps.length == 1) { + out += 'property ' + (it.util.escapeQuotes($deps[0])); + } else { + out += 'properties ' + (it.util.escapeQuotes($deps.join(", "))); + } + out += ' when property ' + (it.util.escapeQuotes($property)) + ' is present\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + } else { + out += ' ) { '; + var arr2 = $deps; + if (arr2) { + var $propertyKey, i2 = -1, + l2 = arr2.length - 1; + while (i2 < l2) { + $propertyKey = arr2[i2 += 1]; + var $prop = it.util.getProperty($propertyKey), + $missingProperty = it.util.escapeQuotes($propertyKey), + $useData = $data + $prop; + if (it.opts._errorDataPathProperty) { + it.errorPath = it.util.getPath($currentErrorPath, $propertyKey, it.opts.jsonPointers); + } + out += ' if ( ' + ($useData) + ' === undefined '; + if ($ownProperties) { + out += ' || ! Object.prototype.hasOwnProperty.call(' + ($data) + ', \'' + (it.util.escapeQuotes($propertyKey)) + '\') '; + } + out += ') { var err = '; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('dependencies') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { property: \'' + (it.util.escapeQuotes($property)) + '\', missingProperty: \'' + ($missingProperty) + '\', depsCount: ' + ($deps.length) + ', deps: \'' + (it.util.escapeQuotes($deps.length == 1 ? $deps[0] : $deps.join(", "))) + '\' } '; + if (it.opts.messages !== false) { + out += ' , message: \'should have '; + if ($deps.length == 1) { + out += 'property ' + (it.util.escapeQuotes($deps[0])); + } else { + out += 'properties ' + (it.util.escapeQuotes($deps.join(", "))); + } + out += ' when property ' + (it.util.escapeQuotes($property)) + ' is present\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + out += '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; } '; + } + } + } + out += ' } '; + if ($breakOnError) { + $closingBraces += '}'; + out += ' else { '; + } + } + } + it.errorPath = $currentErrorPath; + var $currentBaseId = $it.baseId; + for (var $property in $schemaDeps) { + var $sch = $schemaDeps[$property]; + if ((it.opts.strictKeywords ? typeof $sch == 'object' && Object.keys($sch).length > 0 : it.util.schemaHasRules($sch, it.RULES.all))) { + out += ' ' + ($nextValid) + ' = true; if ( ' + ($data) + (it.util.getProperty($property)) + ' !== undefined '; + if ($ownProperties) { + out += ' && Object.prototype.hasOwnProperty.call(' + ($data) + ', \'' + (it.util.escapeQuotes($property)) + '\') '; + } + out += ') { '; + $it.schema = $sch; + $it.schemaPath = $schemaPath + it.util.getProperty($property); + $it.errSchemaPath = $errSchemaPath + '/' + it.util.escapeFragment($property); + out += ' ' + (it.validate($it)) + ' '; + $it.baseId = $currentBaseId; + out += ' } '; + if ($breakOnError) { + out += ' if (' + ($nextValid) + ') { '; + $closingBraces += '}'; + } + } + } + if ($breakOnError) { + out += ' ' + ($closingBraces) + ' if (' + ($errs) + ' == errors) {'; + } + out = it.util.cleanUpCode(out); + return out; +} + + +/***/ }), + +/***/ 241: +/***/ (function(module, __unusedexports, __webpack_require__) { + +// Copyright 2018 Joyent, Inc. + +module.exports = { + read: read, + write: write +}; + +var assert = __webpack_require__(477); +var Buffer = __webpack_require__(215).Buffer; +var utils = __webpack_require__(270); +var Key = __webpack_require__(852); +var PrivateKey = __webpack_require__(502); + +var pem = __webpack_require__(268); +var ssh = __webpack_require__(603); +var rfc4253 = __webpack_require__(538); +var dnssec = __webpack_require__(982); +var putty = __webpack_require__(624); + +var DNSSEC_PRIVKEY_HEADER_PREFIX = 'Private-key-format: v1'; + +function read(buf, options) { + if (typeof (buf) === 'string') { + if (buf.trim().match(/^[-]+[ ]*BEGIN/)) + return (pem.read(buf, options)); + if (buf.match(/^\s*ssh-[a-z]/)) + return (ssh.read(buf, options)); + if (buf.match(/^\s*ecdsa-/)) + return (ssh.read(buf, options)); + if (buf.match(/^putty-user-key-file-2:/i)) + return (putty.read(buf, options)); + if (findDNSSECHeader(buf)) + return (dnssec.read(buf, options)); + buf = Buffer.from(buf, 'binary'); + } else { + assert.buffer(buf); + if (findPEMHeader(buf)) + return (pem.read(buf, options)); + if (findSSHHeader(buf)) + return (ssh.read(buf, options)); + if (findPuTTYHeader(buf)) + return (putty.read(buf, options)); + if (findDNSSECHeader(buf)) + return (dnssec.read(buf, options)); + } + if (buf.readUInt32BE(0) < buf.length) + return (rfc4253.read(buf, options)); + throw (new Error('Failed to auto-detect format of key')); +} + +function findPuTTYHeader(buf) { + var offset = 0; + while (offset < buf.length && + (buf[offset] === 32 || buf[offset] === 10 || buf[offset] === 9)) + ++offset; + if (offset + 22 <= buf.length && + buf.slice(offset, offset + 22).toString('ascii').toLowerCase() === + 'putty-user-key-file-2:') + return (true); + return (false); +} + +function findSSHHeader(buf) { + var offset = 0; + while (offset < buf.length && + (buf[offset] === 32 || buf[offset] === 10 || buf[offset] === 9)) + ++offset; + if (offset + 4 <= buf.length && + buf.slice(offset, offset + 4).toString('ascii') === 'ssh-') + return (true); + if (offset + 6 <= buf.length && + buf.slice(offset, offset + 6).toString('ascii') === 'ecdsa-') + return (true); + return (false); +} + +function findPEMHeader(buf) { + var offset = 0; + while (offset < buf.length && + (buf[offset] === 32 || buf[offset] === 10)) + ++offset; + if (buf[offset] !== 45) + return (false); + while (offset < buf.length && + (buf[offset] === 45)) + ++offset; + while (offset < buf.length && + (buf[offset] === 32)) + ++offset; + if (offset + 5 > buf.length || + buf.slice(offset, offset + 5).toString('ascii') !== 'BEGIN') + return (false); + return (true); +} + +function findDNSSECHeader(buf) { + // private case first + if (buf.length <= DNSSEC_PRIVKEY_HEADER_PREFIX.length) + return (false); + var headerCheck = buf.slice(0, DNSSEC_PRIVKEY_HEADER_PREFIX.length); + if (headerCheck.toString('ascii') === DNSSEC_PRIVKEY_HEADER_PREFIX) + return (true); + + // public-key RFC3110 ? + // 'domain.com. IN KEY ...' or 'domain.com. IN DNSKEY ...' + // skip any comment-lines + if (typeof (buf) !== 'string') { + buf = buf.toString('ascii'); + } + var lines = buf.split('\n'); + var line = 0; + /* JSSTYLED */ + while (lines[line].match(/^\;/)) + line++; + if (lines[line].toString('ascii').match(/\. IN KEY /)) + return (true); + if (lines[line].toString('ascii').match(/\. IN DNSKEY /)) + return (true); + return (false); +} + +function write(key, options) { + throw (new Error('"auto" format cannot be used for writing')); +} + + +/***/ }), + +/***/ 242: +/***/ (function(module, exports) { + +(function(){ + + // Copyright (c) 2005 Tom Wu + // All Rights Reserved. + // See "LICENSE" for details. + + // Basic JavaScript BN library - subset useful for RSA encryption. + + // Bits per digit + var dbits; + + // JavaScript engine analysis + var canary = 0xdeadbeefcafe; + var j_lm = ((canary&0xffffff)==0xefcafe); + + // (public) Constructor + function BigInteger(a,b,c) { + if(a != null) + if("number" == typeof a) this.fromNumber(a,b,c); + else if(b == null && "string" != typeof a) this.fromString(a,256); + else this.fromString(a,b); + } + + // return new, unset BigInteger + function nbi() { return new BigInteger(null); } + + // am: Compute w_j += (x*this_i), propagate carries, + // c is initial carry, returns final carry. + // c < 3*dvalue, x < 2*dvalue, this_i < dvalue + // We need to select the fastest one that works in this environment. + + // am1: use a single mult and divide to get the high bits, + // max digit bits should be 26 because + // max internal value = 2*dvalue^2-2*dvalue (< 2^53) + function am1(i,x,w,j,c,n) { + while(--n >= 0) { + var v = x*this[i++]+w[j]+c; + c = Math.floor(v/0x4000000); + w[j++] = v&0x3ffffff; + } + return c; + } + // am2 avoids a big mult-and-extract completely. + // Max digit bits should be <= 30 because we do bitwise ops + // on values up to 2*hdvalue^2-hdvalue-1 (< 2^31) + function am2(i,x,w,j,c,n) { + var xl = x&0x7fff, xh = x>>15; + while(--n >= 0) { + var l = this[i]&0x7fff; + var h = this[i++]>>15; + var m = xh*l+h*xl; + l = xl*l+((m&0x7fff)<<15)+w[j]+(c&0x3fffffff); + c = (l>>>30)+(m>>>15)+xh*h+(c>>>30); + w[j++] = l&0x3fffffff; + } + return c; + } + // Alternately, set max digit bits to 28 since some + // browsers slow down when dealing with 32-bit numbers. + function am3(i,x,w,j,c,n) { + var xl = x&0x3fff, xh = x>>14; + while(--n >= 0) { + var l = this[i]&0x3fff; + var h = this[i++]>>14; + var m = xh*l+h*xl; + l = xl*l+((m&0x3fff)<<14)+w[j]+c; + c = (l>>28)+(m>>14)+xh*h; + w[j++] = l&0xfffffff; + } + return c; + } + var inBrowser = typeof navigator !== "undefined"; + if(inBrowser && j_lm && (navigator.appName == "Microsoft Internet Explorer")) { + BigInteger.prototype.am = am2; + dbits = 30; + } + else if(inBrowser && j_lm && (navigator.appName != "Netscape")) { + BigInteger.prototype.am = am1; + dbits = 26; + } + else { // Mozilla/Netscape seems to prefer am3 + BigInteger.prototype.am = am3; + dbits = 28; + } + + BigInteger.prototype.DB = dbits; + BigInteger.prototype.DM = ((1<= 0; --i) r[i] = this[i]; + r.t = this.t; + r.s = this.s; + } + + // (protected) set from integer value x, -DV <= x < DV + function bnpFromInt(x) { + this.t = 1; + this.s = (x<0)?-1:0; + if(x > 0) this[0] = x; + else if(x < -1) this[0] = x+this.DV; + else this.t = 0; + } + + // return bigint initialized to value + function nbv(i) { var r = nbi(); r.fromInt(i); return r; } + + // (protected) set from string and radix + function bnpFromString(s,b) { + var k; + if(b == 16) k = 4; + else if(b == 8) k = 3; + else if(b == 256) k = 8; // byte array + else if(b == 2) k = 1; + else if(b == 32) k = 5; + else if(b == 4) k = 2; + else { this.fromRadix(s,b); return; } + this.t = 0; + this.s = 0; + var i = s.length, mi = false, sh = 0; + while(--i >= 0) { + var x = (k==8)?s[i]&0xff:intAt(s,i); + if(x < 0) { + if(s.charAt(i) == "-") mi = true; + continue; + } + mi = false; + if(sh == 0) + this[this.t++] = x; + else if(sh+k > this.DB) { + this[this.t-1] |= (x&((1<<(this.DB-sh))-1))<>(this.DB-sh)); + } + else + this[this.t-1] |= x<= this.DB) sh -= this.DB; + } + if(k == 8 && (s[0]&0x80) != 0) { + this.s = -1; + if(sh > 0) this[this.t-1] |= ((1<<(this.DB-sh))-1)< 0 && this[this.t-1] == c) --this.t; + } + + // (public) return string representation in given radix + function bnToString(b) { + if(this.s < 0) return "-"+this.negate().toString(b); + var k; + if(b == 16) k = 4; + else if(b == 8) k = 3; + else if(b == 2) k = 1; + else if(b == 32) k = 5; + else if(b == 4) k = 2; + else return this.toRadix(b); + var km = (1< 0) { + if(p < this.DB && (d = this[i]>>p) > 0) { m = true; r = int2char(d); } + while(i >= 0) { + if(p < k) { + d = (this[i]&((1<>(p+=this.DB-k); + } + else { + d = (this[i]>>(p-=k))&km; + if(p <= 0) { p += this.DB; --i; } + } + if(d > 0) m = true; + if(m) r += int2char(d); + } + } + return m?r:"0"; + } + + // (public) -this + function bnNegate() { var r = nbi(); BigInteger.ZERO.subTo(this,r); return r; } + + // (public) |this| + function bnAbs() { return (this.s<0)?this.negate():this; } + + // (public) return + if this > a, - if this < a, 0 if equal + function bnCompareTo(a) { + var r = this.s-a.s; + if(r != 0) return r; + var i = this.t; + r = i-a.t; + if(r != 0) return (this.s<0)?-r:r; + while(--i >= 0) if((r=this[i]-a[i]) != 0) return r; + return 0; + } + + // returns bit length of the integer x + function nbits(x) { + var r = 1, t; + if((t=x>>>16) != 0) { x = t; r += 16; } + if((t=x>>8) != 0) { x = t; r += 8; } + if((t=x>>4) != 0) { x = t; r += 4; } + if((t=x>>2) != 0) { x = t; r += 2; } + if((t=x>>1) != 0) { x = t; r += 1; } + return r; + } + + // (public) return the number of bits in "this" + function bnBitLength() { + if(this.t <= 0) return 0; + return this.DB*(this.t-1)+nbits(this[this.t-1]^(this.s&this.DM)); + } + + // (protected) r = this << n*DB + function bnpDLShiftTo(n,r) { + var i; + for(i = this.t-1; i >= 0; --i) r[i+n] = this[i]; + for(i = n-1; i >= 0; --i) r[i] = 0; + r.t = this.t+n; + r.s = this.s; + } + + // (protected) r = this >> n*DB + function bnpDRShiftTo(n,r) { + for(var i = n; i < this.t; ++i) r[i-n] = this[i]; + r.t = Math.max(this.t-n,0); + r.s = this.s; + } + + // (protected) r = this << n + function bnpLShiftTo(n,r) { + var bs = n%this.DB; + var cbs = this.DB-bs; + var bm = (1<= 0; --i) { + r[i+ds+1] = (this[i]>>cbs)|c; + c = (this[i]&bm)<= 0; --i) r[i] = 0; + r[ds] = c; + r.t = this.t+ds+1; + r.s = this.s; + r.clamp(); + } + + // (protected) r = this >> n + function bnpRShiftTo(n,r) { + r.s = this.s; + var ds = Math.floor(n/this.DB); + if(ds >= this.t) { r.t = 0; return; } + var bs = n%this.DB; + var cbs = this.DB-bs; + var bm = (1<>bs; + for(var i = ds+1; i < this.t; ++i) { + r[i-ds-1] |= (this[i]&bm)<>bs; + } + if(bs > 0) r[this.t-ds-1] |= (this.s&bm)<>= this.DB; + } + if(a.t < this.t) { + c -= a.s; + while(i < this.t) { + c += this[i]; + r[i++] = c&this.DM; + c >>= this.DB; + } + c += this.s; + } + else { + c += this.s; + while(i < a.t) { + c -= a[i]; + r[i++] = c&this.DM; + c >>= this.DB; + } + c -= a.s; + } + r.s = (c<0)?-1:0; + if(c < -1) r[i++] = this.DV+c; + else if(c > 0) r[i++] = c; + r.t = i; + r.clamp(); + } + + // (protected) r = this * a, r != this,a (HAC 14.12) + // "this" should be the larger one if appropriate. + function bnpMultiplyTo(a,r) { + var x = this.abs(), y = a.abs(); + var i = x.t; + r.t = i+y.t; + while(--i >= 0) r[i] = 0; + for(i = 0; i < y.t; ++i) r[i+x.t] = x.am(0,y[i],r,i,0,x.t); + r.s = 0; + r.clamp(); + if(this.s != a.s) BigInteger.ZERO.subTo(r,r); + } + + // (protected) r = this^2, r != this (HAC 14.16) + function bnpSquareTo(r) { + var x = this.abs(); + var i = r.t = 2*x.t; + while(--i >= 0) r[i] = 0; + for(i = 0; i < x.t-1; ++i) { + var c = x.am(i,x[i],r,2*i,0,1); + if((r[i+x.t]+=x.am(i+1,2*x[i],r,2*i+1,c,x.t-i-1)) >= x.DV) { + r[i+x.t] -= x.DV; + r[i+x.t+1] = 1; + } + } + if(r.t > 0) r[r.t-1] += x.am(i,x[i],r,2*i,0,1); + r.s = 0; + r.clamp(); + } + + // (protected) divide this by m, quotient and remainder to q, r (HAC 14.20) + // r != q, this != m. q or r may be null. + function bnpDivRemTo(m,q,r) { + var pm = m.abs(); + if(pm.t <= 0) return; + var pt = this.abs(); + if(pt.t < pm.t) { + if(q != null) q.fromInt(0); + if(r != null) this.copyTo(r); + return; + } + if(r == null) r = nbi(); + var y = nbi(), ts = this.s, ms = m.s; + var nsh = this.DB-nbits(pm[pm.t-1]); // normalize modulus + if(nsh > 0) { pm.lShiftTo(nsh,y); pt.lShiftTo(nsh,r); } + else { pm.copyTo(y); pt.copyTo(r); } + var ys = y.t; + var y0 = y[ys-1]; + if(y0 == 0) return; + var yt = y0*(1<1)?y[ys-2]>>this.F2:0); + var d1 = this.FV/yt, d2 = (1<= 0) { + r[r.t++] = 1; + r.subTo(t,r); + } + BigInteger.ONE.dlShiftTo(ys,t); + t.subTo(y,y); // "negative" y so we can replace sub with am later + while(y.t < ys) y[y.t++] = 0; + while(--j >= 0) { + // Estimate quotient digit + var qd = (r[--i]==y0)?this.DM:Math.floor(r[i]*d1+(r[i-1]+e)*d2); + if((r[i]+=y.am(0,qd,r,j,0,ys)) < qd) { // Try it out + y.dlShiftTo(j,t); + r.subTo(t,r); + while(r[i] < --qd) r.subTo(t,r); + } + } + if(q != null) { + r.drShiftTo(ys,q); + if(ts != ms) BigInteger.ZERO.subTo(q,q); + } + r.t = ys; + r.clamp(); + if(nsh > 0) r.rShiftTo(nsh,r); // Denormalize remainder + if(ts < 0) BigInteger.ZERO.subTo(r,r); + } + + // (public) this mod a + function bnMod(a) { + var r = nbi(); + this.abs().divRemTo(a,null,r); + if(this.s < 0 && r.compareTo(BigInteger.ZERO) > 0) a.subTo(r,r); + return r; + } + + // Modular reduction using "classic" algorithm + function Classic(m) { this.m = m; } + function cConvert(x) { + if(x.s < 0 || x.compareTo(this.m) >= 0) return x.mod(this.m); + else return x; + } + function cRevert(x) { return x; } + function cReduce(x) { x.divRemTo(this.m,null,x); } + function cMulTo(x,y,r) { x.multiplyTo(y,r); this.reduce(r); } + function cSqrTo(x,r) { x.squareTo(r); this.reduce(r); } + + Classic.prototype.convert = cConvert; + Classic.prototype.revert = cRevert; + Classic.prototype.reduce = cReduce; + Classic.prototype.mulTo = cMulTo; + Classic.prototype.sqrTo = cSqrTo; + + // (protected) return "-1/this % 2^DB"; useful for Mont. reduction + // justification: + // xy == 1 (mod m) + // xy = 1+km + // xy(2-xy) = (1+km)(1-km) + // x[y(2-xy)] = 1-k^2m^2 + // x[y(2-xy)] == 1 (mod m^2) + // if y is 1/x mod m, then y(2-xy) is 1/x mod m^2 + // should reduce x and y(2-xy) by m^2 at each step to keep size bounded. + // JS multiply "overflows" differently from C/C++, so care is needed here. + function bnpInvDigit() { + if(this.t < 1) return 0; + var x = this[0]; + if((x&1) == 0) return 0; + var y = x&3; // y == 1/x mod 2^2 + y = (y*(2-(x&0xf)*y))&0xf; // y == 1/x mod 2^4 + y = (y*(2-(x&0xff)*y))&0xff; // y == 1/x mod 2^8 + y = (y*(2-(((x&0xffff)*y)&0xffff)))&0xffff; // y == 1/x mod 2^16 + // last step - calculate inverse mod DV directly; + // assumes 16 < DB <= 32 and assumes ability to handle 48-bit ints + y = (y*(2-x*y%this.DV))%this.DV; // y == 1/x mod 2^dbits + // we really want the negative inverse, and -DV < y < DV + return (y>0)?this.DV-y:-y; + } + + // Montgomery reduction + function Montgomery(m) { + this.m = m; + this.mp = m.invDigit(); + this.mpl = this.mp&0x7fff; + this.mph = this.mp>>15; + this.um = (1<<(m.DB-15))-1; + this.mt2 = 2*m.t; + } + + // xR mod m + function montConvert(x) { + var r = nbi(); + x.abs().dlShiftTo(this.m.t,r); + r.divRemTo(this.m,null,r); + if(x.s < 0 && r.compareTo(BigInteger.ZERO) > 0) this.m.subTo(r,r); + return r; + } + + // x/R mod m + function montRevert(x) { + var r = nbi(); + x.copyTo(r); + this.reduce(r); + return r; + } + + // x = x/R mod m (HAC 14.32) + function montReduce(x) { + while(x.t <= this.mt2) // pad x so am has enough room later + x[x.t++] = 0; + for(var i = 0; i < this.m.t; ++i) { + // faster way of calculating u0 = x[i]*mp mod DV + var j = x[i]&0x7fff; + var u0 = (j*this.mpl+(((j*this.mph+(x[i]>>15)*this.mpl)&this.um)<<15))&x.DM; + // use am to combine the multiply-shift-add into one call + j = i+this.m.t; + x[j] += this.m.am(0,u0,x,i,0,this.m.t); + // propagate carry + while(x[j] >= x.DV) { x[j] -= x.DV; x[++j]++; } + } + x.clamp(); + x.drShiftTo(this.m.t,x); + if(x.compareTo(this.m) >= 0) x.subTo(this.m,x); + } + + // r = "x^2/R mod m"; x != r + function montSqrTo(x,r) { x.squareTo(r); this.reduce(r); } + + // r = "xy/R mod m"; x,y != r + function montMulTo(x,y,r) { x.multiplyTo(y,r); this.reduce(r); } + + Montgomery.prototype.convert = montConvert; + Montgomery.prototype.revert = montRevert; + Montgomery.prototype.reduce = montReduce; + Montgomery.prototype.mulTo = montMulTo; + Montgomery.prototype.sqrTo = montSqrTo; + + // (protected) true iff this is even + function bnpIsEven() { return ((this.t>0)?(this[0]&1):this.s) == 0; } + + // (protected) this^e, e < 2^32, doing sqr and mul with "r" (HAC 14.79) + function bnpExp(e,z) { + if(e > 0xffffffff || e < 1) return BigInteger.ONE; + var r = nbi(), r2 = nbi(), g = z.convert(this), i = nbits(e)-1; + g.copyTo(r); + while(--i >= 0) { + z.sqrTo(r,r2); + if((e&(1< 0) z.mulTo(r2,g,r); + else { var t = r; r = r2; r2 = t; } + } + return z.revert(r); + } + + // (public) this^e % m, 0 <= e < 2^32 + function bnModPowInt(e,m) { + var z; + if(e < 256 || m.isEven()) z = new Classic(m); else z = new Montgomery(m); + return this.exp(e,z); + } + + // protected + BigInteger.prototype.copyTo = bnpCopyTo; + BigInteger.prototype.fromInt = bnpFromInt; + BigInteger.prototype.fromString = bnpFromString; + BigInteger.prototype.clamp = bnpClamp; + BigInteger.prototype.dlShiftTo = bnpDLShiftTo; + BigInteger.prototype.drShiftTo = bnpDRShiftTo; + BigInteger.prototype.lShiftTo = bnpLShiftTo; + BigInteger.prototype.rShiftTo = bnpRShiftTo; + BigInteger.prototype.subTo = bnpSubTo; + BigInteger.prototype.multiplyTo = bnpMultiplyTo; + BigInteger.prototype.squareTo = bnpSquareTo; + BigInteger.prototype.divRemTo = bnpDivRemTo; + BigInteger.prototype.invDigit = bnpInvDigit; + BigInteger.prototype.isEven = bnpIsEven; + BigInteger.prototype.exp = bnpExp; + + // public + BigInteger.prototype.toString = bnToString; + BigInteger.prototype.negate = bnNegate; + BigInteger.prototype.abs = bnAbs; + BigInteger.prototype.compareTo = bnCompareTo; + BigInteger.prototype.bitLength = bnBitLength; + BigInteger.prototype.mod = bnMod; + BigInteger.prototype.modPowInt = bnModPowInt; + + // "constants" + BigInteger.ZERO = nbv(0); + BigInteger.ONE = nbv(1); + + // Copyright (c) 2005-2009 Tom Wu + // All Rights Reserved. + // See "LICENSE" for details. + + // Extended JavaScript BN functions, required for RSA private ops. + + // Version 1.1: new BigInteger("0", 10) returns "proper" zero + // Version 1.2: square() API, isProbablePrime fix + + // (public) + function bnClone() { var r = nbi(); this.copyTo(r); return r; } + + // (public) return value as integer + function bnIntValue() { + if(this.s < 0) { + if(this.t == 1) return this[0]-this.DV; + else if(this.t == 0) return -1; + } + else if(this.t == 1) return this[0]; + else if(this.t == 0) return 0; + // assumes 16 < DB < 32 + return ((this[1]&((1<<(32-this.DB))-1))<>24; } + + // (public) return value as short (assumes DB>=16) + function bnShortValue() { return (this.t==0)?this.s:(this[0]<<16)>>16; } + + // (protected) return x s.t. r^x < DV + function bnpChunkSize(r) { return Math.floor(Math.LN2*this.DB/Math.log(r)); } + + // (public) 0 if this == 0, 1 if this > 0 + function bnSigNum() { + if(this.s < 0) return -1; + else if(this.t <= 0 || (this.t == 1 && this[0] <= 0)) return 0; + else return 1; + } + + // (protected) convert to radix string + function bnpToRadix(b) { + if(b == null) b = 10; + if(this.signum() == 0 || b < 2 || b > 36) return "0"; + var cs = this.chunkSize(b); + var a = Math.pow(b,cs); + var d = nbv(a), y = nbi(), z = nbi(), r = ""; + this.divRemTo(d,y,z); + while(y.signum() > 0) { + r = (a+z.intValue()).toString(b).substr(1) + r; + y.divRemTo(d,y,z); + } + return z.intValue().toString(b) + r; + } + + // (protected) convert from radix string + function bnpFromRadix(s,b) { + this.fromInt(0); + if(b == null) b = 10; + var cs = this.chunkSize(b); + var d = Math.pow(b,cs), mi = false, j = 0, w = 0; + for(var i = 0; i < s.length; ++i) { + var x = intAt(s,i); + if(x < 0) { + if(s.charAt(i) == "-" && this.signum() == 0) mi = true; + continue; + } + w = b*w+x; + if(++j >= cs) { + this.dMultiply(d); + this.dAddOffset(w,0); + j = 0; + w = 0; + } + } + if(j > 0) { + this.dMultiply(Math.pow(b,j)); + this.dAddOffset(w,0); + } + if(mi) BigInteger.ZERO.subTo(this,this); + } + + // (protected) alternate constructor + function bnpFromNumber(a,b,c) { + if("number" == typeof b) { + // new BigInteger(int,int,RNG) + if(a < 2) this.fromInt(1); + else { + this.fromNumber(a,c); + if(!this.testBit(a-1)) // force MSB set + this.bitwiseTo(BigInteger.ONE.shiftLeft(a-1),op_or,this); + if(this.isEven()) this.dAddOffset(1,0); // force odd + while(!this.isProbablePrime(b)) { + this.dAddOffset(2,0); + if(this.bitLength() > a) this.subTo(BigInteger.ONE.shiftLeft(a-1),this); + } + } + } + else { + // new BigInteger(int,RNG) + var x = new Array(), t = a&7; + x.length = (a>>3)+1; + b.nextBytes(x); + if(t > 0) x[0] &= ((1< 0) { + if(p < this.DB && (d = this[i]>>p) != (this.s&this.DM)>>p) + r[k++] = d|(this.s<<(this.DB-p)); + while(i >= 0) { + if(p < 8) { + d = (this[i]&((1<>(p+=this.DB-8); + } + else { + d = (this[i]>>(p-=8))&0xff; + if(p <= 0) { p += this.DB; --i; } + } + if((d&0x80) != 0) d |= -256; + if(k == 0 && (this.s&0x80) != (d&0x80)) ++k; + if(k > 0 || d != this.s) r[k++] = d; + } + } + return r; + } + + function bnEquals(a) { return(this.compareTo(a)==0); } + function bnMin(a) { return(this.compareTo(a)<0)?this:a; } + function bnMax(a) { return(this.compareTo(a)>0)?this:a; } + + // (protected) r = this op a (bitwise) + function bnpBitwiseTo(a,op,r) { + var i, f, m = Math.min(a.t,this.t); + for(i = 0; i < m; ++i) r[i] = op(this[i],a[i]); + if(a.t < this.t) { + f = a.s&this.DM; + for(i = m; i < this.t; ++i) r[i] = op(this[i],f); + r.t = this.t; + } + else { + f = this.s&this.DM; + for(i = m; i < a.t; ++i) r[i] = op(f,a[i]); + r.t = a.t; + } + r.s = op(this.s,a.s); + r.clamp(); + } + + // (public) this & a + function op_and(x,y) { return x&y; } + function bnAnd(a) { var r = nbi(); this.bitwiseTo(a,op_and,r); return r; } + + // (public) this | a + function op_or(x,y) { return x|y; } + function bnOr(a) { var r = nbi(); this.bitwiseTo(a,op_or,r); return r; } + + // (public) this ^ a + function op_xor(x,y) { return x^y; } + function bnXor(a) { var r = nbi(); this.bitwiseTo(a,op_xor,r); return r; } + + // (public) this & ~a + function op_andnot(x,y) { return x&~y; } + function bnAndNot(a) { var r = nbi(); this.bitwiseTo(a,op_andnot,r); return r; } + + // (public) ~this + function bnNot() { + var r = nbi(); + for(var i = 0; i < this.t; ++i) r[i] = this.DM&~this[i]; + r.t = this.t; + r.s = ~this.s; + return r; + } + + // (public) this << n + function bnShiftLeft(n) { + var r = nbi(); + if(n < 0) this.rShiftTo(-n,r); else this.lShiftTo(n,r); + return r; + } + + // (public) this >> n + function bnShiftRight(n) { + var r = nbi(); + if(n < 0) this.lShiftTo(-n,r); else this.rShiftTo(n,r); + return r; + } + + // return index of lowest 1-bit in x, x < 2^31 + function lbit(x) { + if(x == 0) return -1; + var r = 0; + if((x&0xffff) == 0) { x >>= 16; r += 16; } + if((x&0xff) == 0) { x >>= 8; r += 8; } + if((x&0xf) == 0) { x >>= 4; r += 4; } + if((x&3) == 0) { x >>= 2; r += 2; } + if((x&1) == 0) ++r; + return r; + } + + // (public) returns index of lowest 1-bit (or -1 if none) + function bnGetLowestSetBit() { + for(var i = 0; i < this.t; ++i) + if(this[i] != 0) return i*this.DB+lbit(this[i]); + if(this.s < 0) return this.t*this.DB; + return -1; + } + + // return number of 1 bits in x + function cbit(x) { + var r = 0; + while(x != 0) { x &= x-1; ++r; } + return r; + } + + // (public) return number of set bits + function bnBitCount() { + var r = 0, x = this.s&this.DM; + for(var i = 0; i < this.t; ++i) r += cbit(this[i]^x); + return r; + } + + // (public) true iff nth bit is set + function bnTestBit(n) { + var j = Math.floor(n/this.DB); + if(j >= this.t) return(this.s!=0); + return((this[j]&(1<<(n%this.DB)))!=0); + } + + // (protected) this op (1<>= this.DB; + } + if(a.t < this.t) { + c += a.s; + while(i < this.t) { + c += this[i]; + r[i++] = c&this.DM; + c >>= this.DB; + } + c += this.s; + } + else { + c += this.s; + while(i < a.t) { + c += a[i]; + r[i++] = c&this.DM; + c >>= this.DB; + } + c += a.s; + } + r.s = (c<0)?-1:0; + if(c > 0) r[i++] = c; + else if(c < -1) r[i++] = this.DV+c; + r.t = i; + r.clamp(); + } + + // (public) this + a + function bnAdd(a) { var r = nbi(); this.addTo(a,r); return r; } + + // (public) this - a + function bnSubtract(a) { var r = nbi(); this.subTo(a,r); return r; } + + // (public) this * a + function bnMultiply(a) { var r = nbi(); this.multiplyTo(a,r); return r; } + + // (public) this^2 + function bnSquare() { var r = nbi(); this.squareTo(r); return r; } + + // (public) this / a + function bnDivide(a) { var r = nbi(); this.divRemTo(a,r,null); return r; } + + // (public) this % a + function bnRemainder(a) { var r = nbi(); this.divRemTo(a,null,r); return r; } + + // (public) [this/a,this%a] + function bnDivideAndRemainder(a) { + var q = nbi(), r = nbi(); + this.divRemTo(a,q,r); + return new Array(q,r); + } + + // (protected) this *= n, this >= 0, 1 < n < DV + function bnpDMultiply(n) { + this[this.t] = this.am(0,n-1,this,0,0,this.t); + ++this.t; + this.clamp(); + } + + // (protected) this += n << w words, this >= 0 + function bnpDAddOffset(n,w) { + if(n == 0) return; + while(this.t <= w) this[this.t++] = 0; + this[w] += n; + while(this[w] >= this.DV) { + this[w] -= this.DV; + if(++w >= this.t) this[this.t++] = 0; + ++this[w]; + } + } + + // A "null" reducer + function NullExp() {} + function nNop(x) { return x; } + function nMulTo(x,y,r) { x.multiplyTo(y,r); } + function nSqrTo(x,r) { x.squareTo(r); } + + NullExp.prototype.convert = nNop; + NullExp.prototype.revert = nNop; + NullExp.prototype.mulTo = nMulTo; + NullExp.prototype.sqrTo = nSqrTo; + + // (public) this^e + function bnPow(e) { return this.exp(e,new NullExp()); } + + // (protected) r = lower n words of "this * a", a.t <= n + // "this" should be the larger one if appropriate. + function bnpMultiplyLowerTo(a,n,r) { + var i = Math.min(this.t+a.t,n); + r.s = 0; // assumes a,this >= 0 + r.t = i; + while(i > 0) r[--i] = 0; + var j; + for(j = r.t-this.t; i < j; ++i) r[i+this.t] = this.am(0,a[i],r,i,0,this.t); + for(j = Math.min(a.t,n); i < j; ++i) this.am(0,a[i],r,i,0,n-i); + r.clamp(); + } + + // (protected) r = "this * a" without lower n words, n > 0 + // "this" should be the larger one if appropriate. + function bnpMultiplyUpperTo(a,n,r) { + --n; + var i = r.t = this.t+a.t-n; + r.s = 0; // assumes a,this >= 0 + while(--i >= 0) r[i] = 0; + for(i = Math.max(n-this.t,0); i < a.t; ++i) + r[this.t+i-n] = this.am(n-i,a[i],r,0,0,this.t+i-n); + r.clamp(); + r.drShiftTo(1,r); + } + + // Barrett modular reduction + function Barrett(m) { + // setup Barrett + this.r2 = nbi(); + this.q3 = nbi(); + BigInteger.ONE.dlShiftTo(2*m.t,this.r2); + this.mu = this.r2.divide(m); + this.m = m; + } + + function barrettConvert(x) { + if(x.s < 0 || x.t > 2*this.m.t) return x.mod(this.m); + else if(x.compareTo(this.m) < 0) return x; + else { var r = nbi(); x.copyTo(r); this.reduce(r); return r; } + } + + function barrettRevert(x) { return x; } + + // x = x mod m (HAC 14.42) + function barrettReduce(x) { + x.drShiftTo(this.m.t-1,this.r2); + if(x.t > this.m.t+1) { x.t = this.m.t+1; x.clamp(); } + this.mu.multiplyUpperTo(this.r2,this.m.t+1,this.q3); + this.m.multiplyLowerTo(this.q3,this.m.t+1,this.r2); + while(x.compareTo(this.r2) < 0) x.dAddOffset(1,this.m.t+1); + x.subTo(this.r2,x); + while(x.compareTo(this.m) >= 0) x.subTo(this.m,x); + } + + // r = x^2 mod m; x != r + function barrettSqrTo(x,r) { x.squareTo(r); this.reduce(r); } + + // r = x*y mod m; x,y != r + function barrettMulTo(x,y,r) { x.multiplyTo(y,r); this.reduce(r); } + + Barrett.prototype.convert = barrettConvert; + Barrett.prototype.revert = barrettRevert; + Barrett.prototype.reduce = barrettReduce; + Barrett.prototype.mulTo = barrettMulTo; + Barrett.prototype.sqrTo = barrettSqrTo; + + // (public) this^e % m (HAC 14.85) + function bnModPow(e,m) { + var i = e.bitLength(), k, r = nbv(1), z; + if(i <= 0) return r; + else if(i < 18) k = 1; + else if(i < 48) k = 3; + else if(i < 144) k = 4; + else if(i < 768) k = 5; + else k = 6; + if(i < 8) + z = new Classic(m); + else if(m.isEven()) + z = new Barrett(m); + else + z = new Montgomery(m); + + // precomputation + var g = new Array(), n = 3, k1 = k-1, km = (1< 1) { + var g2 = nbi(); + z.sqrTo(g[1],g2); + while(n <= km) { + g[n] = nbi(); + z.mulTo(g2,g[n-2],g[n]); + n += 2; + } + } + + var j = e.t-1, w, is1 = true, r2 = nbi(), t; + i = nbits(e[j])-1; + while(j >= 0) { + if(i >= k1) w = (e[j]>>(i-k1))&km; + else { + w = (e[j]&((1<<(i+1))-1))<<(k1-i); + if(j > 0) w |= e[j-1]>>(this.DB+i-k1); + } + + n = k; + while((w&1) == 0) { w >>= 1; --n; } + if((i -= n) < 0) { i += this.DB; --j; } + if(is1) { // ret == 1, don't bother squaring or multiplying it + g[w].copyTo(r); + is1 = false; + } + else { + while(n > 1) { z.sqrTo(r,r2); z.sqrTo(r2,r); n -= 2; } + if(n > 0) z.sqrTo(r,r2); else { t = r; r = r2; r2 = t; } + z.mulTo(r2,g[w],r); + } + + while(j >= 0 && (e[j]&(1< 0) { + x.rShiftTo(g,x); + y.rShiftTo(g,y); + } + while(x.signum() > 0) { + if((i = x.getLowestSetBit()) > 0) x.rShiftTo(i,x); + if((i = y.getLowestSetBit()) > 0) y.rShiftTo(i,y); + if(x.compareTo(y) >= 0) { + x.subTo(y,x); + x.rShiftTo(1,x); + } + else { + y.subTo(x,y); + y.rShiftTo(1,y); + } + } + if(g > 0) y.lShiftTo(g,y); + return y; + } + + // (protected) this % n, n < 2^26 + function bnpModInt(n) { + if(n <= 0) return 0; + var d = this.DV%n, r = (this.s<0)?n-1:0; + if(this.t > 0) + if(d == 0) r = this[0]%n; + else for(var i = this.t-1; i >= 0; --i) r = (d*r+this[i])%n; + return r; + } + + // (public) 1/this % m (HAC 14.61) + function bnModInverse(m) { + var ac = m.isEven(); + if((this.isEven() && ac) || m.signum() == 0) return BigInteger.ZERO; + var u = m.clone(), v = this.clone(); + var a = nbv(1), b = nbv(0), c = nbv(0), d = nbv(1); + while(u.signum() != 0) { + while(u.isEven()) { + u.rShiftTo(1,u); + if(ac) { + if(!a.isEven() || !b.isEven()) { a.addTo(this,a); b.subTo(m,b); } + a.rShiftTo(1,a); + } + else if(!b.isEven()) b.subTo(m,b); + b.rShiftTo(1,b); + } + while(v.isEven()) { + v.rShiftTo(1,v); + if(ac) { + if(!c.isEven() || !d.isEven()) { c.addTo(this,c); d.subTo(m,d); } + c.rShiftTo(1,c); + } + else if(!d.isEven()) d.subTo(m,d); + d.rShiftTo(1,d); + } + if(u.compareTo(v) >= 0) { + u.subTo(v,u); + if(ac) a.subTo(c,a); + b.subTo(d,b); + } + else { + v.subTo(u,v); + if(ac) c.subTo(a,c); + d.subTo(b,d); + } + } + if(v.compareTo(BigInteger.ONE) != 0) return BigInteger.ZERO; + if(d.compareTo(m) >= 0) return d.subtract(m); + if(d.signum() < 0) d.addTo(m,d); else return d; + if(d.signum() < 0) return d.add(m); else return d; + } + + var lowprimes = [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,101,103,107,109,113,127,131,137,139,149,151,157,163,167,173,179,181,191,193,197,199,211,223,227,229,233,239,241,251,257,263,269,271,277,281,283,293,307,311,313,317,331,337,347,349,353,359,367,373,379,383,389,397,401,409,419,421,431,433,439,443,449,457,461,463,467,479,487,491,499,503,509,521,523,541,547,557,563,569,571,577,587,593,599,601,607,613,617,619,631,641,643,647,653,659,661,673,677,683,691,701,709,719,727,733,739,743,751,757,761,769,773,787,797,809,811,821,823,827,829,839,853,857,859,863,877,881,883,887,907,911,919,929,937,941,947,953,967,971,977,983,991,997]; + var lplim = (1<<26)/lowprimes[lowprimes.length-1]; + + // (public) test primality with certainty >= 1-.5^t + function bnIsProbablePrime(t) { + var i, x = this.abs(); + if(x.t == 1 && x[0] <= lowprimes[lowprimes.length-1]) { + for(i = 0; i < lowprimes.length; ++i) + if(x[0] == lowprimes[i]) return true; + return false; + } + if(x.isEven()) return false; + i = 1; + while(i < lowprimes.length) { + var m = lowprimes[i], j = i+1; + while(j < lowprimes.length && m < lplim) m *= lowprimes[j++]; + m = x.modInt(m); + while(i < j) if(m%lowprimes[i++] == 0) return false; + } + return x.millerRabin(t); + } + + // (protected) true if probably prime (HAC 4.24, Miller-Rabin) + function bnpMillerRabin(t) { + var n1 = this.subtract(BigInteger.ONE); + var k = n1.getLowestSetBit(); + if(k <= 0) return false; + var r = n1.shiftRight(k); + t = (t+1)>>1; + if(t > lowprimes.length) t = lowprimes.length; + var a = nbi(); + for(var i = 0; i < t; ++i) { + //Pick bases at random, instead of starting at 2 + a.fromInt(lowprimes[Math.floor(Math.random()*lowprimes.length)]); + var y = a.modPow(r,this); + if(y.compareTo(BigInteger.ONE) != 0 && y.compareTo(n1) != 0) { + var j = 1; + while(j++ < k && y.compareTo(n1) != 0) { + y = y.modPowInt(2,this); + if(y.compareTo(BigInteger.ONE) == 0) return false; + } + if(y.compareTo(n1) != 0) return false; + } + } + return true; + } + + // protected + BigInteger.prototype.chunkSize = bnpChunkSize; + BigInteger.prototype.toRadix = bnpToRadix; + BigInteger.prototype.fromRadix = bnpFromRadix; + BigInteger.prototype.fromNumber = bnpFromNumber; + BigInteger.prototype.bitwiseTo = bnpBitwiseTo; + BigInteger.prototype.changeBit = bnpChangeBit; + BigInteger.prototype.addTo = bnpAddTo; + BigInteger.prototype.dMultiply = bnpDMultiply; + BigInteger.prototype.dAddOffset = bnpDAddOffset; + BigInteger.prototype.multiplyLowerTo = bnpMultiplyLowerTo; + BigInteger.prototype.multiplyUpperTo = bnpMultiplyUpperTo; + BigInteger.prototype.modInt = bnpModInt; + BigInteger.prototype.millerRabin = bnpMillerRabin; + + // public + BigInteger.prototype.clone = bnClone; + BigInteger.prototype.intValue = bnIntValue; + BigInteger.prototype.byteValue = bnByteValue; + BigInteger.prototype.shortValue = bnShortValue; + BigInteger.prototype.signum = bnSigNum; + BigInteger.prototype.toByteArray = bnToByteArray; + BigInteger.prototype.equals = bnEquals; + BigInteger.prototype.min = bnMin; + BigInteger.prototype.max = bnMax; + BigInteger.prototype.and = bnAnd; + BigInteger.prototype.or = bnOr; + BigInteger.prototype.xor = bnXor; + BigInteger.prototype.andNot = bnAndNot; + BigInteger.prototype.not = bnNot; + BigInteger.prototype.shiftLeft = bnShiftLeft; + BigInteger.prototype.shiftRight = bnShiftRight; + BigInteger.prototype.getLowestSetBit = bnGetLowestSetBit; + BigInteger.prototype.bitCount = bnBitCount; + BigInteger.prototype.testBit = bnTestBit; + BigInteger.prototype.setBit = bnSetBit; + BigInteger.prototype.clearBit = bnClearBit; + BigInteger.prototype.flipBit = bnFlipBit; + BigInteger.prototype.add = bnAdd; + BigInteger.prototype.subtract = bnSubtract; + BigInteger.prototype.multiply = bnMultiply; + BigInteger.prototype.divide = bnDivide; + BigInteger.prototype.remainder = bnRemainder; + BigInteger.prototype.divideAndRemainder = bnDivideAndRemainder; + BigInteger.prototype.modPow = bnModPow; + BigInteger.prototype.modInverse = bnModInverse; + BigInteger.prototype.pow = bnPow; + BigInteger.prototype.gcd = bnGCD; + BigInteger.prototype.isProbablePrime = bnIsProbablePrime; + + // JSBN-specific extension + BigInteger.prototype.square = bnSquare; + + // Expose the Barrett function + BigInteger.prototype.Barrett = Barrett + + // BigInteger interfaces not implemented in jsbn: + + // BigInteger(int signum, byte[] magnitude) + // double doubleValue() + // float floatValue() + // int hashCode() + // long longValue() + // static BigInteger valueOf(long val) + + // Random number generator - requires a PRNG backend, e.g. prng4.js + + // For best results, put code like + // + // in your main HTML document. + + var rng_state; + var rng_pool; + var rng_pptr; + + // Mix in a 32-bit integer into the pool + function rng_seed_int(x) { + rng_pool[rng_pptr++] ^= x & 255; + rng_pool[rng_pptr++] ^= (x >> 8) & 255; + rng_pool[rng_pptr++] ^= (x >> 16) & 255; + rng_pool[rng_pptr++] ^= (x >> 24) & 255; + if(rng_pptr >= rng_psize) rng_pptr -= rng_psize; + } + + // Mix in the current time (w/milliseconds) into the pool + function rng_seed_time() { + rng_seed_int(new Date().getTime()); + } + + // Initialize the pool with junk if needed. + if(rng_pool == null) { + rng_pool = new Array(); + rng_pptr = 0; + var t; + if(typeof window !== "undefined" && window.crypto) { + if (window.crypto.getRandomValues) { + // Use webcrypto if available + var ua = new Uint8Array(32); + window.crypto.getRandomValues(ua); + for(t = 0; t < 32; ++t) + rng_pool[rng_pptr++] = ua[t]; + } + else if(navigator.appName == "Netscape" && navigator.appVersion < "5") { + // Extract entropy (256 bits) from NS4 RNG if available + var z = window.crypto.random(32); + for(t = 0; t < z.length; ++t) + rng_pool[rng_pptr++] = z.charCodeAt(t) & 255; + } + } + while(rng_pptr < rng_psize) { // extract some randomness from Math.random() + t = Math.floor(65536 * Math.random()); + rng_pool[rng_pptr++] = t >>> 8; + rng_pool[rng_pptr++] = t & 255; + } + rng_pptr = 0; + rng_seed_time(); + //rng_seed_int(window.screenX); + //rng_seed_int(window.screenY); + } + + function rng_get_byte() { + if(rng_state == null) { + rng_seed_time(); + rng_state = prng_newstate(); + rng_state.init(rng_pool); + for(rng_pptr = 0; rng_pptr < rng_pool.length; ++rng_pptr) + rng_pool[rng_pptr] = 0; + rng_pptr = 0; + //rng_pool = null; + } + // TODO: allow reseeding after first request + return rng_state.next(); + } + + function rng_get_bytes(ba) { + var i; + for(i = 0; i < ba.length; ++i) ba[i] = rng_get_byte(); + } + + function SecureRandom() {} + + SecureRandom.prototype.nextBytes = rng_get_bytes; + + // prng4.js - uses Arcfour as a PRNG + + function Arcfour() { + this.i = 0; + this.j = 0; + this.S = new Array(); + } + + // Initialize arcfour context from key, an array of ints, each from [0..255] + function ARC4init(key) { + var i, j, t; + for(i = 0; i < 256; ++i) + this.S[i] = i; + j = 0; + for(i = 0; i < 256; ++i) { + j = (j + this.S[i] + key[i % key.length]) & 255; + t = this.S[i]; + this.S[i] = this.S[j]; + this.S[j] = t; + } + this.i = 0; + this.j = 0; + } + + function ARC4next() { + var t; + this.i = (this.i + 1) & 255; + this.j = (this.j + this.S[this.i]) & 255; + t = this.S[this.i]; + this.S[this.i] = this.S[this.j]; + this.S[this.j] = t; + return this.S[(t + this.S[this.i]) & 255]; + } + + Arcfour.prototype.init = ARC4init; + Arcfour.prototype.next = ARC4next; + + // Plug in your RNG constructor here + function prng_newstate() { + return new Arcfour(); + } + + // Pool size must be a multiple of 4 and greater than 32. + // An array of bytes the size of the pool will be passed to init() + var rng_psize = 256; + + BigInteger.SecureRandom = SecureRandom; + BigInteger.BigInteger = BigInteger; + if (true) { + exports = module.exports = BigInteger; + } else {} + +}).call(this); + + +/***/ }), + +/***/ 243: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; + + +var net = __webpack_require__(631) + , tls = __webpack_require__(16) + , http = __webpack_require__(605) + , https = __webpack_require__(211) + , events = __webpack_require__(614) + , assert = __webpack_require__(357) + , util = __webpack_require__(669) + , Buffer = __webpack_require__(149).Buffer + ; + +exports.httpOverHttp = httpOverHttp +exports.httpsOverHttp = httpsOverHttp +exports.httpOverHttps = httpOverHttps +exports.httpsOverHttps = httpsOverHttps + + +function httpOverHttp(options) { + var agent = new TunnelingAgent(options) + agent.request = http.request + return agent +} + +function httpsOverHttp(options) { + var agent = new TunnelingAgent(options) + agent.request = http.request + agent.createSocket = createSecureSocket + agent.defaultPort = 443 + return agent +} + +function httpOverHttps(options) { + var agent = new TunnelingAgent(options) + agent.request = https.request + return agent +} + +function httpsOverHttps(options) { + var agent = new TunnelingAgent(options) + agent.request = https.request + agent.createSocket = createSecureSocket + agent.defaultPort = 443 + return agent +} + + +function TunnelingAgent(options) { + var self = this + self.options = options || {} + self.proxyOptions = self.options.proxy || {} + self.maxSockets = self.options.maxSockets || http.Agent.defaultMaxSockets + self.requests = [] + self.sockets = [] + + self.on('free', function onFree(socket, host, port) { + for (var i = 0, len = self.requests.length; i < len; ++i) { + var pending = self.requests[i] + if (pending.host === host && pending.port === port) { + // Detect the request to connect same origin server, + // reuse the connection. + self.requests.splice(i, 1) + pending.request.onSocket(socket) + return + } + } + socket.destroy() + self.removeSocket(socket) + }) +} +util.inherits(TunnelingAgent, events.EventEmitter) + +TunnelingAgent.prototype.addRequest = function addRequest(req, options) { + var self = this + + // Legacy API: addRequest(req, host, port, path) + if (typeof options === 'string') { + options = { + host: options, + port: arguments[2], + path: arguments[3] + }; + } + + if (self.sockets.length >= this.maxSockets) { + // We are over limit so we'll add it to the queue. + self.requests.push({host: options.host, port: options.port, request: req}) + return + } + + // If we are under maxSockets create a new one. + self.createConnection({host: options.host, port: options.port, request: req}) +} + +TunnelingAgent.prototype.createConnection = function createConnection(pending) { + var self = this + + self.createSocket(pending, function(socket) { + socket.on('free', onFree) + socket.on('close', onCloseOrRemove) + socket.on('agentRemove', onCloseOrRemove) + pending.request.onSocket(socket) + + function onFree() { + self.emit('free', socket, pending.host, pending.port) + } + + function onCloseOrRemove(err) { + self.removeSocket(socket) + socket.removeListener('free', onFree) + socket.removeListener('close', onCloseOrRemove) + socket.removeListener('agentRemove', onCloseOrRemove) + } + }) +} + +TunnelingAgent.prototype.createSocket = function createSocket(options, cb) { + var self = this + var placeholder = {} + self.sockets.push(placeholder) + + var connectOptions = mergeOptions({}, self.proxyOptions, + { method: 'CONNECT' + , path: options.host + ':' + options.port + , agent: false + } + ) + if (connectOptions.proxyAuth) { + connectOptions.headers = connectOptions.headers || {} + connectOptions.headers['Proxy-Authorization'] = 'Basic ' + + Buffer.from(connectOptions.proxyAuth).toString('base64') + } + + debug('making CONNECT request') + var connectReq = self.request(connectOptions) + connectReq.useChunkedEncodingByDefault = false // for v0.6 + connectReq.once('response', onResponse) // for v0.6 + connectReq.once('upgrade', onUpgrade) // for v0.6 + connectReq.once('connect', onConnect) // for v0.7 or later + connectReq.once('error', onError) + connectReq.end() + + function onResponse(res) { + // Very hacky. This is necessary to avoid http-parser leaks. + res.upgrade = true + } + + function onUpgrade(res, socket, head) { + // Hacky. + process.nextTick(function() { + onConnect(res, socket, head) + }) + } + + function onConnect(res, socket, head) { + connectReq.removeAllListeners() + socket.removeAllListeners() + + if (res.statusCode === 200) { + assert.equal(head.length, 0) + debug('tunneling connection has established') + self.sockets[self.sockets.indexOf(placeholder)] = socket + cb(socket) + } else { + debug('tunneling socket could not be established, statusCode=%d', res.statusCode) + var error = new Error('tunneling socket could not be established, ' + 'statusCode=' + res.statusCode) + error.code = 'ECONNRESET' + options.request.emit('error', error) + self.removeSocket(placeholder) + } + } + + function onError(cause) { + connectReq.removeAllListeners() + + debug('tunneling socket could not be established, cause=%s\n', cause.message, cause.stack) + var error = new Error('tunneling socket could not be established, ' + 'cause=' + cause.message) + error.code = 'ECONNRESET' + options.request.emit('error', error) + self.removeSocket(placeholder) + } +} + +TunnelingAgent.prototype.removeSocket = function removeSocket(socket) { + var pos = this.sockets.indexOf(socket) + if (pos === -1) return + + this.sockets.splice(pos, 1) + + var pending = this.requests.shift() + if (pending) { + // If we have pending requests and a socket gets closed a new one + // needs to be created to take over in the pool for the one that closed. + this.createConnection(pending) + } +} + +function createSecureSocket(options, cb) { + var self = this + TunnelingAgent.prototype.createSocket.call(self, options, function(socket) { + // 0 is dummy port for v0.6 + var secureSocket = tls.connect(0, mergeOptions({}, self.options, + { servername: options.host + , socket: socket + } + )) + self.sockets[self.sockets.indexOf(socket)] = secureSocket + cb(secureSocket) + }) +} + + +function mergeOptions(target) { + for (var i = 1, len = arguments.length; i < len; ++i) { + var overrides = arguments[i] + if (typeof overrides === 'object') { + var keys = Object.keys(overrides) + for (var j = 0, keyLen = keys.length; j < keyLen; ++j) { + var k = keys[j] + if (overrides[k] !== undefined) { + target[k] = overrides[k] + } + } + } + } + return target +} + + +var debug +if (process.env.NODE_DEBUG && /\btunnel\b/.test(process.env.NODE_DEBUG)) { + debug = function() { + var args = Array.prototype.slice.call(arguments) + if (typeof args[0] === 'string') { + args[0] = 'TUNNEL: ' + args[0] + } else { + args.unshift('TUNNEL:') + } + console.error.apply(console, args) + } +} else { + debug = function() {} +} +exports.debug = debug // for test + + +/***/ }), + +/***/ 249: +/***/ (function(module, __unusedexports, __webpack_require__) { + +// Copyright 2011 Mark Cavage All rights reserved. + +var errors = __webpack_require__(584); +var types = __webpack_require__(362); + +var Reader = __webpack_require__(733); +var Writer = __webpack_require__(998); + + +// --- Exports + +module.exports = { + + Reader: Reader, + + Writer: Writer + +}; + +for (var t in types) { + if (types.hasOwnProperty(t)) + module.exports[t] = types[t]; +} +for (var e in errors) { + if (errors.hasOwnProperty(e)) + module.exports[e] = errors[e]; +} + + +/***/ }), + +/***/ 254: +/***/ (function(module) { + +function Caseless (dict) { + this.dict = dict || {} +} +Caseless.prototype.set = function (name, value, clobber) { + if (typeof name === 'object') { + for (var i in name) { + this.set(i, name[i], value) + } + } else { + if (typeof clobber === 'undefined') clobber = true + var has = this.has(name) + + if (!clobber && has) this.dict[has] = this.dict[has] + ',' + value + else this.dict[has || name] = value + return has + } +} +Caseless.prototype.has = function (name) { + var keys = Object.keys(this.dict) + , name = name.toLowerCase() + ; + for (var i=0;i 0) { + m2 = lines[--ei].match(/*JSSTYLED*/ + /[-]+[ ]*END ([A-Z0-9][A-Za-z0-9]+ )?(PUBLIC|PRIVATE) KEY[ ]*[-]+/); + } + assert.ok(m2, 'invalid PEM footer'); + + /* Begin and end banners must match key type */ + assert.equal(m[2], m2[2]); + var type = m[2].toLowerCase(); + + var alg; + if (m[1]) { + /* They also must match algorithms, if given */ + assert.equal(m[1], m2[1], 'PEM header and footer mismatch'); + alg = m[1].trim(); + } + + lines = lines.slice(si, ei + 1); + + var headers = {}; + while (true) { + lines = lines.slice(1); + m = lines[0].match(/*JSSTYLED*/ + /^([A-Za-z0-9-]+): (.+)$/); + if (!m) + break; + headers[m[1].toLowerCase()] = m[2]; + } + + /* Chop off the first and last lines */ + lines = lines.slice(0, -1).join(''); + buf = Buffer.from(lines, 'base64'); + + var cipher, key, iv; + if (headers['proc-type']) { + var parts = headers['proc-type'].split(','); + if (parts[0] === '4' && parts[1] === 'ENCRYPTED') { + if (typeof (options.passphrase) === 'string') { + options.passphrase = Buffer.from( + options.passphrase, 'utf-8'); + } + if (!Buffer.isBuffer(options.passphrase)) { + throw (new errors.KeyEncryptedError( + options.filename, 'PEM')); + } else { + parts = headers['dek-info'].split(','); + assert.ok(parts.length === 2); + cipher = parts[0].toLowerCase(); + iv = Buffer.from(parts[1], 'hex'); + key = utils.opensslKeyDeriv(cipher, iv, + options.passphrase, 1).key; + } + } + } + + if (alg && alg.toLowerCase() === 'encrypted') { + var eder = new asn1.BerReader(buf); + var pbesEnd; + eder.readSequence(); + + eder.readSequence(); + pbesEnd = eder.offset + eder.length; + + var method = eder.readOID(); + if (method !== OID_PBES2) { + throw (new Error('Unsupported PEM/PKCS8 encryption ' + + 'scheme: ' + method)); + } + + eder.readSequence(); /* PBES2-params */ + + eder.readSequence(); /* keyDerivationFunc */ + var kdfEnd = eder.offset + eder.length; + var kdfOid = eder.readOID(); + if (kdfOid !== OID_PBKDF2) + throw (new Error('Unsupported PBES2 KDF: ' + kdfOid)); + eder.readSequence(); + var salt = eder.readString(asn1.Ber.OctetString, true); + var iterations = eder.readInt(); + var hashAlg = 'sha1'; + if (eder.offset < kdfEnd) { + eder.readSequence(); + var hashAlgOid = eder.readOID(); + hashAlg = OID_TO_HASH[hashAlgOid]; + if (hashAlg === undefined) { + throw (new Error('Unsupported PBKDF2 hash: ' + + hashAlgOid)); + } + } + eder._offset = kdfEnd; + + eder.readSequence(); /* encryptionScheme */ + var cipherOid = eder.readOID(); + cipher = OID_TO_CIPHER[cipherOid]; + if (cipher === undefined) { + throw (new Error('Unsupported PBES2 cipher: ' + + cipherOid)); + } + iv = eder.readString(asn1.Ber.OctetString, true); + + eder._offset = pbesEnd; + buf = eder.readString(asn1.Ber.OctetString, true); + + if (typeof (options.passphrase) === 'string') { + options.passphrase = Buffer.from( + options.passphrase, 'utf-8'); + } + if (!Buffer.isBuffer(options.passphrase)) { + throw (new errors.KeyEncryptedError( + options.filename, 'PEM')); + } + + var cinfo = utils.opensshCipherInfo(cipher); + + cipher = cinfo.opensslName; + key = utils.pbkdf2(hashAlg, salt, iterations, cinfo.keySize, + options.passphrase); + alg = undefined; + } + + if (cipher && key && iv) { + var cipherStream = crypto.createDecipheriv(cipher, key, iv); + var chunk, chunks = []; + cipherStream.once('error', function (e) { + if (e.toString().indexOf('bad decrypt') !== -1) { + throw (new Error('Incorrect passphrase ' + + 'supplied, could not decrypt key')); + } + throw (e); + }); + cipherStream.write(buf); + cipherStream.end(); + while ((chunk = cipherStream.read()) !== null) + chunks.push(chunk); + buf = Buffer.concat(chunks); + } + + /* The new OpenSSH internal format abuses PEM headers */ + if (alg && alg.toLowerCase() === 'openssh') + return (sshpriv.readSSHPrivate(type, buf, options)); + if (alg && alg.toLowerCase() === 'ssh2') + return (rfc4253.readType(type, buf, options)); + + var der = new asn1.BerReader(buf); + der.originalInput = input; + + /* + * All of the PEM file types start with a sequence tag, so chop it + * off here + */ + der.readSequence(); + + /* PKCS#1 type keys name an algorithm in the banner explicitly */ + if (alg) { + if (forceType) + assert.strictEqual(forceType, 'pkcs1'); + return (pkcs1.readPkcs1(alg, type, der)); + } else { + if (forceType) + assert.strictEqual(forceType, 'pkcs8'); + return (pkcs8.readPkcs8(alg, type, der)); + } +} + +function write(key, options, type) { + assert.object(key); + + var alg = { + 'ecdsa': 'EC', + 'rsa': 'RSA', + 'dsa': 'DSA', + 'ed25519': 'EdDSA' + }[key.type]; + var header; + + var der = new asn1.BerWriter(); + + if (PrivateKey.isPrivateKey(key)) { + if (type && type === 'pkcs8') { + header = 'PRIVATE KEY'; + pkcs8.writePkcs8(der, key); + } else { + if (type) + assert.strictEqual(type, 'pkcs1'); + header = alg + ' PRIVATE KEY'; + pkcs1.writePkcs1(der, key); + } + + } else if (Key.isKey(key)) { + if (type && type === 'pkcs1') { + header = alg + ' PUBLIC KEY'; + pkcs1.writePkcs1(der, key); + } else { + if (type) + assert.strictEqual(type, 'pkcs8'); + header = 'PUBLIC KEY'; + pkcs8.writePkcs8(der, key); + } + + } else { + throw (new Error('key is not a Key or PrivateKey')); + } + + var tmp = der.buffer.toString('base64'); + var len = tmp.length + (tmp.length / 64) + + 18 + 16 + header.length*2 + 10; + var buf = Buffer.alloc(len); + var o = 0; + o += buf.write('-----BEGIN ' + header + '-----\n', o); + for (var i = 0; i < tmp.length; ) { + var limit = i + 64; + if (limit > tmp.length) + limit = tmp.length; + o += buf.write(tmp.slice(i, limit), o); + buf[o++] = 10; + i = limit; + } + o += buf.write('-----END ' + header + '-----\n', o); + + return (buf.slice(0, o)); +} + + +/***/ }), + +/***/ 270: +/***/ (function(module, __unusedexports, __webpack_require__) { + +// Copyright 2015 Joyent, Inc. + +module.exports = { + bufferSplit: bufferSplit, + addRSAMissing: addRSAMissing, + calculateDSAPublic: calculateDSAPublic, + calculateED25519Public: calculateED25519Public, + calculateX25519Public: calculateX25519Public, + mpNormalize: mpNormalize, + mpDenormalize: mpDenormalize, + ecNormalize: ecNormalize, + countZeros: countZeros, + assertCompatible: assertCompatible, + isCompatible: isCompatible, + opensslKeyDeriv: opensslKeyDeriv, + opensshCipherInfo: opensshCipherInfo, + publicFromPrivateECDSA: publicFromPrivateECDSA, + zeroPadToLength: zeroPadToLength, + writeBitString: writeBitString, + readBitString: readBitString, + pbkdf2: pbkdf2 +}; + +var assert = __webpack_require__(477); +var Buffer = __webpack_require__(215).Buffer; +var PrivateKey = __webpack_require__(502); +var Key = __webpack_require__(852); +var crypto = __webpack_require__(417); +var algs = __webpack_require__(98); +var asn1 = __webpack_require__(62); + +var ec = __webpack_require__(729); +var jsbn = __webpack_require__(242).BigInteger; +var nacl = __webpack_require__(196); + +var MAX_CLASS_DEPTH = 3; + +function isCompatible(obj, klass, needVer) { + if (obj === null || typeof (obj) !== 'object') + return (false); + if (needVer === undefined) + needVer = klass.prototype._sshpkApiVersion; + if (obj instanceof klass && + klass.prototype._sshpkApiVersion[0] == needVer[0]) + return (true); + var proto = Object.getPrototypeOf(obj); + var depth = 0; + while (proto.constructor.name !== klass.name) { + proto = Object.getPrototypeOf(proto); + if (!proto || ++depth > MAX_CLASS_DEPTH) + return (false); + } + if (proto.constructor.name !== klass.name) + return (false); + var ver = proto._sshpkApiVersion; + if (ver === undefined) + ver = klass._oldVersionDetect(obj); + if (ver[0] != needVer[0] || ver[1] < needVer[1]) + return (false); + return (true); +} + +function assertCompatible(obj, klass, needVer, name) { + if (name === undefined) + name = 'object'; + assert.ok(obj, name + ' must not be null'); + assert.object(obj, name + ' must be an object'); + if (needVer === undefined) + needVer = klass.prototype._sshpkApiVersion; + if (obj instanceof klass && + klass.prototype._sshpkApiVersion[0] == needVer[0]) + return; + var proto = Object.getPrototypeOf(obj); + var depth = 0; + while (proto.constructor.name !== klass.name) { + proto = Object.getPrototypeOf(proto); + assert.ok(proto && ++depth <= MAX_CLASS_DEPTH, + name + ' must be a ' + klass.name + ' instance'); + } + assert.strictEqual(proto.constructor.name, klass.name, + name + ' must be a ' + klass.name + ' instance'); + var ver = proto._sshpkApiVersion; + if (ver === undefined) + ver = klass._oldVersionDetect(obj); + assert.ok(ver[0] == needVer[0] && ver[1] >= needVer[1], + name + ' must be compatible with ' + klass.name + ' klass ' + + 'version ' + needVer[0] + '.' + needVer[1]); +} + +var CIPHER_LEN = { + 'des-ede3-cbc': { key: 24, iv: 8 }, + 'aes-128-cbc': { key: 16, iv: 16 }, + 'aes-256-cbc': { key: 32, iv: 16 } +}; +var PKCS5_SALT_LEN = 8; + +function opensslKeyDeriv(cipher, salt, passphrase, count) { + assert.buffer(salt, 'salt'); + assert.buffer(passphrase, 'passphrase'); + assert.number(count, 'iteration count'); + + var clen = CIPHER_LEN[cipher]; + assert.object(clen, 'supported cipher'); + + salt = salt.slice(0, PKCS5_SALT_LEN); + + var D, D_prev, bufs; + var material = Buffer.alloc(0); + while (material.length < clen.key + clen.iv) { + bufs = []; + if (D_prev) + bufs.push(D_prev); + bufs.push(passphrase); + bufs.push(salt); + D = Buffer.concat(bufs); + for (var j = 0; j < count; ++j) + D = crypto.createHash('md5').update(D).digest(); + material = Buffer.concat([material, D]); + D_prev = D; + } + + return ({ + key: material.slice(0, clen.key), + iv: material.slice(clen.key, clen.key + clen.iv) + }); +} + +/* See: RFC2898 */ +function pbkdf2(hashAlg, salt, iterations, size, passphrase) { + var hkey = Buffer.alloc(salt.length + 4); + salt.copy(hkey); + + var gen = 0, ts = []; + var i = 1; + while (gen < size) { + var t = T(i++); + gen += t.length; + ts.push(t); + } + return (Buffer.concat(ts).slice(0, size)); + + function T(I) { + hkey.writeUInt32BE(I, hkey.length - 4); + + var hmac = crypto.createHmac(hashAlg, passphrase); + hmac.update(hkey); + + var Ti = hmac.digest(); + var Uc = Ti; + var c = 1; + while (c++ < iterations) { + hmac = crypto.createHmac(hashAlg, passphrase); + hmac.update(Uc); + Uc = hmac.digest(); + for (var x = 0; x < Ti.length; ++x) + Ti[x] ^= Uc[x]; + } + return (Ti); + } +} + +/* Count leading zero bits on a buffer */ +function countZeros(buf) { + var o = 0, obit = 8; + while (o < buf.length) { + var mask = (1 << obit); + if ((buf[o] & mask) === mask) + break; + obit--; + if (obit < 0) { + o++; + obit = 8; + } + } + return (o*8 + (8 - obit) - 1); +} + +function bufferSplit(buf, chr) { + assert.buffer(buf); + assert.string(chr); + + var parts = []; + var lastPart = 0; + var matches = 0; + for (var i = 0; i < buf.length; ++i) { + if (buf[i] === chr.charCodeAt(matches)) + ++matches; + else if (buf[i] === chr.charCodeAt(0)) + matches = 1; + else + matches = 0; + + if (matches >= chr.length) { + var newPart = i + 1; + parts.push(buf.slice(lastPart, newPart - matches)); + lastPart = newPart; + matches = 0; + } + } + if (lastPart <= buf.length) + parts.push(buf.slice(lastPart, buf.length)); + + return (parts); +} + +function ecNormalize(buf, addZero) { + assert.buffer(buf); + if (buf[0] === 0x00 && buf[1] === 0x04) { + if (addZero) + return (buf); + return (buf.slice(1)); + } else if (buf[0] === 0x04) { + if (!addZero) + return (buf); + } else { + while (buf[0] === 0x00) + buf = buf.slice(1); + if (buf[0] === 0x02 || buf[0] === 0x03) + throw (new Error('Compressed elliptic curve points ' + + 'are not supported')); + if (buf[0] !== 0x04) + throw (new Error('Not a valid elliptic curve point')); + if (!addZero) + return (buf); + } + var b = Buffer.alloc(buf.length + 1); + b[0] = 0x0; + buf.copy(b, 1); + return (b); +} + +function readBitString(der, tag) { + if (tag === undefined) + tag = asn1.Ber.BitString; + var buf = der.readString(tag, true); + assert.strictEqual(buf[0], 0x00, 'bit strings with unused bits are ' + + 'not supported (0x' + buf[0].toString(16) + ')'); + return (buf.slice(1)); +} + +function writeBitString(der, buf, tag) { + if (tag === undefined) + tag = asn1.Ber.BitString; + var b = Buffer.alloc(buf.length + 1); + b[0] = 0x00; + buf.copy(b, 1); + der.writeBuffer(b, tag); +} + +function mpNormalize(buf) { + assert.buffer(buf); + while (buf.length > 1 && buf[0] === 0x00 && (buf[1] & 0x80) === 0x00) + buf = buf.slice(1); + if ((buf[0] & 0x80) === 0x80) { + var b = Buffer.alloc(buf.length + 1); + b[0] = 0x00; + buf.copy(b, 1); + buf = b; + } + return (buf); +} + +function mpDenormalize(buf) { + assert.buffer(buf); + while (buf.length > 1 && buf[0] === 0x00) + buf = buf.slice(1); + return (buf); +} + +function zeroPadToLength(buf, len) { + assert.buffer(buf); + assert.number(len); + while (buf.length > len) { + assert.equal(buf[0], 0x00); + buf = buf.slice(1); + } + while (buf.length < len) { + var b = Buffer.alloc(buf.length + 1); + b[0] = 0x00; + buf.copy(b, 1); + buf = b; + } + return (buf); +} + +function bigintToMpBuf(bigint) { + var buf = Buffer.from(bigint.toByteArray()); + buf = mpNormalize(buf); + return (buf); +} + +function calculateDSAPublic(g, p, x) { + assert.buffer(g); + assert.buffer(p); + assert.buffer(x); + g = new jsbn(g); + p = new jsbn(p); + x = new jsbn(x); + var y = g.modPow(x, p); + var ybuf = bigintToMpBuf(y); + return (ybuf); +} + +function calculateED25519Public(k) { + assert.buffer(k); + + var kp = nacl.sign.keyPair.fromSeed(new Uint8Array(k)); + return (Buffer.from(kp.publicKey)); +} + +function calculateX25519Public(k) { + assert.buffer(k); + + var kp = nacl.box.keyPair.fromSeed(new Uint8Array(k)); + return (Buffer.from(kp.publicKey)); +} + +function addRSAMissing(key) { + assert.object(key); + assertCompatible(key, PrivateKey, [1, 1]); + + var d = new jsbn(key.part.d.data); + var buf; + + if (!key.part.dmodp) { + var p = new jsbn(key.part.p.data); + var dmodp = d.mod(p.subtract(1)); + + buf = bigintToMpBuf(dmodp); + key.part.dmodp = {name: 'dmodp', data: buf}; + key.parts.push(key.part.dmodp); + } + if (!key.part.dmodq) { + var q = new jsbn(key.part.q.data); + var dmodq = d.mod(q.subtract(1)); + + buf = bigintToMpBuf(dmodq); + key.part.dmodq = {name: 'dmodq', data: buf}; + key.parts.push(key.part.dmodq); + } +} + +function publicFromPrivateECDSA(curveName, priv) { + assert.string(curveName, 'curveName'); + assert.buffer(priv); + var params = algs.curves[curveName]; + var p = new jsbn(params.p); + var a = new jsbn(params.a); + var b = new jsbn(params.b); + var curve = new ec.ECCurveFp(p, a, b); + var G = curve.decodePointHex(params.G.toString('hex')); + + var d = new jsbn(mpNormalize(priv)); + var pub = G.multiply(d); + pub = Buffer.from(curve.encodePointHex(pub), 'hex'); + + var parts = []; + parts.push({name: 'curve', data: Buffer.from(curveName)}); + parts.push({name: 'Q', data: pub}); + + var key = new Key({type: 'ecdsa', curve: curve, parts: parts}); + return (key); +} + +function opensshCipherInfo(cipher) { + var inf = {}; + switch (cipher) { + case '3des-cbc': + inf.keySize = 24; + inf.blockSize = 8; + inf.opensslName = 'des-ede3-cbc'; + break; + case 'blowfish-cbc': + inf.keySize = 16; + inf.blockSize = 8; + inf.opensslName = 'bf-cbc'; + break; + case 'aes128-cbc': + case 'aes128-ctr': + case 'aes128-gcm@openssh.com': + inf.keySize = 16; + inf.blockSize = 16; + inf.opensslName = 'aes-128-' + cipher.slice(7, 10); + break; + case 'aes192-cbc': + case 'aes192-ctr': + case 'aes192-gcm@openssh.com': + inf.keySize = 24; + inf.blockSize = 16; + inf.opensslName = 'aes-192-' + cipher.slice(7, 10); + break; + case 'aes256-cbc': + case 'aes256-ctr': + case 'aes256-gcm@openssh.com': + inf.keySize = 32; + inf.blockSize = 16; + inf.opensslName = 'aes-256-' + cipher.slice(7, 10); + break; + default: + throw (new Error( + 'Unsupported openssl cipher "' + cipher + '"')); + } + return (inf); +} + + +/***/ }), + +/***/ 281: +/***/ (function(module) { + +"use strict"; + +module.exports = function generate_enum(it, $keyword, $ruleType) { + var out = ' '; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $data = 'data' + ($dataLvl || ''); + var $valid = 'valid' + $lvl; + var $isData = it.opts.$data && $schema && $schema.$data, + $schemaValue; + if ($isData) { + out += ' var schema' + ($lvl) + ' = ' + (it.util.getData($schema.$data, $dataLvl, it.dataPathArr)) + '; '; + $schemaValue = 'schema' + $lvl; + } else { + $schemaValue = $schema; + } + var $i = 'i' + $lvl, + $vSchema = 'schema' + $lvl; + if (!$isData) { + out += ' var ' + ($vSchema) + ' = validate.schema' + ($schemaPath) + ';'; + } + out += 'var ' + ($valid) + ';'; + if ($isData) { + out += ' if (schema' + ($lvl) + ' === undefined) ' + ($valid) + ' = true; else if (!Array.isArray(schema' + ($lvl) + ')) ' + ($valid) + ' = false; else {'; + } + out += '' + ($valid) + ' = false;for (var ' + ($i) + '=0; ' + ($i) + '<' + ($vSchema) + '.length; ' + ($i) + '++) if (equal(' + ($data) + ', ' + ($vSchema) + '[' + ($i) + '])) { ' + ($valid) + ' = true; break; }'; + if ($isData) { + out += ' } '; + } + out += ' if (!' + ($valid) + ') { '; + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('enum') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { allowedValues: schema' + ($lvl) + ' } '; + if (it.opts.messages !== false) { + out += ' , message: \'should be equal to one of the allowed values\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + out += ' }'; + if ($breakOnError) { + out += ' else { '; + } + return out; +} + + +/***/ }), + +/***/ 286: +/***/ (function(__unusedmodule, exports) { + +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +// NOTE: These type checking functions intentionally don't use `instanceof` +// because it is fragile and can be easily faked with `Object.create()`. + +function isArray(arg) { + if (Array.isArray) { + return Array.isArray(arg); + } + return objectToString(arg) === '[object Array]'; +} +exports.isArray = isArray; + +function isBoolean(arg) { + return typeof arg === 'boolean'; +} +exports.isBoolean = isBoolean; + +function isNull(arg) { + return arg === null; +} +exports.isNull = isNull; + +function isNullOrUndefined(arg) { + return arg == null; +} +exports.isNullOrUndefined = isNullOrUndefined; + +function isNumber(arg) { + return typeof arg === 'number'; +} +exports.isNumber = isNumber; + +function isString(arg) { + return typeof arg === 'string'; +} +exports.isString = isString; + +function isSymbol(arg) { + return typeof arg === 'symbol'; +} +exports.isSymbol = isSymbol; + +function isUndefined(arg) { + return arg === void 0; +} +exports.isUndefined = isUndefined; + +function isRegExp(re) { + return objectToString(re) === '[object RegExp]'; +} +exports.isRegExp = isRegExp; + +function isObject(arg) { + return typeof arg === 'object' && arg !== null; +} +exports.isObject = isObject; + +function isDate(d) { + return objectToString(d) === '[object Date]'; +} +exports.isDate = isDate; + +function isError(e) { + return (objectToString(e) === '[object Error]' || e instanceof Error); +} +exports.isError = isError; + +function isFunction(arg) { + return typeof arg === 'function'; +} +exports.isFunction = isFunction; + +function isPrimitive(arg) { + return arg === null || + typeof arg === 'boolean' || + typeof arg === 'number' || + typeof arg === 'string' || + typeof arg === 'symbol' || // ES6 symbol + typeof arg === 'undefined'; +} +exports.isPrimitive = isPrimitive; + +exports.isBuffer = Buffer.isBuffer; + +function objectToString(o) { + return Object.prototype.toString.call(o); +} + + +/***/ }), + +/***/ 287: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; + + +var url = __webpack_require__(835) +var qs = __webpack_require__(386) +var caseless = __webpack_require__(254) +var uuid = __webpack_require__(826) +var oauth = __webpack_require__(113) +var crypto = __webpack_require__(417) +var Buffer = __webpack_require__(149).Buffer + +function OAuth (request) { + this.request = request + this.params = null +} + +OAuth.prototype.buildParams = function (_oauth, uri, method, query, form, qsLib) { + var oa = {} + for (var i in _oauth) { + oa['oauth_' + i] = _oauth[i] + } + if (!oa.oauth_version) { + oa.oauth_version = '1.0' + } + if (!oa.oauth_timestamp) { + oa.oauth_timestamp = Math.floor(Date.now() / 1000).toString() + } + if (!oa.oauth_nonce) { + oa.oauth_nonce = uuid().replace(/-/g, '') + } + if (!oa.oauth_signature_method) { + oa.oauth_signature_method = 'HMAC-SHA1' + } + + var consumer_secret_or_private_key = oa.oauth_consumer_secret || oa.oauth_private_key // eslint-disable-line camelcase + delete oa.oauth_consumer_secret + delete oa.oauth_private_key + + var token_secret = oa.oauth_token_secret // eslint-disable-line camelcase + delete oa.oauth_token_secret + + var realm = oa.oauth_realm + delete oa.oauth_realm + delete oa.oauth_transport_method + + var baseurl = uri.protocol + '//' + uri.host + uri.pathname + var params = qsLib.parse([].concat(query, form, qsLib.stringify(oa)).join('&')) + + oa.oauth_signature = oauth.sign( + oa.oauth_signature_method, + method, + baseurl, + params, + consumer_secret_or_private_key, // eslint-disable-line camelcase + token_secret // eslint-disable-line camelcase + ) + + if (realm) { + oa.realm = realm + } + + return oa +} + +OAuth.prototype.buildBodyHash = function (_oauth, body) { + if (['HMAC-SHA1', 'RSA-SHA1'].indexOf(_oauth.signature_method || 'HMAC-SHA1') < 0) { + this.request.emit('error', new Error('oauth: ' + _oauth.signature_method + + ' signature_method not supported with body_hash signing.')) + } + + var shasum = crypto.createHash('sha1') + shasum.update(body || '') + var sha1 = shasum.digest('hex') + + return Buffer.from(sha1, 'hex').toString('base64') +} + +OAuth.prototype.concatParams = function (oa, sep, wrap) { + wrap = wrap || '' + + var params = Object.keys(oa).filter(function (i) { + return i !== 'realm' && i !== 'oauth_signature' + }).sort() + + if (oa.realm) { + params.splice(0, 0, 'realm') + } + params.push('oauth_signature') + + return params.map(function (i) { + return i + '=' + wrap + oauth.rfc3986(oa[i]) + wrap + }).join(sep) +} + +OAuth.prototype.onRequest = function (_oauth) { + var self = this + self.params = _oauth + + var uri = self.request.uri || {} + var method = self.request.method || '' + var headers = caseless(self.request.headers) + var body = self.request.body || '' + var qsLib = self.request.qsLib || qs + + var form + var query + var contentType = headers.get('content-type') || '' + var formContentType = 'application/x-www-form-urlencoded' + var transport = _oauth.transport_method || 'header' + + if (contentType.slice(0, formContentType.length) === formContentType) { + contentType = formContentType + form = body + } + if (uri.query) { + query = uri.query + } + if (transport === 'body' && (method !== 'POST' || contentType !== formContentType)) { + self.request.emit('error', new Error('oauth: transport_method of body requires POST ' + + 'and content-type ' + formContentType)) + } + + if (!form && typeof _oauth.body_hash === 'boolean') { + _oauth.body_hash = self.buildBodyHash(_oauth, self.request.body.toString()) + } + + var oa = self.buildParams(_oauth, uri, method, query, form, qsLib) + + switch (transport) { + case 'header': + self.request.setHeader('Authorization', 'OAuth ' + self.concatParams(oa, ',', '"')) + break + + case 'query': + var href = self.request.uri.href += (query ? '&' : '?') + self.concatParams(oa, '&') + self.request.uri = url.parse(href) + self.request.path = self.request.uri.path + break + + case 'body': + self.request.body = (form ? form + '&' : '') + self.concatParams(oa, '&') + break + + default: + self.request.emit('error', new Error('oauth: transport_method invalid')) + } +} + +exports.OAuth = OAuth + + +/***/ }), + +/***/ 290: +/***/ (function(module, __unusedexports, __webpack_require__) { + +// Copyright 2017 Joyent, Inc. + +module.exports = { + DiffieHellman: DiffieHellman, + generateECDSA: generateECDSA, + generateED25519: generateED25519 +}; + +var assert = __webpack_require__(477); +var crypto = __webpack_require__(417); +var Buffer = __webpack_require__(215).Buffer; +var algs = __webpack_require__(98); +var utils = __webpack_require__(270); +var nacl = __webpack_require__(196); + +var Key = __webpack_require__(852); +var PrivateKey = __webpack_require__(502); + +var CRYPTO_HAVE_ECDH = (crypto.createECDH !== undefined); + +var ecdh = __webpack_require__(886); +var ec = __webpack_require__(729); +var jsbn = __webpack_require__(242).BigInteger; + +function DiffieHellman(key) { + utils.assertCompatible(key, Key, [1, 4], 'key'); + this._isPriv = PrivateKey.isPrivateKey(key, [1, 3]); + this._algo = key.type; + this._curve = key.curve; + this._key = key; + if (key.type === 'dsa') { + if (!CRYPTO_HAVE_ECDH) { + throw (new Error('Due to bugs in the node 0.10 ' + + 'crypto API, node 0.12.x or later is required ' + + 'to use DH')); + } + this._dh = crypto.createDiffieHellman( + key.part.p.data, undefined, + key.part.g.data, undefined); + this._p = key.part.p; + this._g = key.part.g; + if (this._isPriv) + this._dh.setPrivateKey(key.part.x.data); + this._dh.setPublicKey(key.part.y.data); + + } else if (key.type === 'ecdsa') { + if (!CRYPTO_HAVE_ECDH) { + this._ecParams = new X9ECParameters(this._curve); + + if (this._isPriv) { + this._priv = new ECPrivate( + this._ecParams, key.part.d.data); + } + return; + } + + var curve = { + 'nistp256': 'prime256v1', + 'nistp384': 'secp384r1', + 'nistp521': 'secp521r1' + }[key.curve]; + this._dh = crypto.createECDH(curve); + if (typeof (this._dh) !== 'object' || + typeof (this._dh.setPrivateKey) !== 'function') { + CRYPTO_HAVE_ECDH = false; + DiffieHellman.call(this, key); + return; + } + if (this._isPriv) + this._dh.setPrivateKey(key.part.d.data); + this._dh.setPublicKey(key.part.Q.data); + + } else if (key.type === 'curve25519') { + if (this._isPriv) { + utils.assertCompatible(key, PrivateKey, [1, 5], 'key'); + this._priv = key.part.k.data; + } + + } else { + throw (new Error('DH not supported for ' + key.type + ' keys')); + } +} + +DiffieHellman.prototype.getPublicKey = function () { + if (this._isPriv) + return (this._key.toPublic()); + return (this._key); +}; + +DiffieHellman.prototype.getPrivateKey = function () { + if (this._isPriv) + return (this._key); + else + return (undefined); +}; +DiffieHellman.prototype.getKey = DiffieHellman.prototype.getPrivateKey; + +DiffieHellman.prototype._keyCheck = function (pk, isPub) { + assert.object(pk, 'key'); + if (!isPub) + utils.assertCompatible(pk, PrivateKey, [1, 3], 'key'); + utils.assertCompatible(pk, Key, [1, 4], 'key'); + + if (pk.type !== this._algo) { + throw (new Error('A ' + pk.type + ' key cannot be used in ' + + this._algo + ' Diffie-Hellman')); + } + + if (pk.curve !== this._curve) { + throw (new Error('A key from the ' + pk.curve + ' curve ' + + 'cannot be used with a ' + this._curve + + ' Diffie-Hellman')); + } + + if (pk.type === 'dsa') { + assert.deepEqual(pk.part.p, this._p, + 'DSA key prime does not match'); + assert.deepEqual(pk.part.g, this._g, + 'DSA key generator does not match'); + } +}; + +DiffieHellman.prototype.setKey = function (pk) { + this._keyCheck(pk); + + if (pk.type === 'dsa') { + this._dh.setPrivateKey(pk.part.x.data); + this._dh.setPublicKey(pk.part.y.data); + + } else if (pk.type === 'ecdsa') { + if (CRYPTO_HAVE_ECDH) { + this._dh.setPrivateKey(pk.part.d.data); + this._dh.setPublicKey(pk.part.Q.data); + } else { + this._priv = new ECPrivate( + this._ecParams, pk.part.d.data); + } + + } else if (pk.type === 'curve25519') { + var k = pk.part.k; + if (!pk.part.k) + k = pk.part.r; + this._priv = k.data; + if (this._priv[0] === 0x00) + this._priv = this._priv.slice(1); + this._priv = this._priv.slice(0, 32); + } + this._key = pk; + this._isPriv = true; +}; +DiffieHellman.prototype.setPrivateKey = DiffieHellman.prototype.setKey; + +DiffieHellman.prototype.computeSecret = function (otherpk) { + this._keyCheck(otherpk, true); + if (!this._isPriv) + throw (new Error('DH exchange has not been initialized with ' + + 'a private key yet')); + + var pub; + if (this._algo === 'dsa') { + return (this._dh.computeSecret( + otherpk.part.y.data)); + + } else if (this._algo === 'ecdsa') { + if (CRYPTO_HAVE_ECDH) { + return (this._dh.computeSecret( + otherpk.part.Q.data)); + } else { + pub = new ECPublic( + this._ecParams, otherpk.part.Q.data); + return (this._priv.deriveSharedSecret(pub)); + } + + } else if (this._algo === 'curve25519') { + pub = otherpk.part.A.data; + while (pub[0] === 0x00 && pub.length > 32) + pub = pub.slice(1); + var priv = this._priv; + assert.strictEqual(pub.length, 32); + assert.strictEqual(priv.length, 32); + + var secret = nacl.box.before(new Uint8Array(pub), + new Uint8Array(priv)); + + return (Buffer.from(secret)); + } + + throw (new Error('Invalid algorithm: ' + this._algo)); +}; + +DiffieHellman.prototype.generateKey = function () { + var parts = []; + var priv, pub; + if (this._algo === 'dsa') { + this._dh.generateKeys(); + + parts.push({name: 'p', data: this._p.data}); + parts.push({name: 'q', data: this._key.part.q.data}); + parts.push({name: 'g', data: this._g.data}); + parts.push({name: 'y', data: this._dh.getPublicKey()}); + parts.push({name: 'x', data: this._dh.getPrivateKey()}); + this._key = new PrivateKey({ + type: 'dsa', + parts: parts + }); + this._isPriv = true; + return (this._key); + + } else if (this._algo === 'ecdsa') { + if (CRYPTO_HAVE_ECDH) { + this._dh.generateKeys(); + + parts.push({name: 'curve', + data: Buffer.from(this._curve)}); + parts.push({name: 'Q', data: this._dh.getPublicKey()}); + parts.push({name: 'd', data: this._dh.getPrivateKey()}); + this._key = new PrivateKey({ + type: 'ecdsa', + curve: this._curve, + parts: parts + }); + this._isPriv = true; + return (this._key); + + } else { + var n = this._ecParams.getN(); + var r = new jsbn(crypto.randomBytes(n.bitLength())); + var n1 = n.subtract(jsbn.ONE); + priv = r.mod(n1).add(jsbn.ONE); + pub = this._ecParams.getG().multiply(priv); + + priv = Buffer.from(priv.toByteArray()); + pub = Buffer.from(this._ecParams.getCurve(). + encodePointHex(pub), 'hex'); + + this._priv = new ECPrivate(this._ecParams, priv); + + parts.push({name: 'curve', + data: Buffer.from(this._curve)}); + parts.push({name: 'Q', data: pub}); + parts.push({name: 'd', data: priv}); + + this._key = new PrivateKey({ + type: 'ecdsa', + curve: this._curve, + parts: parts + }); + this._isPriv = true; + return (this._key); + } + + } else if (this._algo === 'curve25519') { + var pair = nacl.box.keyPair(); + priv = Buffer.from(pair.secretKey); + pub = Buffer.from(pair.publicKey); + priv = Buffer.concat([priv, pub]); + assert.strictEqual(priv.length, 64); + assert.strictEqual(pub.length, 32); + + parts.push({name: 'A', data: pub}); + parts.push({name: 'k', data: priv}); + this._key = new PrivateKey({ + type: 'curve25519', + parts: parts + }); + this._isPriv = true; + return (this._key); + } + + throw (new Error('Invalid algorithm: ' + this._algo)); +}; +DiffieHellman.prototype.generateKeys = DiffieHellman.prototype.generateKey; + +/* These are helpers for using ecc-jsbn (for node 0.10 compatibility). */ + +function X9ECParameters(name) { + var params = algs.curves[name]; + assert.object(params); + + var p = new jsbn(params.p); + var a = new jsbn(params.a); + var b = new jsbn(params.b); + var n = new jsbn(params.n); + var h = jsbn.ONE; + var curve = new ec.ECCurveFp(p, a, b); + var G = curve.decodePointHex(params.G.toString('hex')); + + this.curve = curve; + this.g = G; + this.n = n; + this.h = h; +} +X9ECParameters.prototype.getCurve = function () { return (this.curve); }; +X9ECParameters.prototype.getG = function () { return (this.g); }; +X9ECParameters.prototype.getN = function () { return (this.n); }; +X9ECParameters.prototype.getH = function () { return (this.h); }; + +function ECPublic(params, buffer) { + this._params = params; + if (buffer[0] === 0x00) + buffer = buffer.slice(1); + this._pub = params.getCurve().decodePointHex(buffer.toString('hex')); +} + +function ECPrivate(params, buffer) { + this._params = params; + this._priv = new jsbn(utils.mpNormalize(buffer)); +} +ECPrivate.prototype.deriveSharedSecret = function (pubKey) { + assert.ok(pubKey instanceof ECPublic); + var S = pubKey._pub.multiply(this._priv); + return (Buffer.from(S.getX().toBigInteger().toByteArray())); +}; + +function generateED25519() { + var pair = nacl.sign.keyPair(); + var priv = Buffer.from(pair.secretKey); + var pub = Buffer.from(pair.publicKey); + assert.strictEqual(priv.length, 64); + assert.strictEqual(pub.length, 32); + + var parts = []; + parts.push({name: 'A', data: pub}); + parts.push({name: 'k', data: priv.slice(0, 32)}); + var key = new PrivateKey({ + type: 'ed25519', + parts: parts + }); + return (key); +} + +/* Generates a new ECDSA private key on a given curve. */ +function generateECDSA(curve) { + var parts = []; + var key; + + if (CRYPTO_HAVE_ECDH) { + /* + * Node crypto doesn't expose key generation directly, but the + * ECDH instances can generate keys. It turns out this just + * calls into the OpenSSL generic key generator, and we can + * read its output happily without doing an actual DH. So we + * use that here. + */ + var osCurve = { + 'nistp256': 'prime256v1', + 'nistp384': 'secp384r1', + 'nistp521': 'secp521r1' + }[curve]; + + var dh = crypto.createECDH(osCurve); + dh.generateKeys(); + + parts.push({name: 'curve', + data: Buffer.from(curve)}); + parts.push({name: 'Q', data: dh.getPublicKey()}); + parts.push({name: 'd', data: dh.getPrivateKey()}); + + key = new PrivateKey({ + type: 'ecdsa', + curve: curve, + parts: parts + }); + return (key); + } else { + + var ecParams = new X9ECParameters(curve); + + /* This algorithm taken from FIPS PUB 186-4 (section B.4.1) */ + var n = ecParams.getN(); + /* + * The crypto.randomBytes() function can only give us whole + * bytes, so taking a nod from X9.62, we round up. + */ + var cByteLen = Math.ceil((n.bitLength() + 64) / 8); + var c = new jsbn(crypto.randomBytes(cByteLen)); + + var n1 = n.subtract(jsbn.ONE); + var priv = c.mod(n1).add(jsbn.ONE); + var pub = ecParams.getG().multiply(priv); + + priv = Buffer.from(priv.toByteArray()); + pub = Buffer.from(ecParams.getCurve(). + encodePointHex(pub), 'hex'); + + parts.push({name: 'curve', data: Buffer.from(curve)}); + parts.push({name: 'Q', data: pub}); + parts.push({name: 'd', data: priv}); + + key = new PrivateKey({ + type: 'ecdsa', + curve: curve, + parts: parts + }); + return (key); + } +} + + +/***/ }), + +/***/ 293: +/***/ (function(module) { + +module.exports = require("buffer"); + +/***/ }), + +/***/ 314: +/***/ (function(module) { + +"use strict"; + +module.exports = function generate_custom(it, $keyword, $ruleType) { + var out = ' '; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $errorKeyword; + var $data = 'data' + ($dataLvl || ''); + var $valid = 'valid' + $lvl; + var $errs = 'errs__' + $lvl; + var $isData = it.opts.$data && $schema && $schema.$data, + $schemaValue; + if ($isData) { + out += ' var schema' + ($lvl) + ' = ' + (it.util.getData($schema.$data, $dataLvl, it.dataPathArr)) + '; '; + $schemaValue = 'schema' + $lvl; + } else { + $schemaValue = $schema; + } + var $rule = this, + $definition = 'definition' + $lvl, + $rDef = $rule.definition, + $closingBraces = ''; + var $compile, $inline, $macro, $ruleValidate, $validateCode; + if ($isData && $rDef.$data) { + $validateCode = 'keywordValidate' + $lvl; + var $validateSchema = $rDef.validateSchema; + out += ' var ' + ($definition) + ' = RULES.custom[\'' + ($keyword) + '\'].definition; var ' + ($validateCode) + ' = ' + ($definition) + '.validate;'; + } else { + $ruleValidate = it.useCustomRule($rule, $schema, it.schema, it); + if (!$ruleValidate) return; + $schemaValue = 'validate.schema' + $schemaPath; + $validateCode = $ruleValidate.code; + $compile = $rDef.compile; + $inline = $rDef.inline; + $macro = $rDef.macro; + } + var $ruleErrs = $validateCode + '.errors', + $i = 'i' + $lvl, + $ruleErr = 'ruleErr' + $lvl, + $asyncKeyword = $rDef.async; + if ($asyncKeyword && !it.async) throw new Error('async keyword in sync schema'); + if (!($inline || $macro)) { + out += '' + ($ruleErrs) + ' = null;'; + } + out += 'var ' + ($errs) + ' = errors;var ' + ($valid) + ';'; + if ($isData && $rDef.$data) { + $closingBraces += '}'; + out += ' if (' + ($schemaValue) + ' === undefined) { ' + ($valid) + ' = true; } else { '; + if ($validateSchema) { + $closingBraces += '}'; + out += ' ' + ($valid) + ' = ' + ($definition) + '.validateSchema(' + ($schemaValue) + '); if (' + ($valid) + ') { '; + } + } + if ($inline) { + if ($rDef.statements) { + out += ' ' + ($ruleValidate.validate) + ' '; + } else { + out += ' ' + ($valid) + ' = ' + ($ruleValidate.validate) + '; '; + } + } else if ($macro) { + var $it = it.util.copy(it); + var $closingBraces = ''; + $it.level++; + var $nextValid = 'valid' + $it.level; + $it.schema = $ruleValidate.validate; + $it.schemaPath = ''; + var $wasComposite = it.compositeRule; + it.compositeRule = $it.compositeRule = true; + var $code = it.validate($it).replace(/validate\.schema/g, $validateCode); + it.compositeRule = $it.compositeRule = $wasComposite; + out += ' ' + ($code); + } else { + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; + out += ' ' + ($validateCode) + '.call( '; + if (it.opts.passContext) { + out += 'this'; + } else { + out += 'self'; + } + if ($compile || $rDef.schema === false) { + out += ' , ' + ($data) + ' '; + } else { + out += ' , ' + ($schemaValue) + ' , ' + ($data) + ' , validate.schema' + (it.schemaPath) + ' '; + } + out += ' , (dataPath || \'\')'; + if (it.errorPath != '""') { + out += ' + ' + (it.errorPath); + } + var $parentData = $dataLvl ? 'data' + (($dataLvl - 1) || '') : 'parentData', + $parentDataProperty = $dataLvl ? it.dataPathArr[$dataLvl] : 'parentDataProperty'; + out += ' , ' + ($parentData) + ' , ' + ($parentDataProperty) + ' , rootData ) '; + var def_callRuleValidate = out; + out = $$outStack.pop(); + if ($rDef.errors === false) { + out += ' ' + ($valid) + ' = '; + if ($asyncKeyword) { + out += 'await '; + } + out += '' + (def_callRuleValidate) + '; '; + } else { + if ($asyncKeyword) { + $ruleErrs = 'customErrors' + $lvl; + out += ' var ' + ($ruleErrs) + ' = null; try { ' + ($valid) + ' = await ' + (def_callRuleValidate) + '; } catch (e) { ' + ($valid) + ' = false; if (e instanceof ValidationError) ' + ($ruleErrs) + ' = e.errors; else throw e; } '; + } else { + out += ' ' + ($ruleErrs) + ' = null; ' + ($valid) + ' = ' + (def_callRuleValidate) + '; '; + } + } + } + if ($rDef.modifying) { + out += ' if (' + ($parentData) + ') ' + ($data) + ' = ' + ($parentData) + '[' + ($parentDataProperty) + '];'; + } + out += '' + ($closingBraces); + if ($rDef.valid) { + if ($breakOnError) { + out += ' if (true) { '; + } + } else { + out += ' if ( '; + if ($rDef.valid === undefined) { + out += ' !'; + if ($macro) { + out += '' + ($nextValid); + } else { + out += '' + ($valid); + } + } else { + out += ' ' + (!$rDef.valid) + ' '; + } + out += ') { '; + $errorKeyword = $rule.keyword; + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ($errorKeyword || 'custom') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { keyword: \'' + ($rule.keyword) + '\' } '; + if (it.opts.messages !== false) { + out += ' , message: \'should pass "' + ($rule.keyword) + '" keyword validation\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + var def_customError = out; + out = $$outStack.pop(); + if ($inline) { + if ($rDef.errors) { + if ($rDef.errors != 'full') { + out += ' for (var ' + ($i) + '=' + ($errs) + '; ' + ($i) + '', + $notOp = $isMax ? '>' : '<', + $errorKeyword = undefined; + if ($isDataExcl) { + var $schemaValueExcl = it.util.getData($schemaExcl.$data, $dataLvl, it.dataPathArr), + $exclusive = 'exclusive' + $lvl, + $exclType = 'exclType' + $lvl, + $exclIsNumber = 'exclIsNumber' + $lvl, + $opExpr = 'op' + $lvl, + $opStr = '\' + ' + $opExpr + ' + \''; + out += ' var schemaExcl' + ($lvl) + ' = ' + ($schemaValueExcl) + '; '; + $schemaValueExcl = 'schemaExcl' + $lvl; + out += ' var ' + ($exclusive) + '; var ' + ($exclType) + ' = typeof ' + ($schemaValueExcl) + '; if (' + ($exclType) + ' != \'boolean\' && ' + ($exclType) + ' != \'undefined\' && ' + ($exclType) + ' != \'number\') { '; + var $errorKeyword = $exclusiveKeyword; + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ($errorKeyword || '_exclusiveLimit') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: {} '; + if (it.opts.messages !== false) { + out += ' , message: \'' + ($exclusiveKeyword) + ' should be boolean\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + out += ' } else if ( '; + if ($isData) { + out += ' (' + ($schemaValue) + ' !== undefined && typeof ' + ($schemaValue) + ' != \'number\') || '; + } + out += ' ' + ($exclType) + ' == \'number\' ? ( (' + ($exclusive) + ' = ' + ($schemaValue) + ' === undefined || ' + ($schemaValueExcl) + ' ' + ($op) + '= ' + ($schemaValue) + ') ? ' + ($data) + ' ' + ($notOp) + '= ' + ($schemaValueExcl) + ' : ' + ($data) + ' ' + ($notOp) + ' ' + ($schemaValue) + ' ) : ( (' + ($exclusive) + ' = ' + ($schemaValueExcl) + ' === true) ? ' + ($data) + ' ' + ($notOp) + '= ' + ($schemaValue) + ' : ' + ($data) + ' ' + ($notOp) + ' ' + ($schemaValue) + ' ) || ' + ($data) + ' !== ' + ($data) + ') { var op' + ($lvl) + ' = ' + ($exclusive) + ' ? \'' + ($op) + '\' : \'' + ($op) + '=\'; '; + if ($schema === undefined) { + $errorKeyword = $exclusiveKeyword; + $errSchemaPath = it.errSchemaPath + '/' + $exclusiveKeyword; + $schemaValue = $schemaValueExcl; + $isData = $isDataExcl; + } + } else { + var $exclIsNumber = typeof $schemaExcl == 'number', + $opStr = $op; + if ($exclIsNumber && $isData) { + var $opExpr = '\'' + $opStr + '\''; + out += ' if ( '; + if ($isData) { + out += ' (' + ($schemaValue) + ' !== undefined && typeof ' + ($schemaValue) + ' != \'number\') || '; + } + out += ' ( ' + ($schemaValue) + ' === undefined || ' + ($schemaExcl) + ' ' + ($op) + '= ' + ($schemaValue) + ' ? ' + ($data) + ' ' + ($notOp) + '= ' + ($schemaExcl) + ' : ' + ($data) + ' ' + ($notOp) + ' ' + ($schemaValue) + ' ) || ' + ($data) + ' !== ' + ($data) + ') { '; + } else { + if ($exclIsNumber && $schema === undefined) { + $exclusive = true; + $errorKeyword = $exclusiveKeyword; + $errSchemaPath = it.errSchemaPath + '/' + $exclusiveKeyword; + $schemaValue = $schemaExcl; + $notOp += '='; + } else { + if ($exclIsNumber) $schemaValue = Math[$isMax ? 'min' : 'max']($schemaExcl, $schema); + if ($schemaExcl === ($exclIsNumber ? $schemaValue : true)) { + $exclusive = true; + $errorKeyword = $exclusiveKeyword; + $errSchemaPath = it.errSchemaPath + '/' + $exclusiveKeyword; + $notOp += '='; + } else { + $exclusive = false; + $opStr += '='; + } + } + var $opExpr = '\'' + $opStr + '\''; + out += ' if ( '; + if ($isData) { + out += ' (' + ($schemaValue) + ' !== undefined && typeof ' + ($schemaValue) + ' != \'number\') || '; + } + out += ' ' + ($data) + ' ' + ($notOp) + ' ' + ($schemaValue) + ' || ' + ($data) + ' !== ' + ($data) + ') { '; + } + } + $errorKeyword = $errorKeyword || $keyword; + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ($errorKeyword || '_limit') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { comparison: ' + ($opExpr) + ', limit: ' + ($schemaValue) + ', exclusive: ' + ($exclusive) + ' } '; + if (it.opts.messages !== false) { + out += ' , message: \'should be ' + ($opStr) + ' '; + if ($isData) { + out += '\' + ' + ($schemaValue); + } else { + out += '' + ($schemaValue) + '\''; + } + } + if (it.opts.verbose) { + out += ' , schema: '; + if ($isData) { + out += 'validate.schema' + ($schemaPath); + } else { + out += '' + ($schema); + } + out += ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + out += ' } '; + if ($breakOnError) { + out += ' else { '; + } + return out; +} + + +/***/ }), + +/***/ 342: +/***/ (function(module, __unusedexports, __webpack_require__) { + +// Copyright 2012 Joyent, Inc. All rights reserved. + +var assert = __webpack_require__(477); +var util = __webpack_require__(669); +var utils = __webpack_require__(909); + + + +///--- Globals + +var HASH_ALGOS = utils.HASH_ALGOS; +var PK_ALGOS = utils.PK_ALGOS; +var HttpSignatureError = utils.HttpSignatureError; +var InvalidAlgorithmError = utils.InvalidAlgorithmError; +var validateAlgorithm = utils.validateAlgorithm; + +var State = { + New: 0, + Params: 1 +}; + +var ParamsState = { + Name: 0, + Quote: 1, + Value: 2, + Comma: 3 +}; + + +///--- Specific Errors + + +function ExpiredRequestError(message) { + HttpSignatureError.call(this, message, ExpiredRequestError); +} +util.inherits(ExpiredRequestError, HttpSignatureError); + + +function InvalidHeaderError(message) { + HttpSignatureError.call(this, message, InvalidHeaderError); +} +util.inherits(InvalidHeaderError, HttpSignatureError); + + +function InvalidParamsError(message) { + HttpSignatureError.call(this, message, InvalidParamsError); +} +util.inherits(InvalidParamsError, HttpSignatureError); + + +function MissingHeaderError(message) { + HttpSignatureError.call(this, message, MissingHeaderError); +} +util.inherits(MissingHeaderError, HttpSignatureError); + +function StrictParsingError(message) { + HttpSignatureError.call(this, message, StrictParsingError); +} +util.inherits(StrictParsingError, HttpSignatureError); + +///--- Exported API + +module.exports = { + + /** + * Parses the 'Authorization' header out of an http.ServerRequest object. + * + * Note that this API will fully validate the Authorization header, and throw + * on any error. It will not however check the signature, or the keyId format + * as those are specific to your environment. You can use the options object + * to pass in extra constraints. + * + * As a response object you can expect this: + * + * { + * "scheme": "Signature", + * "params": { + * "keyId": "foo", + * "algorithm": "rsa-sha256", + * "headers": [ + * "date" or "x-date", + * "digest" + * ], + * "signature": "base64" + * }, + * "signingString": "ready to be passed to crypto.verify()" + * } + * + * @param {Object} request an http.ServerRequest. + * @param {Object} options an optional options object with: + * - clockSkew: allowed clock skew in seconds (default 300). + * - headers: required header names (def: date or x-date) + * - algorithms: algorithms to support (default: all). + * - strict: should enforce latest spec parsing + * (default: false). + * @return {Object} parsed out object (see above). + * @throws {TypeError} on invalid input. + * @throws {InvalidHeaderError} on an invalid Authorization header error. + * @throws {InvalidParamsError} if the params in the scheme are invalid. + * @throws {MissingHeaderError} if the params indicate a header not present, + * either in the request headers from the params, + * or not in the params from a required header + * in options. + * @throws {StrictParsingError} if old attributes are used in strict parsing + * mode. + * @throws {ExpiredRequestError} if the value of date or x-date exceeds skew. + */ + parseRequest: function parseRequest(request, options) { + assert.object(request, 'request'); + assert.object(request.headers, 'request.headers'); + if (options === undefined) { + options = {}; + } + if (options.headers === undefined) { + options.headers = [request.headers['x-date'] ? 'x-date' : 'date']; + } + assert.object(options, 'options'); + assert.arrayOfString(options.headers, 'options.headers'); + assert.optionalFinite(options.clockSkew, 'options.clockSkew'); + + var authzHeaderName = options.authorizationHeaderName || 'authorization'; + + if (!request.headers[authzHeaderName]) { + throw new MissingHeaderError('no ' + authzHeaderName + ' header ' + + 'present in the request'); + } + + options.clockSkew = options.clockSkew || 300; + + + var i = 0; + var state = State.New; + var substate = ParamsState.Name; + var tmpName = ''; + var tmpValue = ''; + + var parsed = { + scheme: '', + params: {}, + signingString: '' + }; + + var authz = request.headers[authzHeaderName]; + for (i = 0; i < authz.length; i++) { + var c = authz.charAt(i); + + switch (Number(state)) { + + case State.New: + if (c !== ' ') parsed.scheme += c; + else state = State.Params; + break; + + case State.Params: + switch (Number(substate)) { + + case ParamsState.Name: + var code = c.charCodeAt(0); + // restricted name of A-Z / a-z + if ((code >= 0x41 && code <= 0x5a) || // A-Z + (code >= 0x61 && code <= 0x7a)) { // a-z + tmpName += c; + } else if (c === '=') { + if (tmpName.length === 0) + throw new InvalidHeaderError('bad param format'); + substate = ParamsState.Quote; + } else { + throw new InvalidHeaderError('bad param format'); + } + break; + + case ParamsState.Quote: + if (c === '"') { + tmpValue = ''; + substate = ParamsState.Value; + } else { + throw new InvalidHeaderError('bad param format'); + } + break; + + case ParamsState.Value: + if (c === '"') { + parsed.params[tmpName] = tmpValue; + substate = ParamsState.Comma; + } else { + tmpValue += c; + } + break; + + case ParamsState.Comma: + if (c === ',') { + tmpName = ''; + substate = ParamsState.Name; + } else { + throw new InvalidHeaderError('bad param format'); + } + break; + + default: + throw new Error('Invalid substate'); + } + break; + + default: + throw new Error('Invalid substate'); + } + + } + + if (!parsed.params.headers || parsed.params.headers === '') { + if (request.headers['x-date']) { + parsed.params.headers = ['x-date']; + } else { + parsed.params.headers = ['date']; + } + } else { + parsed.params.headers = parsed.params.headers.split(' '); + } + + // Minimally validate the parsed object + if (!parsed.scheme || parsed.scheme !== 'Signature') + throw new InvalidHeaderError('scheme was not "Signature"'); + + if (!parsed.params.keyId) + throw new InvalidHeaderError('keyId was not specified'); + + if (!parsed.params.algorithm) + throw new InvalidHeaderError('algorithm was not specified'); + + if (!parsed.params.signature) + throw new InvalidHeaderError('signature was not specified'); + + // Check the algorithm against the official list + parsed.params.algorithm = parsed.params.algorithm.toLowerCase(); + try { + validateAlgorithm(parsed.params.algorithm); + } catch (e) { + if (e instanceof InvalidAlgorithmError) + throw (new InvalidParamsError(parsed.params.algorithm + ' is not ' + + 'supported')); + else + throw (e); + } + + // Build the signingString + for (i = 0; i < parsed.params.headers.length; i++) { + var h = parsed.params.headers[i].toLowerCase(); + parsed.params.headers[i] = h; + + if (h === 'request-line') { + if (!options.strict) { + /* + * We allow headers from the older spec drafts if strict parsing isn't + * specified in options. + */ + parsed.signingString += + request.method + ' ' + request.url + ' HTTP/' + request.httpVersion; + } else { + /* Strict parsing doesn't allow older draft headers. */ + throw (new StrictParsingError('request-line is not a valid header ' + + 'with strict parsing enabled.')); + } + } else if (h === '(request-target)') { + parsed.signingString += + '(request-target): ' + request.method.toLowerCase() + ' ' + + request.url; + } else { + var value = request.headers[h]; + if (value === undefined) + throw new MissingHeaderError(h + ' was not in the request'); + parsed.signingString += h + ': ' + value; + } + + if ((i + 1) < parsed.params.headers.length) + parsed.signingString += '\n'; + } + + // Check against the constraints + var date; + if (request.headers.date || request.headers['x-date']) { + if (request.headers['x-date']) { + date = new Date(request.headers['x-date']); + } else { + date = new Date(request.headers.date); + } + var now = new Date(); + var skew = Math.abs(now.getTime() - date.getTime()); + + if (skew > options.clockSkew * 1000) { + throw new ExpiredRequestError('clock skew of ' + + (skew / 1000) + + 's was greater than ' + + options.clockSkew + 's'); + } + } + + options.headers.forEach(function (hdr) { + // Remember that we already checked any headers in the params + // were in the request, so if this passes we're good. + if (parsed.params.headers.indexOf(hdr.toLowerCase()) < 0) + throw new MissingHeaderError(hdr + ' was not a signed header'); + }); + + if (options.algorithms) { + if (options.algorithms.indexOf(parsed.params.algorithm) === -1) + throw new InvalidParamsError(parsed.params.algorithm + + ' is not a supported algorithm'); + } + + parsed.algorithm = parsed.params.algorithm.toUpperCase(); + parsed.keyId = parsed.params.keyId; + return parsed; + } + +}; + + +/***/ }), + +/***/ 343: +/***/ (function(module) { + +"use strict"; + +module.exports = function generate_properties(it, $keyword, $ruleType) { + var out = ' '; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $data = 'data' + ($dataLvl || ''); + var $errs = 'errs__' + $lvl; + var $it = it.util.copy(it); + var $closingBraces = ''; + $it.level++; + var $nextValid = 'valid' + $it.level; + var $key = 'key' + $lvl, + $idx = 'idx' + $lvl, + $dataNxt = $it.dataLevel = it.dataLevel + 1, + $nextData = 'data' + $dataNxt, + $dataProperties = 'dataProperties' + $lvl; + var $schemaKeys = Object.keys($schema || {}), + $pProperties = it.schema.patternProperties || {}, + $pPropertyKeys = Object.keys($pProperties), + $aProperties = it.schema.additionalProperties, + $someProperties = $schemaKeys.length || $pPropertyKeys.length, + $noAdditional = $aProperties === false, + $additionalIsSchema = typeof $aProperties == 'object' && Object.keys($aProperties).length, + $removeAdditional = it.opts.removeAdditional, + $checkAdditional = $noAdditional || $additionalIsSchema || $removeAdditional, + $ownProperties = it.opts.ownProperties, + $currentBaseId = it.baseId; + var $required = it.schema.required; + if ($required && !(it.opts.$data && $required.$data) && $required.length < it.opts.loopRequired) var $requiredHash = it.util.toHash($required); + out += 'var ' + ($errs) + ' = errors;var ' + ($nextValid) + ' = true;'; + if ($ownProperties) { + out += ' var ' + ($dataProperties) + ' = undefined;'; + } + if ($checkAdditional) { + if ($ownProperties) { + out += ' ' + ($dataProperties) + ' = ' + ($dataProperties) + ' || Object.keys(' + ($data) + '); for (var ' + ($idx) + '=0; ' + ($idx) + '<' + ($dataProperties) + '.length; ' + ($idx) + '++) { var ' + ($key) + ' = ' + ($dataProperties) + '[' + ($idx) + ']; '; + } else { + out += ' for (var ' + ($key) + ' in ' + ($data) + ') { '; + } + if ($someProperties) { + out += ' var isAdditional' + ($lvl) + ' = !(false '; + if ($schemaKeys.length) { + if ($schemaKeys.length > 8) { + out += ' || validate.schema' + ($schemaPath) + '.hasOwnProperty(' + ($key) + ') '; + } else { + var arr1 = $schemaKeys; + if (arr1) { + var $propertyKey, i1 = -1, + l1 = arr1.length - 1; + while (i1 < l1) { + $propertyKey = arr1[i1 += 1]; + out += ' || ' + ($key) + ' == ' + (it.util.toQuotedString($propertyKey)) + ' '; + } + } + } + } + if ($pPropertyKeys.length) { + var arr2 = $pPropertyKeys; + if (arr2) { + var $pProperty, $i = -1, + l2 = arr2.length - 1; + while ($i < l2) { + $pProperty = arr2[$i += 1]; + out += ' || ' + (it.usePattern($pProperty)) + '.test(' + ($key) + ') '; + } + } + } + out += ' ); if (isAdditional' + ($lvl) + ') { '; + } + if ($removeAdditional == 'all') { + out += ' delete ' + ($data) + '[' + ($key) + ']; '; + } else { + var $currentErrorPath = it.errorPath; + var $additionalProperty = '\' + ' + $key + ' + \''; + if (it.opts._errorDataPathProperty) { + it.errorPath = it.util.getPathExpr(it.errorPath, $key, it.opts.jsonPointers); + } + if ($noAdditional) { + if ($removeAdditional) { + out += ' delete ' + ($data) + '[' + ($key) + ']; '; + } else { + out += ' ' + ($nextValid) + ' = false; '; + var $currErrSchemaPath = $errSchemaPath; + $errSchemaPath = it.errSchemaPath + '/additionalProperties'; + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('additionalProperties') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { additionalProperty: \'' + ($additionalProperty) + '\' } '; + if (it.opts.messages !== false) { + out += ' , message: \''; + if (it.opts._errorDataPathProperty) { + out += 'is an invalid additional property'; + } else { + out += 'should NOT have additional properties'; + } + out += '\' '; + } + if (it.opts.verbose) { + out += ' , schema: false , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + $errSchemaPath = $currErrSchemaPath; + if ($breakOnError) { + out += ' break; '; + } + } + } else if ($additionalIsSchema) { + if ($removeAdditional == 'failing') { + out += ' var ' + ($errs) + ' = errors; '; + var $wasComposite = it.compositeRule; + it.compositeRule = $it.compositeRule = true; + $it.schema = $aProperties; + $it.schemaPath = it.schemaPath + '.additionalProperties'; + $it.errSchemaPath = it.errSchemaPath + '/additionalProperties'; + $it.errorPath = it.opts._errorDataPathProperty ? it.errorPath : it.util.getPathExpr(it.errorPath, $key, it.opts.jsonPointers); + var $passData = $data + '[' + $key + ']'; + $it.dataPathArr[$dataNxt] = $key; + var $code = it.validate($it); + $it.baseId = $currentBaseId; + if (it.util.varOccurences($code, $nextData) < 2) { + out += ' ' + (it.util.varReplace($code, $nextData, $passData)) + ' '; + } else { + out += ' var ' + ($nextData) + ' = ' + ($passData) + '; ' + ($code) + ' '; + } + out += ' if (!' + ($nextValid) + ') { errors = ' + ($errs) + '; if (validate.errors !== null) { if (errors) validate.errors.length = errors; else validate.errors = null; } delete ' + ($data) + '[' + ($key) + ']; } '; + it.compositeRule = $it.compositeRule = $wasComposite; + } else { + $it.schema = $aProperties; + $it.schemaPath = it.schemaPath + '.additionalProperties'; + $it.errSchemaPath = it.errSchemaPath + '/additionalProperties'; + $it.errorPath = it.opts._errorDataPathProperty ? it.errorPath : it.util.getPathExpr(it.errorPath, $key, it.opts.jsonPointers); + var $passData = $data + '[' + $key + ']'; + $it.dataPathArr[$dataNxt] = $key; + var $code = it.validate($it); + $it.baseId = $currentBaseId; + if (it.util.varOccurences($code, $nextData) < 2) { + out += ' ' + (it.util.varReplace($code, $nextData, $passData)) + ' '; + } else { + out += ' var ' + ($nextData) + ' = ' + ($passData) + '; ' + ($code) + ' '; + } + if ($breakOnError) { + out += ' if (!' + ($nextValid) + ') break; '; + } + } + } + it.errorPath = $currentErrorPath; + } + if ($someProperties) { + out += ' } '; + } + out += ' } '; + if ($breakOnError) { + out += ' if (' + ($nextValid) + ') { '; + $closingBraces += '}'; + } + } + var $useDefaults = it.opts.useDefaults && !it.compositeRule; + if ($schemaKeys.length) { + var arr3 = $schemaKeys; + if (arr3) { + var $propertyKey, i3 = -1, + l3 = arr3.length - 1; + while (i3 < l3) { + $propertyKey = arr3[i3 += 1]; + var $sch = $schema[$propertyKey]; + if ((it.opts.strictKeywords ? typeof $sch == 'object' && Object.keys($sch).length > 0 : it.util.schemaHasRules($sch, it.RULES.all))) { + var $prop = it.util.getProperty($propertyKey), + $passData = $data + $prop, + $hasDefault = $useDefaults && $sch.default !== undefined; + $it.schema = $sch; + $it.schemaPath = $schemaPath + $prop; + $it.errSchemaPath = $errSchemaPath + '/' + it.util.escapeFragment($propertyKey); + $it.errorPath = it.util.getPath(it.errorPath, $propertyKey, it.opts.jsonPointers); + $it.dataPathArr[$dataNxt] = it.util.toQuotedString($propertyKey); + var $code = it.validate($it); + $it.baseId = $currentBaseId; + if (it.util.varOccurences($code, $nextData) < 2) { + $code = it.util.varReplace($code, $nextData, $passData); + var $useData = $passData; + } else { + var $useData = $nextData; + out += ' var ' + ($nextData) + ' = ' + ($passData) + '; '; + } + if ($hasDefault) { + out += ' ' + ($code) + ' '; + } else { + if ($requiredHash && $requiredHash[$propertyKey]) { + out += ' if ( ' + ($useData) + ' === undefined '; + if ($ownProperties) { + out += ' || ! Object.prototype.hasOwnProperty.call(' + ($data) + ', \'' + (it.util.escapeQuotes($propertyKey)) + '\') '; + } + out += ') { ' + ($nextValid) + ' = false; '; + var $currentErrorPath = it.errorPath, + $currErrSchemaPath = $errSchemaPath, + $missingProperty = it.util.escapeQuotes($propertyKey); + if (it.opts._errorDataPathProperty) { + it.errorPath = it.util.getPath($currentErrorPath, $propertyKey, it.opts.jsonPointers); + } + $errSchemaPath = it.errSchemaPath + '/required'; + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('required') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { missingProperty: \'' + ($missingProperty) + '\' } '; + if (it.opts.messages !== false) { + out += ' , message: \''; + if (it.opts._errorDataPathProperty) { + out += 'is a required property'; + } else { + out += 'should have required property \\\'' + ($missingProperty) + '\\\''; + } + out += '\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + $errSchemaPath = $currErrSchemaPath; + it.errorPath = $currentErrorPath; + out += ' } else { '; + } else { + if ($breakOnError) { + out += ' if ( ' + ($useData) + ' === undefined '; + if ($ownProperties) { + out += ' || ! Object.prototype.hasOwnProperty.call(' + ($data) + ', \'' + (it.util.escapeQuotes($propertyKey)) + '\') '; + } + out += ') { ' + ($nextValid) + ' = true; } else { '; + } else { + out += ' if (' + ($useData) + ' !== undefined '; + if ($ownProperties) { + out += ' && Object.prototype.hasOwnProperty.call(' + ($data) + ', \'' + (it.util.escapeQuotes($propertyKey)) + '\') '; + } + out += ' ) { '; + } + } + out += ' ' + ($code) + ' } '; + } + } + if ($breakOnError) { + out += ' if (' + ($nextValid) + ') { '; + $closingBraces += '}'; + } + } + } + } + if ($pPropertyKeys.length) { + var arr4 = $pPropertyKeys; + if (arr4) { + var $pProperty, i4 = -1, + l4 = arr4.length - 1; + while (i4 < l4) { + $pProperty = arr4[i4 += 1]; + var $sch = $pProperties[$pProperty]; + if ((it.opts.strictKeywords ? typeof $sch == 'object' && Object.keys($sch).length > 0 : it.util.schemaHasRules($sch, it.RULES.all))) { + $it.schema = $sch; + $it.schemaPath = it.schemaPath + '.patternProperties' + it.util.getProperty($pProperty); + $it.errSchemaPath = it.errSchemaPath + '/patternProperties/' + it.util.escapeFragment($pProperty); + if ($ownProperties) { + out += ' ' + ($dataProperties) + ' = ' + ($dataProperties) + ' || Object.keys(' + ($data) + '); for (var ' + ($idx) + '=0; ' + ($idx) + '<' + ($dataProperties) + '.length; ' + ($idx) + '++) { var ' + ($key) + ' = ' + ($dataProperties) + '[' + ($idx) + ']; '; + } else { + out += ' for (var ' + ($key) + ' in ' + ($data) + ') { '; + } + out += ' if (' + (it.usePattern($pProperty)) + '.test(' + ($key) + ')) { '; + $it.errorPath = it.util.getPathExpr(it.errorPath, $key, it.opts.jsonPointers); + var $passData = $data + '[' + $key + ']'; + $it.dataPathArr[$dataNxt] = $key; + var $code = it.validate($it); + $it.baseId = $currentBaseId; + if (it.util.varOccurences($code, $nextData) < 2) { + out += ' ' + (it.util.varReplace($code, $nextData, $passData)) + ' '; + } else { + out += ' var ' + ($nextData) + ' = ' + ($passData) + '; ' + ($code) + ' '; + } + if ($breakOnError) { + out += ' if (!' + ($nextValid) + ') break; '; + } + out += ' } '; + if ($breakOnError) { + out += ' else ' + ($nextValid) + ' = true; '; + } + out += ' } '; + if ($breakOnError) { + out += ' if (' + ($nextValid) + ') { '; + $closingBraces += '}'; + } + } + } + } + } + if ($breakOnError) { + out += ' ' + ($closingBraces) + ' if (' + ($errs) + ' == errors) {'; + } + out = it.util.cleanUpCode(out); + return out; +} + + +/***/ }), + +/***/ 348: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +/* + * lib/jsprim.js: utilities for primitive JavaScript types + */ + +var mod_assert = __webpack_require__(477); +var mod_util = __webpack_require__(669); + +var mod_extsprintf = __webpack_require__(697); +var mod_verror = __webpack_require__(956); +var mod_jsonschema = __webpack_require__(703); + +/* + * Public interface + */ +exports.deepCopy = deepCopy; +exports.deepEqual = deepEqual; +exports.isEmpty = isEmpty; +exports.hasKey = hasKey; +exports.forEachKey = forEachKey; +exports.pluck = pluck; +exports.flattenObject = flattenObject; +exports.flattenIter = flattenIter; +exports.validateJsonObject = validateJsonObjectJS; +exports.validateJsonObjectJS = validateJsonObjectJS; +exports.randElt = randElt; +exports.extraProperties = extraProperties; +exports.mergeObjects = mergeObjects; + +exports.startsWith = startsWith; +exports.endsWith = endsWith; + +exports.parseInteger = parseInteger; + +exports.iso8601 = iso8601; +exports.rfc1123 = rfc1123; +exports.parseDateTime = parseDateTime; + +exports.hrtimediff = hrtimeDiff; +exports.hrtimeDiff = hrtimeDiff; +exports.hrtimeAccum = hrtimeAccum; +exports.hrtimeAdd = hrtimeAdd; +exports.hrtimeNanosec = hrtimeNanosec; +exports.hrtimeMicrosec = hrtimeMicrosec; +exports.hrtimeMillisec = hrtimeMillisec; + + +/* + * Deep copy an acyclic *basic* Javascript object. This only handles basic + * scalars (strings, numbers, booleans) and arbitrarily deep arrays and objects + * containing these. This does *not* handle instances of other classes. + */ +function deepCopy(obj) +{ + var ret, key; + var marker = '__deepCopy'; + + if (obj && obj[marker]) + throw (new Error('attempted deep copy of cyclic object')); + + if (obj && obj.constructor == Object) { + ret = {}; + obj[marker] = true; + + for (key in obj) { + if (key == marker) + continue; + + ret[key] = deepCopy(obj[key]); + } + + delete (obj[marker]); + return (ret); + } + + if (obj && obj.constructor == Array) { + ret = []; + obj[marker] = true; + + for (key = 0; key < obj.length; key++) + ret.push(deepCopy(obj[key])); + + delete (obj[marker]); + return (ret); + } + + /* + * It must be a primitive type -- just return it. + */ + return (obj); +} + +function deepEqual(obj1, obj2) +{ + if (typeof (obj1) != typeof (obj2)) + return (false); + + if (obj1 === null || obj2 === null || typeof (obj1) != 'object') + return (obj1 === obj2); + + if (obj1.constructor != obj2.constructor) + return (false); + + var k; + for (k in obj1) { + if (!obj2.hasOwnProperty(k)) + return (false); + + if (!deepEqual(obj1[k], obj2[k])) + return (false); + } + + for (k in obj2) { + if (!obj1.hasOwnProperty(k)) + return (false); + } + + return (true); +} + +function isEmpty(obj) +{ + var key; + for (key in obj) + return (false); + return (true); +} + +function hasKey(obj, key) +{ + mod_assert.equal(typeof (key), 'string'); + return (Object.prototype.hasOwnProperty.call(obj, key)); +} + +function forEachKey(obj, callback) +{ + for (var key in obj) { + if (hasKey(obj, key)) { + callback(key, obj[key]); + } + } +} + +function pluck(obj, key) +{ + mod_assert.equal(typeof (key), 'string'); + return (pluckv(obj, key)); +} + +function pluckv(obj, key) +{ + if (obj === null || typeof (obj) !== 'object') + return (undefined); + + if (obj.hasOwnProperty(key)) + return (obj[key]); + + var i = key.indexOf('.'); + if (i == -1) + return (undefined); + + var key1 = key.substr(0, i); + if (!obj.hasOwnProperty(key1)) + return (undefined); + + return (pluckv(obj[key1], key.substr(i + 1))); +} + +/* + * Invoke callback(row) for each entry in the array that would be returned by + * flattenObject(data, depth). This is just like flattenObject(data, + * depth).forEach(callback), except that the intermediate array is never + * created. + */ +function flattenIter(data, depth, callback) +{ + doFlattenIter(data, depth, [], callback); +} + +function doFlattenIter(data, depth, accum, callback) +{ + var each; + var key; + + if (depth === 0) { + each = accum.slice(0); + each.push(data); + callback(each); + return; + } + + mod_assert.ok(data !== null); + mod_assert.equal(typeof (data), 'object'); + mod_assert.equal(typeof (depth), 'number'); + mod_assert.ok(depth >= 0); + + for (key in data) { + each = accum.slice(0); + each.push(key); + doFlattenIter(data[key], depth - 1, each, callback); + } +} + +function flattenObject(data, depth) +{ + if (depth === 0) + return ([ data ]); + + mod_assert.ok(data !== null); + mod_assert.equal(typeof (data), 'object'); + mod_assert.equal(typeof (depth), 'number'); + mod_assert.ok(depth >= 0); + + var rv = []; + var key; + + for (key in data) { + flattenObject(data[key], depth - 1).forEach(function (p) { + rv.push([ key ].concat(p)); + }); + } + + return (rv); +} + +function startsWith(str, prefix) +{ + return (str.substr(0, prefix.length) == prefix); +} + +function endsWith(str, suffix) +{ + return (str.substr( + str.length - suffix.length, suffix.length) == suffix); +} + +function iso8601(d) +{ + if (typeof (d) == 'number') + d = new Date(d); + mod_assert.ok(d.constructor === Date); + return (mod_extsprintf.sprintf('%4d-%02d-%02dT%02d:%02d:%02d.%03dZ', + d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate(), + d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), + d.getUTCMilliseconds())); +} + +var RFC1123_MONTHS = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; +var RFC1123_DAYS = [ + 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + +function rfc1123(date) { + return (mod_extsprintf.sprintf('%s, %02d %s %04d %02d:%02d:%02d GMT', + RFC1123_DAYS[date.getUTCDay()], date.getUTCDate(), + RFC1123_MONTHS[date.getUTCMonth()], date.getUTCFullYear(), + date.getUTCHours(), date.getUTCMinutes(), + date.getUTCSeconds())); +} + +/* + * Parses a date expressed as a string, as either a number of milliseconds since + * the epoch or any string format that Date accepts, giving preference to the + * former where these two sets overlap (e.g., small numbers). + */ +function parseDateTime(str) +{ + /* + * This is irritatingly implicit, but significantly more concise than + * alternatives. The "+str" will convert a string containing only a + * number directly to a Number, or NaN for other strings. Thus, if the + * conversion succeeds, we use it (this is the milliseconds-since-epoch + * case). Otherwise, we pass the string directly to the Date + * constructor to parse. + */ + var numeric = +str; + if (!isNaN(numeric)) { + return (new Date(numeric)); + } else { + return (new Date(str)); + } +} + + +/* + * Number.*_SAFE_INTEGER isn't present before node v0.12, so we hardcode + * the ES6 definitions here, while allowing for them to someday be higher. + */ +var MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER || 9007199254740991; +var MIN_SAFE_INTEGER = Number.MIN_SAFE_INTEGER || -9007199254740991; + + +/* + * Default options for parseInteger(). + */ +var PI_DEFAULTS = { + base: 10, + allowSign: true, + allowPrefix: false, + allowTrailing: false, + allowImprecise: false, + trimWhitespace: false, + leadingZeroIsOctal: false +}; + +var CP_0 = 0x30; +var CP_9 = 0x39; + +var CP_A = 0x41; +var CP_B = 0x42; +var CP_O = 0x4f; +var CP_T = 0x54; +var CP_X = 0x58; +var CP_Z = 0x5a; + +var CP_a = 0x61; +var CP_b = 0x62; +var CP_o = 0x6f; +var CP_t = 0x74; +var CP_x = 0x78; +var CP_z = 0x7a; + +var PI_CONV_DEC = 0x30; +var PI_CONV_UC = 0x37; +var PI_CONV_LC = 0x57; + + +/* + * A stricter version of parseInt() that provides options for changing what + * is an acceptable string (for example, disallowing trailing characters). + */ +function parseInteger(str, uopts) +{ + mod_assert.string(str, 'str'); + mod_assert.optionalObject(uopts, 'options'); + + var baseOverride = false; + var options = PI_DEFAULTS; + + if (uopts) { + baseOverride = hasKey(uopts, 'base'); + options = mergeObjects(options, uopts); + mod_assert.number(options.base, 'options.base'); + mod_assert.ok(options.base >= 2, 'options.base >= 2'); + mod_assert.ok(options.base <= 36, 'options.base <= 36'); + mod_assert.bool(options.allowSign, 'options.allowSign'); + mod_assert.bool(options.allowPrefix, 'options.allowPrefix'); + mod_assert.bool(options.allowTrailing, + 'options.allowTrailing'); + mod_assert.bool(options.allowImprecise, + 'options.allowImprecise'); + mod_assert.bool(options.trimWhitespace, + 'options.trimWhitespace'); + mod_assert.bool(options.leadingZeroIsOctal, + 'options.leadingZeroIsOctal'); + + if (options.leadingZeroIsOctal) { + mod_assert.ok(!baseOverride, + '"base" and "leadingZeroIsOctal" are ' + + 'mutually exclusive'); + } + } + + var c; + var pbase = -1; + var base = options.base; + var start; + var mult = 1; + var value = 0; + var idx = 0; + var len = str.length; + + /* Trim any whitespace on the left side. */ + if (options.trimWhitespace) { + while (idx < len && isSpace(str.charCodeAt(idx))) { + ++idx; + } + } + + /* Check the number for a leading sign. */ + if (options.allowSign) { + if (str[idx] === '-') { + idx += 1; + mult = -1; + } else if (str[idx] === '+') { + idx += 1; + } + } + + /* Parse the base-indicating prefix if there is one. */ + if (str[idx] === '0') { + if (options.allowPrefix) { + pbase = prefixToBase(str.charCodeAt(idx + 1)); + if (pbase !== -1 && (!baseOverride || pbase === base)) { + base = pbase; + idx += 2; + } + } + + if (pbase === -1 && options.leadingZeroIsOctal) { + base = 8; + } + } + + /* Parse the actual digits. */ + for (start = idx; idx < len; ++idx) { + c = translateDigit(str.charCodeAt(idx)); + if (c !== -1 && c < base) { + value *= base; + value += c; + } else { + break; + } + } + + /* If we didn't parse any digits, we have an invalid number. */ + if (start === idx) { + return (new Error('invalid number: ' + JSON.stringify(str))); + } + + /* Trim any whitespace on the right side. */ + if (options.trimWhitespace) { + while (idx < len && isSpace(str.charCodeAt(idx))) { + ++idx; + } + } + + /* Check for trailing characters. */ + if (idx < len && !options.allowTrailing) { + return (new Error('trailing characters after number: ' + + JSON.stringify(str.slice(idx)))); + } + + /* If our value is 0, we return now, to avoid returning -0. */ + if (value === 0) { + return (0); + } + + /* Calculate our final value. */ + var result = value * mult; + + /* + * If the string represents a value that cannot be precisely represented + * by JavaScript, then we want to check that: + * + * - We never increased the value past MAX_SAFE_INTEGER + * - We don't make the result negative and below MIN_SAFE_INTEGER + * + * Because we only ever increment the value during parsing, there's no + * chance of moving past MAX_SAFE_INTEGER and then dropping below it + * again, losing precision in the process. This means that we only need + * to do our checks here, at the end. + */ + if (!options.allowImprecise && + (value > MAX_SAFE_INTEGER || result < MIN_SAFE_INTEGER)) { + return (new Error('number is outside of the supported range: ' + + JSON.stringify(str.slice(start, idx)))); + } + + return (result); +} + + +/* + * Interpret a character code as a base-36 digit. + */ +function translateDigit(d) +{ + if (d >= CP_0 && d <= CP_9) { + /* '0' to '9' -> 0 to 9 */ + return (d - PI_CONV_DEC); + } else if (d >= CP_A && d <= CP_Z) { + /* 'A' - 'Z' -> 10 to 35 */ + return (d - PI_CONV_UC); + } else if (d >= CP_a && d <= CP_z) { + /* 'a' - 'z' -> 10 to 35 */ + return (d - PI_CONV_LC); + } else { + /* Invalid character code */ + return (-1); + } +} + + +/* + * Test if a value matches the ECMAScript definition of trimmable whitespace. + */ +function isSpace(c) +{ + return (c === 0x20) || + (c >= 0x0009 && c <= 0x000d) || + (c === 0x00a0) || + (c === 0x1680) || + (c === 0x180e) || + (c >= 0x2000 && c <= 0x200a) || + (c === 0x2028) || + (c === 0x2029) || + (c === 0x202f) || + (c === 0x205f) || + (c === 0x3000) || + (c === 0xfeff); +} + + +/* + * Determine which base a character indicates (e.g., 'x' indicates hex). + */ +function prefixToBase(c) +{ + if (c === CP_b || c === CP_B) { + /* 0b/0B (binary) */ + return (2); + } else if (c === CP_o || c === CP_O) { + /* 0o/0O (octal) */ + return (8); + } else if (c === CP_t || c === CP_T) { + /* 0t/0T (decimal) */ + return (10); + } else if (c === CP_x || c === CP_X) { + /* 0x/0X (hexadecimal) */ + return (16); + } else { + /* Not a meaningful character */ + return (-1); + } +} + + +function validateJsonObjectJS(schema, input) +{ + var report = mod_jsonschema.validate(input, schema); + + if (report.errors.length === 0) + return (null); + + /* Currently, we only do anything useful with the first error. */ + var error = report.errors[0]; + + /* The failed property is given by a URI with an irrelevant prefix. */ + var propname = error['property']; + var reason = error['message'].toLowerCase(); + var i, j; + + /* + * There's at least one case where the property error message is + * confusing at best. We work around this here. + */ + if ((i = reason.indexOf('the property ')) != -1 && + (j = reason.indexOf(' is not defined in the schema and the ' + + 'schema does not allow additional properties')) != -1) { + i += 'the property '.length; + if (propname === '') + propname = reason.substr(i, j - i); + else + propname = propname + '.' + reason.substr(i, j - i); + + reason = 'unsupported property'; + } + + var rv = new mod_verror.VError('property "%s": %s', propname, reason); + rv.jsv_details = error; + return (rv); +} + +function randElt(arr) +{ + mod_assert.ok(Array.isArray(arr) && arr.length > 0, + 'randElt argument must be a non-empty array'); + + return (arr[Math.floor(Math.random() * arr.length)]); +} + +function assertHrtime(a) +{ + mod_assert.ok(a[0] >= 0 && a[1] >= 0, + 'negative numbers not allowed in hrtimes'); + mod_assert.ok(a[1] < 1e9, 'nanoseconds column overflow'); +} + +/* + * Compute the time elapsed between hrtime readings A and B, where A is later + * than B. hrtime readings come from Node's process.hrtime(). There is no + * defined way to represent negative deltas, so it's illegal to diff B from A + * where the time denoted by B is later than the time denoted by A. If this + * becomes valuable, we can define a representation and extend the + * implementation to support it. + */ +function hrtimeDiff(a, b) +{ + assertHrtime(a); + assertHrtime(b); + mod_assert.ok(a[0] > b[0] || (a[0] == b[0] && a[1] >= b[1]), + 'negative differences not allowed'); + + var rv = [ a[0] - b[0], 0 ]; + + if (a[1] >= b[1]) { + rv[1] = a[1] - b[1]; + } else { + rv[0]--; + rv[1] = 1e9 - (b[1] - a[1]); + } + + return (rv); +} + +/* + * Convert a hrtime reading from the array format returned by Node's + * process.hrtime() into a scalar number of nanoseconds. + */ +function hrtimeNanosec(a) +{ + assertHrtime(a); + + return (Math.floor(a[0] * 1e9 + a[1])); +} + +/* + * Convert a hrtime reading from the array format returned by Node's + * process.hrtime() into a scalar number of microseconds. + */ +function hrtimeMicrosec(a) +{ + assertHrtime(a); + + return (Math.floor(a[0] * 1e6 + a[1] / 1e3)); +} + +/* + * Convert a hrtime reading from the array format returned by Node's + * process.hrtime() into a scalar number of milliseconds. + */ +function hrtimeMillisec(a) +{ + assertHrtime(a); + + return (Math.floor(a[0] * 1e3 + a[1] / 1e6)); +} + +/* + * Add two hrtime readings A and B, overwriting A with the result of the + * addition. This function is useful for accumulating several hrtime intervals + * into a counter. Returns A. + */ +function hrtimeAccum(a, b) +{ + assertHrtime(a); + assertHrtime(b); + + /* + * Accumulate the nanosecond component. + */ + a[1] += b[1]; + if (a[1] >= 1e9) { + /* + * The nanosecond component overflowed, so carry to the seconds + * field. + */ + a[0]++; + a[1] -= 1e9; + } + + /* + * Accumulate the seconds component. + */ + a[0] += b[0]; + + return (a); +} + +/* + * Add two hrtime readings A and B, returning the result as a new hrtime array. + * Does not modify either input argument. + */ +function hrtimeAdd(a, b) +{ + assertHrtime(a); + + var rv = [ a[0], a[1] ]; + + return (hrtimeAccum(rv, b)); +} + + +/* + * Check an object for unexpected properties. Accepts the object to check, and + * an array of allowed property names (strings). Returns an array of key names + * that were found on the object, but did not appear in the list of allowed + * properties. If no properties were found, the returned array will be of + * zero length. + */ +function extraProperties(obj, allowed) +{ + mod_assert.ok(typeof (obj) === 'object' && obj !== null, + 'obj argument must be a non-null object'); + mod_assert.ok(Array.isArray(allowed), + 'allowed argument must be an array of strings'); + for (var i = 0; i < allowed.length; i++) { + mod_assert.ok(typeof (allowed[i]) === 'string', + 'allowed argument must be an array of strings'); + } + + return (Object.keys(obj).filter(function (key) { + return (allowed.indexOf(key) === -1); + })); +} + +/* + * Given three sets of properties "provided" (may be undefined), "overrides" + * (required), and "defaults" (may be undefined), construct an object containing + * the union of these sets with "overrides" overriding "provided", and + * "provided" overriding "defaults". None of the input objects are modified. + */ +function mergeObjects(provided, overrides, defaults) +{ + var rv, k; + + rv = {}; + if (defaults) { + for (k in defaults) + rv[k] = defaults[k]; + } + + if (provided) { + for (k in provided) + rv[k] = provided[k]; + } + + if (overrides) { + for (k in overrides) + rv[k] = overrides[k]; + } + + return (rv); +} + + +/***/ }), + +/***/ 349: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; +/*! + * Copyright (c) 2015, Salesforce.com, Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of Salesforce.com nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +var Store = __webpack_require__(627).Store; +var permuteDomain = __webpack_require__(383).permuteDomain; +var pathMatch = __webpack_require__(54).pathMatch; +var util = __webpack_require__(669); + +function MemoryCookieStore() { + Store.call(this); + this.idx = {}; +} +util.inherits(MemoryCookieStore, Store); +exports.MemoryCookieStore = MemoryCookieStore; +MemoryCookieStore.prototype.idx = null; + +// Since it's just a struct in RAM, this Store is synchronous +MemoryCookieStore.prototype.synchronous = true; + +// force a default depth: +MemoryCookieStore.prototype.inspect = function() { + return "{ idx: "+util.inspect(this.idx, false, 2)+' }'; +}; + +// Use the new custom inspection symbol to add the custom inspect function if +// available. +if (util.inspect.custom) { + MemoryCookieStore.prototype[util.inspect.custom] = MemoryCookieStore.prototype.inspect; +} + +MemoryCookieStore.prototype.findCookie = function(domain, path, key, cb) { + if (!this.idx[domain]) { + return cb(null,undefined); + } + if (!this.idx[domain][path]) { + return cb(null,undefined); + } + return cb(null,this.idx[domain][path][key]||null); +}; + +MemoryCookieStore.prototype.findCookies = function(domain, path, cb) { + var results = []; + if (!domain) { + return cb(null,[]); + } + + var pathMatcher; + if (!path) { + // null means "all paths" + pathMatcher = function matchAll(domainIndex) { + for (var curPath in domainIndex) { + var pathIndex = domainIndex[curPath]; + for (var key in pathIndex) { + results.push(pathIndex[key]); + } + } + }; + + } else { + pathMatcher = function matchRFC(domainIndex) { + //NOTE: we should use path-match algorithm from S5.1.4 here + //(see : https://github.com/ChromiumWebApps/chromium/blob/b3d3b4da8bb94c1b2e061600df106d590fda3620/net/cookies/canonical_cookie.cc#L299) + Object.keys(domainIndex).forEach(function (cookiePath) { + if (pathMatch(path, cookiePath)) { + var pathIndex = domainIndex[cookiePath]; + + for (var key in pathIndex) { + results.push(pathIndex[key]); + } + } + }); + }; + } + + var domains = permuteDomain(domain) || [domain]; + var idx = this.idx; + domains.forEach(function(curDomain) { + var domainIndex = idx[curDomain]; + if (!domainIndex) { + return; + } + pathMatcher(domainIndex); + }); + + cb(null,results); +}; + +MemoryCookieStore.prototype.putCookie = function(cookie, cb) { + if (!this.idx[cookie.domain]) { + this.idx[cookie.domain] = {}; + } + if (!this.idx[cookie.domain][cookie.path]) { + this.idx[cookie.domain][cookie.path] = {}; + } + this.idx[cookie.domain][cookie.path][cookie.key] = cookie; + cb(null); +}; + +MemoryCookieStore.prototype.updateCookie = function(oldCookie, newCookie, cb) { + // updateCookie() may avoid updating cookies that are identical. For example, + // lastAccessed may not be important to some stores and an equality + // comparison could exclude that field. + this.putCookie(newCookie,cb); +}; + +MemoryCookieStore.prototype.removeCookie = function(domain, path, key, cb) { + if (this.idx[domain] && this.idx[domain][path] && this.idx[domain][path][key]) { + delete this.idx[domain][path][key]; + } + cb(null); +}; + +MemoryCookieStore.prototype.removeCookies = function(domain, path, cb) { + if (this.idx[domain]) { + if (path) { + delete this.idx[domain][path]; + } else { + delete this.idx[domain]; + } + } + return cb(null); +}; + +MemoryCookieStore.prototype.removeAllCookies = function(cb) { + this.idx = {}; + return cb(null); +} + +MemoryCookieStore.prototype.getAllCookies = function(cb) { + var cookies = []; + var idx = this.idx; + + var domains = Object.keys(idx); + domains.forEach(function(domain) { + var paths = Object.keys(idx[domain]); + paths.forEach(function(path) { + var keys = Object.keys(idx[domain][path]); + keys.forEach(function(key) { + if (key !== null) { + cookies.push(idx[domain][path][key]); + } + }); + }); + }); + + // Sort by creationIndex so deserializing retains the creation order. + // When implementing your own store, this SHOULD retain the order too + cookies.sort(function(a,b) { + return (a.creationIndex||0) - (b.creationIndex||0); + }); + + cb(null, cookies); +}; + + +/***/ }), + +/***/ 357: +/***/ (function(module) { + +module.exports = require("assert"); + +/***/ }), + +/***/ 362: +/***/ (function(module) { + +// Copyright 2011 Mark Cavage All rights reserved. + + +module.exports = { + EOC: 0, + Boolean: 1, + Integer: 2, + BitString: 3, + OctetString: 4, + Null: 5, + OID: 6, + ObjectDescriptor: 7, + External: 8, + Real: 9, // float + Enumeration: 10, + PDV: 11, + Utf8String: 12, + RelativeOID: 13, + Sequence: 16, + Set: 17, + NumericString: 18, + PrintableString: 19, + T61String: 20, + VideotexString: 21, + IA5String: 22, + UTCTime: 23, + GeneralizedTime: 24, + GraphicString: 25, + VisibleString: 26, + GeneralString: 28, + UniversalString: 29, + CharacterString: 30, + BMPString: 31, + Constructor: 32, + Context: 128 +}; + + +/***/ }), + +/***/ 363: +/***/ (function(module, __unusedexports, __webpack_require__) { + +// Copyright 2015 Joyent, Inc. + +module.exports = { + Verifier: Verifier, + Signer: Signer +}; + +var nacl = __webpack_require__(196); +var stream = __webpack_require__(413); +var util = __webpack_require__(669); +var assert = __webpack_require__(477); +var Buffer = __webpack_require__(215).Buffer; +var Signature = __webpack_require__(575); + +function Verifier(key, hashAlgo) { + if (hashAlgo.toLowerCase() !== 'sha512') + throw (new Error('ED25519 only supports the use of ' + + 'SHA-512 hashes')); + + this.key = key; + this.chunks = []; + + stream.Writable.call(this, {}); +} +util.inherits(Verifier, stream.Writable); + +Verifier.prototype._write = function (chunk, enc, cb) { + this.chunks.push(chunk); + cb(); +}; + +Verifier.prototype.update = function (chunk) { + if (typeof (chunk) === 'string') + chunk = Buffer.from(chunk, 'binary'); + this.chunks.push(chunk); +}; + +Verifier.prototype.verify = function (signature, fmt) { + var sig; + if (Signature.isSignature(signature, [2, 0])) { + if (signature.type !== 'ed25519') + return (false); + sig = signature.toBuffer('raw'); + + } else if (typeof (signature) === 'string') { + sig = Buffer.from(signature, 'base64'); + + } else if (Signature.isSignature(signature, [1, 0])) { + throw (new Error('signature was created by too old ' + + 'a version of sshpk and cannot be verified')); + } + + assert.buffer(sig); + return (nacl.sign.detached.verify( + new Uint8Array(Buffer.concat(this.chunks)), + new Uint8Array(sig), + new Uint8Array(this.key.part.A.data))); +}; + +function Signer(key, hashAlgo) { + if (hashAlgo.toLowerCase() !== 'sha512') + throw (new Error('ED25519 only supports the use of ' + + 'SHA-512 hashes')); + + this.key = key; + this.chunks = []; + + stream.Writable.call(this, {}); +} +util.inherits(Signer, stream.Writable); + +Signer.prototype._write = function (chunk, enc, cb) { + this.chunks.push(chunk); + cb(); +}; + +Signer.prototype.update = function (chunk) { + if (typeof (chunk) === 'string') + chunk = Buffer.from(chunk, 'binary'); + this.chunks.push(chunk); +}; + +Signer.prototype.sign = function () { + var sig = nacl.sign.detached( + new Uint8Array(Buffer.concat(this.chunks)), + new Uint8Array(Buffer.concat([ + this.key.part.k.data, this.key.part.A.data]))); + var sigBuf = Buffer.from(sig); + var sigObj = Signature.parse(sigBuf, 'ed25519', 'raw'); + sigObj.hashAlgorithm = 'sha512'; + return (sigObj); +}; + + +/***/ }), + +/***/ 374: +/***/ (function(module) { + +"use strict"; + + +var hasOwn = Object.prototype.hasOwnProperty; +var toStr = Object.prototype.toString; +var defineProperty = Object.defineProperty; +var gOPD = Object.getOwnPropertyDescriptor; + +var isArray = function isArray(arr) { + if (typeof Array.isArray === 'function') { + return Array.isArray(arr); + } + + return toStr.call(arr) === '[object Array]'; +}; + +var isPlainObject = function isPlainObject(obj) { + if (!obj || toStr.call(obj) !== '[object Object]') { + return false; + } + + var hasOwnConstructor = hasOwn.call(obj, 'constructor'); + var hasIsPrototypeOf = obj.constructor && obj.constructor.prototype && hasOwn.call(obj.constructor.prototype, 'isPrototypeOf'); + // Not own constructor property must be Object + if (obj.constructor && !hasOwnConstructor && !hasIsPrototypeOf) { + return false; + } + + // Own properties are enumerated firstly, so to speed up, + // if last one is own, then all properties are own. + var key; + for (key in obj) { /**/ } + + return typeof key === 'undefined' || hasOwn.call(obj, key); +}; + +// If name is '__proto__', and Object.defineProperty is available, define __proto__ as an own property on target +var setProperty = function setProperty(target, options) { + if (defineProperty && options.name === '__proto__') { + defineProperty(target, options.name, { + enumerable: true, + configurable: true, + value: options.newValue, + writable: true + }); + } else { + target[options.name] = options.newValue; + } +}; + +// Return undefined instead of __proto__ if '__proto__' is not an own property +var getProperty = function getProperty(obj, name) { + if (name === '__proto__') { + if (!hasOwn.call(obj, name)) { + return void 0; + } else if (gOPD) { + // In early versions of node, obj['__proto__'] is buggy when obj has + // __proto__ as an own property. Object.getOwnPropertyDescriptor() works. + return gOPD(obj, name).value; + } + } + + return obj[name]; +}; + +module.exports = function extend() { + var options, name, src, copy, copyIsArray, clone; + var target = arguments[0]; + var i = 1; + var length = arguments.length; + var deep = false; + + // Handle a deep copy situation + if (typeof target === 'boolean') { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } + if (target == null || (typeof target !== 'object' && typeof target !== 'function')) { + target = {}; + } + + for (; i < length; ++i) { + options = arguments[i]; + // Only deal with non-null/undefined values + if (options != null) { + // Extend the base object + for (name in options) { + src = getProperty(target, name); + copy = getProperty(options, name); + + // Prevent never-ending loop + if (target !== copy) { + // Recurse if we're merging plain objects or arrays + if (deep && copy && (isPlainObject(copy) || (copyIsArray = isArray(copy)))) { + if (copyIsArray) { + copyIsArray = false; + clone = src && isArray(src) ? src : []; + } else { + clone = src && isPlainObject(src) ? src : {}; + } + + // Never move original objects, clone them + setProperty(target, { name: name, newValue: extend(deep, clone, copy) }); + + // Don't bring in undefined values + } else if (typeof copy !== 'undefined') { + setProperty(target, { name: name, newValue: copy }); + } + } + } + } + } + + // Return the modified object + return target; +}; + + +/***/ }), + +/***/ 378: +/***/ (function(module, __unusedexports, __webpack_require__) { + +// Copyright 2017 Joyent, Inc. + +module.exports = Identity; + +var assert = __webpack_require__(477); +var algs = __webpack_require__(98); +var crypto = __webpack_require__(417); +var Fingerprint = __webpack_require__(400); +var Signature = __webpack_require__(575); +var errs = __webpack_require__(753); +var util = __webpack_require__(669); +var utils = __webpack_require__(270); +var asn1 = __webpack_require__(62); +var Buffer = __webpack_require__(215).Buffer; + +/*JSSTYLED*/ +var DNS_NAME_RE = /^([*]|[a-z0-9][a-z0-9\-]{0,62})(?:\.([*]|[a-z0-9][a-z0-9\-]{0,62}))*$/i; + +var oids = {}; +oids.cn = '2.5.4.3'; +oids.o = '2.5.4.10'; +oids.ou = '2.5.4.11'; +oids.l = '2.5.4.7'; +oids.s = '2.5.4.8'; +oids.c = '2.5.4.6'; +oids.sn = '2.5.4.4'; +oids.postalCode = '2.5.4.17'; +oids.serialNumber = '2.5.4.5'; +oids.street = '2.5.4.9'; +oids.x500UniqueIdentifier = '2.5.4.45'; +oids.role = '2.5.4.72'; +oids.telephoneNumber = '2.5.4.20'; +oids.description = '2.5.4.13'; +oids.dc = '0.9.2342.19200300.100.1.25'; +oids.uid = '0.9.2342.19200300.100.1.1'; +oids.mail = '0.9.2342.19200300.100.1.3'; +oids.title = '2.5.4.12'; +oids.gn = '2.5.4.42'; +oids.initials = '2.5.4.43'; +oids.pseudonym = '2.5.4.65'; +oids.emailAddress = '1.2.840.113549.1.9.1'; + +var unoids = {}; +Object.keys(oids).forEach(function (k) { + unoids[oids[k]] = k; +}); + +function Identity(opts) { + var self = this; + assert.object(opts, 'options'); + assert.arrayOfObject(opts.components, 'options.components'); + this.components = opts.components; + this.componentLookup = {}; + this.components.forEach(function (c) { + if (c.name && !c.oid) + c.oid = oids[c.name]; + if (c.oid && !c.name) + c.name = unoids[c.oid]; + if (self.componentLookup[c.name] === undefined) + self.componentLookup[c.name] = []; + self.componentLookup[c.name].push(c); + }); + if (this.componentLookup.cn && this.componentLookup.cn.length > 0) { + this.cn = this.componentLookup.cn[0].value; + } + assert.optionalString(opts.type, 'options.type'); + if (opts.type === undefined) { + if (this.components.length === 1 && + this.componentLookup.cn && + this.componentLookup.cn.length === 1 && + this.componentLookup.cn[0].value.match(DNS_NAME_RE)) { + this.type = 'host'; + this.hostname = this.componentLookup.cn[0].value; + + } else if (this.componentLookup.dc && + this.components.length === this.componentLookup.dc.length) { + this.type = 'host'; + this.hostname = this.componentLookup.dc.map( + function (c) { + return (c.value); + }).join('.'); + + } else if (this.componentLookup.uid && + this.components.length === + this.componentLookup.uid.length) { + this.type = 'user'; + this.uid = this.componentLookup.uid[0].value; + + } else if (this.componentLookup.cn && + this.componentLookup.cn.length === 1 && + this.componentLookup.cn[0].value.match(DNS_NAME_RE)) { + this.type = 'host'; + this.hostname = this.componentLookup.cn[0].value; + + } else if (this.componentLookup.uid && + this.componentLookup.uid.length === 1) { + this.type = 'user'; + this.uid = this.componentLookup.uid[0].value; + + } else if (this.componentLookup.mail && + this.componentLookup.mail.length === 1) { + this.type = 'email'; + this.email = this.componentLookup.mail[0].value; + + } else if (this.componentLookup.cn && + this.componentLookup.cn.length === 1) { + this.type = 'user'; + this.uid = this.componentLookup.cn[0].value; + + } else { + this.type = 'unknown'; + } + } else { + this.type = opts.type; + if (this.type === 'host') + this.hostname = opts.hostname; + else if (this.type === 'user') + this.uid = opts.uid; + else if (this.type === 'email') + this.email = opts.email; + else + throw (new Error('Unknown type ' + this.type)); + } +} + +Identity.prototype.toString = function () { + return (this.components.map(function (c) { + var n = c.name.toUpperCase(); + /*JSSTYLED*/ + n = n.replace(/=/g, '\\='); + var v = c.value; + /*JSSTYLED*/ + v = v.replace(/,/g, '\\,'); + return (n + '=' + v); + }).join(', ')); +}; + +Identity.prototype.get = function (name, asArray) { + assert.string(name, 'name'); + var arr = this.componentLookup[name]; + if (arr === undefined || arr.length === 0) + return (undefined); + if (!asArray && arr.length > 1) + throw (new Error('Multiple values for attribute ' + name)); + if (!asArray) + return (arr[0].value); + return (arr.map(function (c) { + return (c.value); + })); +}; + +Identity.prototype.toArray = function (idx) { + return (this.components.map(function (c) { + return ({ + name: c.name, + value: c.value + }); + })); +}; + +/* + * These are from X.680 -- PrintableString allowed chars are in section 37.4 + * table 8. Spec for IA5Strings is "1,6 + SPACE + DEL" where 1 refers to + * ISO IR #001 (standard ASCII control characters) and 6 refers to ISO IR #006 + * (the basic ASCII character set). + */ +/* JSSTYLED */ +var NOT_PRINTABLE = /[^a-zA-Z0-9 '(),+.\/:=?-]/; +/* JSSTYLED */ +var NOT_IA5 = /[^\x00-\x7f]/; + +Identity.prototype.toAsn1 = function (der, tag) { + der.startSequence(tag); + this.components.forEach(function (c) { + der.startSequence(asn1.Ber.Constructor | asn1.Ber.Set); + der.startSequence(); + der.writeOID(c.oid); + /* + * If we fit in a PrintableString, use that. Otherwise use an + * IA5String or UTF8String. + * + * If this identity was parsed from a DN, use the ASN.1 types + * from the original representation (otherwise this might not + * be a full match for the original in some validators). + */ + if (c.asn1type === asn1.Ber.Utf8String || + c.value.match(NOT_IA5)) { + var v = Buffer.from(c.value, 'utf8'); + der.writeBuffer(v, asn1.Ber.Utf8String); + + } else if (c.asn1type === asn1.Ber.IA5String || + c.value.match(NOT_PRINTABLE)) { + der.writeString(c.value, asn1.Ber.IA5String); + + } else { + var type = asn1.Ber.PrintableString; + if (c.asn1type !== undefined) + type = c.asn1type; + der.writeString(c.value, type); + } + der.endSequence(); + der.endSequence(); + }); + der.endSequence(); +}; + +function globMatch(a, b) { + if (a === '**' || b === '**') + return (true); + var aParts = a.split('.'); + var bParts = b.split('.'); + if (aParts.length !== bParts.length) + return (false); + for (var i = 0; i < aParts.length; ++i) { + if (aParts[i] === '*' || bParts[i] === '*') + continue; + if (aParts[i] !== bParts[i]) + return (false); + } + return (true); +} + +Identity.prototype.equals = function (other) { + if (!Identity.isIdentity(other, [1, 0])) + return (false); + if (other.components.length !== this.components.length) + return (false); + for (var i = 0; i < this.components.length; ++i) { + if (this.components[i].oid !== other.components[i].oid) + return (false); + if (!globMatch(this.components[i].value, + other.components[i].value)) { + return (false); + } + } + return (true); +}; + +Identity.forHost = function (hostname) { + assert.string(hostname, 'hostname'); + return (new Identity({ + type: 'host', + hostname: hostname, + components: [ { name: 'cn', value: hostname } ] + })); +}; + +Identity.forUser = function (uid) { + assert.string(uid, 'uid'); + return (new Identity({ + type: 'user', + uid: uid, + components: [ { name: 'uid', value: uid } ] + })); +}; + +Identity.forEmail = function (email) { + assert.string(email, 'email'); + return (new Identity({ + type: 'email', + email: email, + components: [ { name: 'mail', value: email } ] + })); +}; + +Identity.parseDN = function (dn) { + assert.string(dn, 'dn'); + var parts = ['']; + var idx = 0; + var rem = dn; + while (rem.length > 0) { + var m; + /*JSSTYLED*/ + if ((m = /^,/.exec(rem)) !== null) { + parts[++idx] = ''; + rem = rem.slice(m[0].length); + /*JSSTYLED*/ + } else if ((m = /^\\,/.exec(rem)) !== null) { + parts[idx] += ','; + rem = rem.slice(m[0].length); + /*JSSTYLED*/ + } else if ((m = /^\\./.exec(rem)) !== null) { + parts[idx] += m[0]; + rem = rem.slice(m[0].length); + /*JSSTYLED*/ + } else if ((m = /^[^\\,]+/.exec(rem)) !== null) { + parts[idx] += m[0]; + rem = rem.slice(m[0].length); + } else { + throw (new Error('Failed to parse DN')); + } + } + var cmps = parts.map(function (c) { + c = c.trim(); + var eqPos = c.indexOf('='); + while (eqPos > 0 && c.charAt(eqPos - 1) === '\\') + eqPos = c.indexOf('=', eqPos + 1); + if (eqPos === -1) { + throw (new Error('Failed to parse DN')); + } + /*JSSTYLED*/ + var name = c.slice(0, eqPos).toLowerCase().replace(/\\=/g, '='); + var value = c.slice(eqPos + 1); + return ({ name: name, value: value }); + }); + return (new Identity({ components: cmps })); +}; + +Identity.fromArray = function (components) { + assert.arrayOfObject(components, 'components'); + components.forEach(function (cmp) { + assert.object(cmp, 'component'); + assert.string(cmp.name, 'component.name'); + if (!Buffer.isBuffer(cmp.value) && + !(typeof (cmp.value) === 'string')) { + throw (new Error('Invalid component value')); + } + }); + return (new Identity({ components: components })); +}; + +Identity.parseAsn1 = function (der, top) { + var components = []; + der.readSequence(top); + var end = der.offset + der.length; + while (der.offset < end) { + der.readSequence(asn1.Ber.Constructor | asn1.Ber.Set); + var after = der.offset + der.length; + der.readSequence(); + var oid = der.readOID(); + var type = der.peek(); + var value; + switch (type) { + case asn1.Ber.PrintableString: + case asn1.Ber.IA5String: + case asn1.Ber.OctetString: + case asn1.Ber.T61String: + value = der.readString(type); + break; + case asn1.Ber.Utf8String: + value = der.readString(type, true); + value = value.toString('utf8'); + break; + case asn1.Ber.CharacterString: + case asn1.Ber.BMPString: + value = der.readString(type, true); + value = value.toString('utf16le'); + break; + default: + throw (new Error('Unknown asn1 type ' + type)); + } + components.push({ oid: oid, asn1type: type, value: value }); + der._offset = after; + } + der._offset = end; + return (new Identity({ + components: components + })); +}; + +Identity.isIdentity = function (obj, ver) { + return (utils.isCompatible(obj, Identity, ver)); +}; + +/* + * API versions for Identity: + * [1,0] -- initial ver + */ +Identity.prototype._sshpkApiVersion = [1, 0]; + +Identity._oldVersionDetect = function (obj) { + return ([1, 0]); +}; + + +/***/ }), + +/***/ 380: +/***/ (function(module) { + +module.exports = {"$id":"request.json#","$schema":"http://json-schema.org/draft-06/schema#","type":"object","required":["method","url","httpVersion","cookies","headers","queryString","headersSize","bodySize"],"properties":{"method":{"type":"string"},"url":{"type":"string","format":"uri"},"httpVersion":{"type":"string"},"cookies":{"type":"array","items":{"$ref":"cookie.json#"}},"headers":{"type":"array","items":{"$ref":"header.json#"}},"queryString":{"type":"array","items":{"$ref":"query.json#"}},"postData":{"$ref":"postData.json#"},"headersSize":{"type":"integer"},"bodySize":{"type":"integer"},"comment":{"type":"string"}}}; + +/***/ }), + +/***/ 382: +/***/ (function(module, __unusedexports, __webpack_require__) { + +var stream = __webpack_require__(413) + + +function isStream (obj) { + return obj instanceof stream.Stream +} + + +function isReadable (obj) { + return isStream(obj) && typeof obj._read == 'function' && typeof obj._readableState == 'object' +} + + +function isWritable (obj) { + return isStream(obj) && typeof obj._write == 'function' && typeof obj._writableState == 'object' +} + + +function isDuplex (obj) { + return isReadable(obj) && isWritable(obj) +} + + +module.exports = isStream +module.exports.isReadable = isReadable +module.exports.isWritable = isWritable +module.exports.isDuplex = isDuplex + + +/***/ }), + +/***/ 383: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; +/*! + * Copyright (c) 2015, Salesforce.com, Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of Salesforce.com nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +var pubsuffix = __webpack_require__(519); + +// Gives the permutation of all possible domainMatch()es of a given domain. The +// array is in shortest-to-longest order. Handy for indexing. +function permuteDomain (domain) { + var pubSuf = pubsuffix.getPublicSuffix(domain); + if (!pubSuf) { + return null; + } + if (pubSuf == domain) { + return [domain]; + } + + var prefix = domain.slice(0, -(pubSuf.length + 1)); // ".example.com" + var parts = prefix.split('.').reverse(); + var cur = pubSuf; + var permutations = [cur]; + while (parts.length) { + cur = parts.shift() + '.' + cur; + permutations.push(cur); + } + return permutations; +} + +exports.permuteDomain = permuteDomain; + + +/***/ }), + +/***/ 386: +/***/ (function(module, __unusedexports, __webpack_require__) { + +"use strict"; + + +var stringify = __webpack_require__(897); +var parse = __webpack_require__(755); +var formats = __webpack_require__(13); + +module.exports = { + formats: formats, + parse: parse, + stringify: stringify +}; + + +/***/ }), + +/***/ 397: +/***/ (function(module) { + +"use strict"; + +module.exports = function generate_multipleOf(it, $keyword, $ruleType) { + var out = ' '; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $data = 'data' + ($dataLvl || ''); + var $isData = it.opts.$data && $schema && $schema.$data, + $schemaValue; + if ($isData) { + out += ' var schema' + ($lvl) + ' = ' + (it.util.getData($schema.$data, $dataLvl, it.dataPathArr)) + '; '; + $schemaValue = 'schema' + $lvl; + } else { + $schemaValue = $schema; + } + out += 'var division' + ($lvl) + ';if ('; + if ($isData) { + out += ' ' + ($schemaValue) + ' !== undefined && ( typeof ' + ($schemaValue) + ' != \'number\' || '; + } + out += ' (division' + ($lvl) + ' = ' + ($data) + ' / ' + ($schemaValue) + ', '; + if (it.opts.multipleOfPrecision) { + out += ' Math.abs(Math.round(division' + ($lvl) + ') - division' + ($lvl) + ') > 1e-' + (it.opts.multipleOfPrecision) + ' '; + } else { + out += ' division' + ($lvl) + ' !== parseInt(division' + ($lvl) + ') '; + } + out += ' ) '; + if ($isData) { + out += ' ) '; + } + out += ' ) { '; + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('multipleOf') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { multipleOf: ' + ($schemaValue) + ' } '; + if (it.opts.messages !== false) { + out += ' , message: \'should be multiple of '; + if ($isData) { + out += '\' + ' + ($schemaValue); + } else { + out += '' + ($schemaValue) + '\''; + } + } + if (it.opts.verbose) { + out += ' , schema: '; + if ($isData) { + out += 'validate.schema' + ($schemaPath); + } else { + out += '' + ($schema); + } + out += ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + out += '} '; + if ($breakOnError) { + out += ' else { '; + } + return out; +} + + +/***/ }), + +/***/ 400: +/***/ (function(module, __unusedexports, __webpack_require__) { + +// Copyright 2018 Joyent, Inc. + +module.exports = Fingerprint; + +var assert = __webpack_require__(477); +var Buffer = __webpack_require__(215).Buffer; +var algs = __webpack_require__(98); +var crypto = __webpack_require__(417); +var errs = __webpack_require__(753); +var Key = __webpack_require__(852); +var PrivateKey = __webpack_require__(502); +var Certificate = __webpack_require__(752); +var utils = __webpack_require__(270); + +var FingerprintFormatError = errs.FingerprintFormatError; +var InvalidAlgorithmError = errs.InvalidAlgorithmError; + +function Fingerprint(opts) { + assert.object(opts, 'options'); + assert.string(opts.type, 'options.type'); + assert.buffer(opts.hash, 'options.hash'); + assert.string(opts.algorithm, 'options.algorithm'); + + this.algorithm = opts.algorithm.toLowerCase(); + if (algs.hashAlgs[this.algorithm] !== true) + throw (new InvalidAlgorithmError(this.algorithm)); + + this.hash = opts.hash; + this.type = opts.type; + this.hashType = opts.hashType; +} + +Fingerprint.prototype.toString = function (format) { + if (format === undefined) { + if (this.algorithm === 'md5' || this.hashType === 'spki') + format = 'hex'; + else + format = 'base64'; + } + assert.string(format); + + switch (format) { + case 'hex': + if (this.hashType === 'spki') + return (this.hash.toString('hex')); + return (addColons(this.hash.toString('hex'))); + case 'base64': + if (this.hashType === 'spki') + return (this.hash.toString('base64')); + return (sshBase64Format(this.algorithm, + this.hash.toString('base64'))); + default: + throw (new FingerprintFormatError(undefined, format)); + } +}; + +Fingerprint.prototype.matches = function (other) { + assert.object(other, 'key or certificate'); + if (this.type === 'key' && this.hashType !== 'ssh') { + utils.assertCompatible(other, Key, [1, 7], 'key with spki'); + if (PrivateKey.isPrivateKey(other)) { + utils.assertCompatible(other, PrivateKey, [1, 6], + 'privatekey with spki support'); + } + } else if (this.type === 'key') { + utils.assertCompatible(other, Key, [1, 0], 'key'); + } else { + utils.assertCompatible(other, Certificate, [1, 0], + 'certificate'); + } + + var theirHash = other.hash(this.algorithm, this.hashType); + var theirHash2 = crypto.createHash(this.algorithm). + update(theirHash).digest('base64'); + + if (this.hash2 === undefined) + this.hash2 = crypto.createHash(this.algorithm). + update(this.hash).digest('base64'); + + return (this.hash2 === theirHash2); +}; + +/*JSSTYLED*/ +var base64RE = /^[A-Za-z0-9+\/=]+$/; +/*JSSTYLED*/ +var hexRE = /^[a-fA-F0-9]+$/; + +Fingerprint.parse = function (fp, options) { + assert.string(fp, 'fingerprint'); + + var alg, hash, enAlgs; + if (Array.isArray(options)) { + enAlgs = options; + options = {}; + } + assert.optionalObject(options, 'options'); + if (options === undefined) + options = {}; + if (options.enAlgs !== undefined) + enAlgs = options.enAlgs; + if (options.algorithms !== undefined) + enAlgs = options.algorithms; + assert.optionalArrayOfString(enAlgs, 'algorithms'); + + var hashType = 'ssh'; + if (options.hashType !== undefined) + hashType = options.hashType; + assert.string(hashType, 'options.hashType'); + + var parts = fp.split(':'); + if (parts.length == 2) { + alg = parts[0].toLowerCase(); + if (!base64RE.test(parts[1])) + throw (new FingerprintFormatError(fp)); + try { + hash = Buffer.from(parts[1], 'base64'); + } catch (e) { + throw (new FingerprintFormatError(fp)); + } + } else if (parts.length > 2) { + alg = 'md5'; + if (parts[0].toLowerCase() === 'md5') + parts = parts.slice(1); + parts = parts.map(function (p) { + while (p.length < 2) + p = '0' + p; + if (p.length > 2) + throw (new FingerprintFormatError(fp)); + return (p); + }); + parts = parts.join(''); + if (!hexRE.test(parts) || parts.length % 2 !== 0) + throw (new FingerprintFormatError(fp)); + try { + hash = Buffer.from(parts, 'hex'); + } catch (e) { + throw (new FingerprintFormatError(fp)); + } + } else { + if (hexRE.test(fp)) { + hash = Buffer.from(fp, 'hex'); + } else if (base64RE.test(fp)) { + hash = Buffer.from(fp, 'base64'); + } else { + throw (new FingerprintFormatError(fp)); + } + + switch (hash.length) { + case 32: + alg = 'sha256'; + break; + case 16: + alg = 'md5'; + break; + case 20: + alg = 'sha1'; + break; + case 64: + alg = 'sha512'; + break; + default: + throw (new FingerprintFormatError(fp)); + } + + /* Plain hex/base64: guess it's probably SPKI unless told. */ + if (options.hashType === undefined) + hashType = 'spki'; + } + + if (alg === undefined) + throw (new FingerprintFormatError(fp)); + + if (algs.hashAlgs[alg] === undefined) + throw (new InvalidAlgorithmError(alg)); + + if (enAlgs !== undefined) { + enAlgs = enAlgs.map(function (a) { return a.toLowerCase(); }); + if (enAlgs.indexOf(alg) === -1) + throw (new InvalidAlgorithmError(alg)); + } + + return (new Fingerprint({ + algorithm: alg, + hash: hash, + type: options.type || 'key', + hashType: hashType + })); +}; + +function addColons(s) { + /*JSSTYLED*/ + return (s.replace(/(.{2})(?=.)/g, '$1:')); +} + +function base64Strip(s) { + /*JSSTYLED*/ + return (s.replace(/=*$/, '')); +} + +function sshBase64Format(alg, h) { + return (alg.toUpperCase() + ':' + base64Strip(h)); +} + +Fingerprint.isFingerprint = function (obj, ver) { + return (utils.isCompatible(obj, Fingerprint, ver)); +}; + +/* + * API versions for Fingerprint: + * [1,0] -- initial ver + * [1,1] -- first tagged ver + * [1,2] -- hashType and spki support + */ +Fingerprint.prototype._sshpkApiVersion = [1, 2]; + +Fingerprint._oldVersionDetect = function (obj) { + assert.func(obj.toString); + assert.func(obj.matches); + return ([1, 0]); +}; + + +/***/ }), + +/***/ 413: +/***/ (function(module) { + +module.exports = require("stream"); + +/***/ }), + +/***/ 416: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; + + +var fs = __webpack_require__(747) +var qs = __webpack_require__(191) +var validate = __webpack_require__(846) +var extend = __webpack_require__(374) + +function Har (request) { + this.request = request +} + +Har.prototype.reducer = function (obj, pair) { + // new property ? + if (obj[pair.name] === undefined) { + obj[pair.name] = pair.value + return obj + } + + // existing? convert to array + var arr = [ + obj[pair.name], + pair.value + ] + + obj[pair.name] = arr + + return obj +} + +Har.prototype.prep = function (data) { + // construct utility properties + data.queryObj = {} + data.headersObj = {} + data.postData.jsonObj = false + data.postData.paramsObj = false + + // construct query objects + if (data.queryString && data.queryString.length) { + data.queryObj = data.queryString.reduce(this.reducer, {}) + } + + // construct headers objects + if (data.headers && data.headers.length) { + // loweCase header keys + data.headersObj = data.headers.reduceRight(function (headers, header) { + headers[header.name] = header.value + return headers + }, {}) + } + + // construct Cookie header + if (data.cookies && data.cookies.length) { + var cookies = data.cookies.map(function (cookie) { + return cookie.name + '=' + cookie.value + }) + + if (cookies.length) { + data.headersObj.cookie = cookies.join('; ') + } + } + + // prep body + function some (arr) { + return arr.some(function (type) { + return data.postData.mimeType.indexOf(type) === 0 + }) + } + + if (some([ + 'multipart/mixed', + 'multipart/related', + 'multipart/form-data', + 'multipart/alternative'])) { + // reset values + data.postData.mimeType = 'multipart/form-data' + } else if (some([ + 'application/x-www-form-urlencoded'])) { + if (!data.postData.params) { + data.postData.text = '' + } else { + data.postData.paramsObj = data.postData.params.reduce(this.reducer, {}) + + // always overwrite + data.postData.text = qs.stringify(data.postData.paramsObj) + } + } else if (some([ + 'text/json', + 'text/x-json', + 'application/json', + 'application/x-json'])) { + data.postData.mimeType = 'application/json' + + if (data.postData.text) { + try { + data.postData.jsonObj = JSON.parse(data.postData.text) + } catch (e) { + this.request.debug(e) + + // force back to text/plain + data.postData.mimeType = 'text/plain' + } + } + } + + return data +} + +Har.prototype.options = function (options) { + // skip if no har property defined + if (!options.har) { + return options + } + + var har = {} + extend(har, options.har) + + // only process the first entry + if (har.log && har.log.entries) { + har = har.log.entries[0] + } + + // add optional properties to make validation successful + har.url = har.url || options.url || options.uri || options.baseUrl || '/' + har.httpVersion = har.httpVersion || 'HTTP/1.1' + har.queryString = har.queryString || [] + har.headers = har.headers || [] + har.cookies = har.cookies || [] + har.postData = har.postData || {} + har.postData.mimeType = har.postData.mimeType || 'application/octet-stream' + + har.bodySize = 0 + har.headersSize = 0 + har.postData.size = 0 + + if (!validate.request(har)) { + return options + } + + // clean up and get some utility properties + var req = this.prep(har) + + // construct new options + if (req.url) { + options.url = req.url + } + + if (req.method) { + options.method = req.method + } + + if (Object.keys(req.queryObj).length) { + options.qs = req.queryObj + } + + if (Object.keys(req.headersObj).length) { + options.headers = req.headersObj + } + + function test (type) { + return req.postData.mimeType.indexOf(type) === 0 + } + if (test('application/x-www-form-urlencoded')) { + options.form = req.postData.paramsObj + } else if (test('application/json')) { + if (req.postData.jsonObj) { + options.body = req.postData.jsonObj + options.json = true + } + } else if (test('multipart/form-data')) { + options.formData = {} + + req.postData.params.forEach(function (param) { + var attachment = {} + + if (!param.fileName && !param.contentType) { + options.formData[param.name] = param.value + return + } + + // attempt to read from disk! + if (param.fileName && !param.value) { + attachment.value = fs.createReadStream(param.fileName) + } else if (param.value) { + attachment.value = param.value + } + + if (param.fileName) { + attachment.options = { + filename: param.fileName, + contentType: param.contentType ? param.contentType : null + } + } + + options.formData[param.name] = attachment + }) + } else { + if (req.postData.text) { + options.body = req.postData.text + } + } + + return options +} + +exports.Har = Har + + +/***/ }), + +/***/ 417: +/***/ (function(module) { + +module.exports = require("crypto"); + +/***/ }), + +/***/ 424: +/***/ (function(module, __unusedexports, __webpack_require__) { + +var iterate = __webpack_require__(157) + , initState = __webpack_require__(147) + , terminator = __webpack_require__(939) + ; + +// 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); +} + + +/***/ }), + +/***/ 428: +/***/ (function(module, __unusedexports, __webpack_require__) { + +// Copyright 2015 Joyent, Inc. + +var assert = __webpack_require__(477); +var crypto = __webpack_require__(417); +var sshpk = __webpack_require__(650); +var utils = __webpack_require__(909); + +var HASH_ALGOS = utils.HASH_ALGOS; +var PK_ALGOS = utils.PK_ALGOS; +var InvalidAlgorithmError = utils.InvalidAlgorithmError; +var HttpSignatureError = utils.HttpSignatureError; +var validateAlgorithm = utils.validateAlgorithm; + +///--- Exported API + +module.exports = { + /** + * Verify RSA/DSA signature against public key. You are expected to pass in + * an object that was returned from `parse()`. + * + * @param {Object} parsedSignature the object you got from `parse`. + * @param {String} pubkey RSA/DSA private key PEM. + * @return {Boolean} true if valid, false otherwise. + * @throws {TypeError} if you pass in bad arguments. + * @throws {InvalidAlgorithmError} + */ + verifySignature: function verifySignature(parsedSignature, pubkey) { + assert.object(parsedSignature, 'parsedSignature'); + if (typeof (pubkey) === 'string' || Buffer.isBuffer(pubkey)) + pubkey = sshpk.parseKey(pubkey); + assert.ok(sshpk.Key.isKey(pubkey, [1, 1]), 'pubkey must be a sshpk.Key'); + + var alg = validateAlgorithm(parsedSignature.algorithm); + if (alg[0] === 'hmac' || alg[0] !== pubkey.type) + return (false); + + var v = pubkey.createVerify(alg[1]); + v.update(parsedSignature.signingString); + return (v.verify(parsedSignature.params.signature, 'base64')); + }, + + /** + * Verify HMAC against shared secret. You are expected to pass in an object + * that was returned from `parse()`. + * + * @param {Object} parsedSignature the object you got from `parse`. + * @param {String} secret HMAC shared secret. + * @return {Boolean} true if valid, false otherwise. + * @throws {TypeError} if you pass in bad arguments. + * @throws {InvalidAlgorithmError} + */ + verifyHMAC: function verifyHMAC(parsedSignature, secret) { + assert.object(parsedSignature, 'parsedHMAC'); + assert.string(secret, 'secret'); + + var alg = validateAlgorithm(parsedSignature.algorithm); + if (alg[0] !== 'hmac') + return (false); + + var hashAlg = alg[1].toUpperCase(); + + var hmac = crypto.createHmac(hashAlg, secret); + hmac.update(parsedSignature.signingString); + + /* + * Now double-hash to avoid leaking timing information - there's + * no easy constant-time compare in JS, so we use this approach + * instead. See for more info: + * https://www.isecpartners.com/blog/2011/february/double-hmac- + * verification.aspx + */ + var h1 = crypto.createHmac(hashAlg, secret); + h1.update(hmac.digest()); + h1 = h1.digest(); + var h2 = crypto.createHmac(hashAlg, secret); + h2.update(new Buffer(parsedSignature.params.signature, 'base64')); + h2 = h2.digest(); + + /* Node 0.8 returns strings from .digest(). */ + if (typeof (h1) === 'string') + return (h1 === h2); + /* And node 0.10 lacks the .equals() method on Buffers. */ + if (Buffer.isBuffer(h1) && !h1.equals) + return (h1.toString('binary') === h2.toString('binary')); + + return (h1.equals(h2)); + } +}; + + +/***/ }), + +/***/ 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)); +/** + * 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 (s || '') + .replace(/%/g, '%25') + .replace(/\r/g, '%0D') + .replace(/\n/g, '%0A'); +} +function escapeProperty(s) { + return (s || '') + .replace(/%/g, '%25') + .replace(/\r/g, '%0D') + .replace(/\n/g, '%0A') + .replace(/:/g, '%3A') + .replace(/,/g, '%2C'); +} +//# sourceMappingURL=command.js.map + +/***/ }), + +/***/ 449: +/***/ (function(module, __unusedexports, __webpack_require__) { + +// Copyright 2015 Joyent, Inc. + +module.exports = { + read: read, + readPkcs1: readPkcs1, + write: write, + writePkcs1: writePkcs1 +}; + +var assert = __webpack_require__(477); +var asn1 = __webpack_require__(62); +var Buffer = __webpack_require__(215).Buffer; +var algs = __webpack_require__(98); +var utils = __webpack_require__(270); + +var Key = __webpack_require__(852); +var PrivateKey = __webpack_require__(502); +var pem = __webpack_require__(268); + +var pkcs8 = __webpack_require__(707); +var readECDSACurve = pkcs8.readECDSACurve; + +function read(buf, options) { + return (pem.read(buf, options, 'pkcs1')); +} + +function write(key, options) { + return (pem.write(key, options, 'pkcs1')); +} + +/* Helper to read in a single mpint */ +function readMPInt(der, nm) { + assert.strictEqual(der.peek(), asn1.Ber.Integer, + nm + ' is not an Integer'); + return (utils.mpNormalize(der.readString(asn1.Ber.Integer, true))); +} + +function readPkcs1(alg, type, der) { + switch (alg) { + case 'RSA': + if (type === 'public') + return (readPkcs1RSAPublic(der)); + else if (type === 'private') + return (readPkcs1RSAPrivate(der)); + throw (new Error('Unknown key type: ' + type)); + case 'DSA': + if (type === 'public') + return (readPkcs1DSAPublic(der)); + else if (type === 'private') + return (readPkcs1DSAPrivate(der)); + throw (new Error('Unknown key type: ' + type)); + case 'EC': + case 'ECDSA': + if (type === 'private') + return (readPkcs1ECDSAPrivate(der)); + else if (type === 'public') + return (readPkcs1ECDSAPublic(der)); + throw (new Error('Unknown key type: ' + type)); + case 'EDDSA': + case 'EdDSA': + if (type === 'private') + return (readPkcs1EdDSAPrivate(der)); + throw (new Error(type + ' keys not supported with EdDSA')); + default: + throw (new Error('Unknown key algo: ' + alg)); + } +} + +function readPkcs1RSAPublic(der) { + // modulus and exponent + var n = readMPInt(der, 'modulus'); + var e = readMPInt(der, 'exponent'); + + // now, make the key + var key = { + type: 'rsa', + parts: [ + { name: 'e', data: e }, + { name: 'n', data: n } + ] + }; + + return (new Key(key)); +} + +function readPkcs1RSAPrivate(der) { + var version = readMPInt(der, 'version'); + assert.strictEqual(version[0], 0); + + // modulus then public exponent + var n = readMPInt(der, 'modulus'); + var e = readMPInt(der, 'public exponent'); + var d = readMPInt(der, 'private exponent'); + var p = readMPInt(der, 'prime1'); + var q = readMPInt(der, 'prime2'); + var dmodp = readMPInt(der, 'exponent1'); + var dmodq = readMPInt(der, 'exponent2'); + var iqmp = readMPInt(der, 'iqmp'); + + // now, make the key + var key = { + type: 'rsa', + parts: [ + { name: 'n', data: n }, + { name: 'e', data: e }, + { name: 'd', data: d }, + { name: 'iqmp', data: iqmp }, + { name: 'p', data: p }, + { name: 'q', data: q }, + { name: 'dmodp', data: dmodp }, + { name: 'dmodq', data: dmodq } + ] + }; + + return (new PrivateKey(key)); +} + +function readPkcs1DSAPrivate(der) { + var version = readMPInt(der, 'version'); + assert.strictEqual(version.readUInt8(0), 0); + + var p = readMPInt(der, 'p'); + var q = readMPInt(der, 'q'); + var g = readMPInt(der, 'g'); + var y = readMPInt(der, 'y'); + var x = readMPInt(der, 'x'); + + // now, make the key + var key = { + type: 'dsa', + parts: [ + { name: 'p', data: p }, + { name: 'q', data: q }, + { name: 'g', data: g }, + { name: 'y', data: y }, + { name: 'x', data: x } + ] + }; + + return (new PrivateKey(key)); +} + +function readPkcs1EdDSAPrivate(der) { + var version = readMPInt(der, 'version'); + assert.strictEqual(version.readUInt8(0), 1); + + // private key + var k = der.readString(asn1.Ber.OctetString, true); + + der.readSequence(0xa0); + var oid = der.readOID(); + assert.strictEqual(oid, '1.3.101.112', 'the ed25519 curve identifier'); + + der.readSequence(0xa1); + var A = utils.readBitString(der); + + var key = { + type: 'ed25519', + parts: [ + { name: 'A', data: utils.zeroPadToLength(A, 32) }, + { name: 'k', data: k } + ] + }; + + return (new PrivateKey(key)); +} + +function readPkcs1DSAPublic(der) { + var y = readMPInt(der, 'y'); + var p = readMPInt(der, 'p'); + var q = readMPInt(der, 'q'); + var g = readMPInt(der, 'g'); + + var key = { + type: 'dsa', + parts: [ + { name: 'y', data: y }, + { name: 'p', data: p }, + { name: 'q', data: q }, + { name: 'g', data: g } + ] + }; + + return (new Key(key)); +} + +function readPkcs1ECDSAPublic(der) { + der.readSequence(); + + var oid = der.readOID(); + assert.strictEqual(oid, '1.2.840.10045.2.1', 'must be ecPublicKey'); + + var curveOid = der.readOID(); + + var curve; + var curves = Object.keys(algs.curves); + for (var j = 0; j < curves.length; ++j) { + var c = curves[j]; + var cd = algs.curves[c]; + if (cd.pkcs8oid === curveOid) { + curve = c; + break; + } + } + assert.string(curve, 'a known ECDSA named curve'); + + var Q = der.readString(asn1.Ber.BitString, true); + Q = utils.ecNormalize(Q); + + var key = { + type: 'ecdsa', + parts: [ + { name: 'curve', data: Buffer.from(curve) }, + { name: 'Q', data: Q } + ] + }; + + return (new Key(key)); +} + +function readPkcs1ECDSAPrivate(der) { + var version = readMPInt(der, 'version'); + assert.strictEqual(version.readUInt8(0), 1); + + // private key + var d = der.readString(asn1.Ber.OctetString, true); + + der.readSequence(0xa0); + var curve = readECDSACurve(der); + assert.string(curve, 'a known elliptic curve'); + + der.readSequence(0xa1); + var Q = der.readString(asn1.Ber.BitString, true); + Q = utils.ecNormalize(Q); + + var key = { + type: 'ecdsa', + parts: [ + { name: 'curve', data: Buffer.from(curve) }, + { name: 'Q', data: Q }, + { name: 'd', data: d } + ] + }; + + return (new PrivateKey(key)); +} + +function writePkcs1(der, key) { + der.startSequence(); + + switch (key.type) { + case 'rsa': + if (PrivateKey.isPrivateKey(key)) + writePkcs1RSAPrivate(der, key); + else + writePkcs1RSAPublic(der, key); + break; + case 'dsa': + if (PrivateKey.isPrivateKey(key)) + writePkcs1DSAPrivate(der, key); + else + writePkcs1DSAPublic(der, key); + break; + case 'ecdsa': + if (PrivateKey.isPrivateKey(key)) + writePkcs1ECDSAPrivate(der, key); + else + writePkcs1ECDSAPublic(der, key); + break; + case 'ed25519': + if (PrivateKey.isPrivateKey(key)) + writePkcs1EdDSAPrivate(der, key); + else + writePkcs1EdDSAPublic(der, key); + break; + default: + throw (new Error('Unknown key algo: ' + key.type)); + } + + der.endSequence(); +} + +function writePkcs1RSAPublic(der, key) { + der.writeBuffer(key.part.n.data, asn1.Ber.Integer); + der.writeBuffer(key.part.e.data, asn1.Ber.Integer); +} + +function writePkcs1RSAPrivate(der, key) { + var ver = Buffer.from([0]); + der.writeBuffer(ver, asn1.Ber.Integer); + + der.writeBuffer(key.part.n.data, asn1.Ber.Integer); + der.writeBuffer(key.part.e.data, asn1.Ber.Integer); + der.writeBuffer(key.part.d.data, asn1.Ber.Integer); + der.writeBuffer(key.part.p.data, asn1.Ber.Integer); + der.writeBuffer(key.part.q.data, asn1.Ber.Integer); + if (!key.part.dmodp || !key.part.dmodq) + utils.addRSAMissing(key); + der.writeBuffer(key.part.dmodp.data, asn1.Ber.Integer); + der.writeBuffer(key.part.dmodq.data, asn1.Ber.Integer); + der.writeBuffer(key.part.iqmp.data, asn1.Ber.Integer); +} + +function writePkcs1DSAPrivate(der, key) { + var ver = Buffer.from([0]); + der.writeBuffer(ver, asn1.Ber.Integer); + + der.writeBuffer(key.part.p.data, asn1.Ber.Integer); + der.writeBuffer(key.part.q.data, asn1.Ber.Integer); + der.writeBuffer(key.part.g.data, asn1.Ber.Integer); + der.writeBuffer(key.part.y.data, asn1.Ber.Integer); + der.writeBuffer(key.part.x.data, asn1.Ber.Integer); +} + +function writePkcs1DSAPublic(der, key) { + der.writeBuffer(key.part.y.data, asn1.Ber.Integer); + der.writeBuffer(key.part.p.data, asn1.Ber.Integer); + der.writeBuffer(key.part.q.data, asn1.Ber.Integer); + der.writeBuffer(key.part.g.data, asn1.Ber.Integer); +} + +function writePkcs1ECDSAPublic(der, key) { + der.startSequence(); + + der.writeOID('1.2.840.10045.2.1'); /* ecPublicKey */ + var curve = key.part.curve.data.toString(); + var curveOid = algs.curves[curve].pkcs8oid; + assert.string(curveOid, 'a known ECDSA named curve'); + der.writeOID(curveOid); + + der.endSequence(); + + var Q = utils.ecNormalize(key.part.Q.data, true); + der.writeBuffer(Q, asn1.Ber.BitString); +} + +function writePkcs1ECDSAPrivate(der, key) { + var ver = Buffer.from([1]); + der.writeBuffer(ver, asn1.Ber.Integer); + + der.writeBuffer(key.part.d.data, asn1.Ber.OctetString); + + der.startSequence(0xa0); + var curve = key.part.curve.data.toString(); + var curveOid = algs.curves[curve].pkcs8oid; + assert.string(curveOid, 'a known ECDSA named curve'); + der.writeOID(curveOid); + der.endSequence(); + + der.startSequence(0xa1); + var Q = utils.ecNormalize(key.part.Q.data, true); + der.writeBuffer(Q, asn1.Ber.BitString); + der.endSequence(); +} + +function writePkcs1EdDSAPrivate(der, key) { + var ver = Buffer.from([1]); + der.writeBuffer(ver, asn1.Ber.Integer); + + der.writeBuffer(key.part.k.data, asn1.Ber.OctetString); + + der.startSequence(0xa0); + der.writeOID('1.3.101.112'); + der.endSequence(); + + der.startSequence(0xa1); + utils.writeBitString(der, key.part.A.data); + der.endSequence(); +} + +function writePkcs1EdDSAPublic(der, key) { + throw (new Error('Public keys are not supported for EdDSA PKCS#1')); +} + + +/***/ }), + +/***/ 455: +/***/ (function(module, __unusedexports, __webpack_require__) { + +"use strict"; + + +var http = __webpack_require__(605) +var https = __webpack_require__(211) +var url = __webpack_require__(835) +var util = __webpack_require__(669) +var stream = __webpack_require__(413) +var zlib = __webpack_require__(761) +var aws2 = __webpack_require__(942) +var aws4 = __webpack_require__(658) +var httpSignature = __webpack_require__(789) +var mime = __webpack_require__(779) +var caseless = __webpack_require__(254) +var ForeverAgent = __webpack_require__(792) +var FormData = __webpack_require__(928) +var extend = __webpack_require__(374) +var isstream = __webpack_require__(382) +var isTypedArray = __webpack_require__(944).strict +var helpers = __webpack_require__(810) +var cookies = __webpack_require__(602) +var getProxyFromURI = __webpack_require__(721) +var Querystring = __webpack_require__(629).Querystring +var Har = __webpack_require__(416).Har +var Auth = __webpack_require__(554).Auth +var OAuth = __webpack_require__(287).OAuth +var hawk = __webpack_require__(964) +var Multipart = __webpack_require__(469).Multipart +var Redirect = __webpack_require__(552).Redirect +var Tunnel = __webpack_require__(461).Tunnel +var now = __webpack_require__(742) +var Buffer = __webpack_require__(149).Buffer + +var safeStringify = helpers.safeStringify +var isReadStream = helpers.isReadStream +var toBase64 = helpers.toBase64 +var defer = helpers.defer +var copy = helpers.copy +var version = helpers.version +var globalCookieJar = cookies.jar() + +var globalPool = {} + +function filterForNonReserved (reserved, options) { + // Filter out properties that are not reserved. + // Reserved values are passed in at call site. + + var object = {} + for (var i in options) { + var notReserved = (reserved.indexOf(i) === -1) + if (notReserved) { + object[i] = options[i] + } + } + return object +} + +function filterOutReservedFunctions (reserved, options) { + // Filter out properties that are functions and are reserved. + // Reserved values are passed in at call site. + + var object = {} + for (var i in options) { + var isReserved = !(reserved.indexOf(i) === -1) + var isFunction = (typeof options[i] === 'function') + if (!(isReserved && isFunction)) { + object[i] = options[i] + } + } + return object +} + +// Return a simpler request object to allow serialization +function requestToJSON () { + var self = this + return { + uri: self.uri, + method: self.method, + headers: self.headers + } +} + +// Return a simpler response object to allow serialization +function responseToJSON () { + var self = this + return { + statusCode: self.statusCode, + body: self.body, + headers: self.headers, + request: requestToJSON.call(self.request) + } +} + +function Request (options) { + // if given the method property in options, set property explicitMethod to true + + // extend the Request instance with any non-reserved properties + // remove any reserved functions from the options object + // set Request instance to be readable and writable + // call init + + var self = this + + // start with HAR, then override with additional options + if (options.har) { + self._har = new Har(self) + options = self._har.options(options) + } + + stream.Stream.call(self) + var reserved = Object.keys(Request.prototype) + var nonReserved = filterForNonReserved(reserved, options) + + extend(self, nonReserved) + options = filterOutReservedFunctions(reserved, options) + + self.readable = true + self.writable = true + if (options.method) { + self.explicitMethod = true + } + self._qs = new Querystring(self) + self._auth = new Auth(self) + self._oauth = new OAuth(self) + self._multipart = new Multipart(self) + self._redirect = new Redirect(self) + self._tunnel = new Tunnel(self) + self.init(options) +} + +util.inherits(Request, stream.Stream) + +// Debugging +Request.debug = process.env.NODE_DEBUG && /\brequest\b/.test(process.env.NODE_DEBUG) +function debug () { + if (Request.debug) { + console.error('REQUEST %s', util.format.apply(util, arguments)) + } +} +Request.prototype.debug = debug + +Request.prototype.init = function (options) { + // init() contains all the code to setup the request object. + // the actual outgoing request is not started until start() is called + // this function is called from both the constructor and on redirect. + var self = this + if (!options) { + options = {} + } + self.headers = self.headers ? copy(self.headers) : {} + + // Delete headers with value undefined since they break + // ClientRequest.OutgoingMessage.setHeader in node 0.12 + for (var headerName in self.headers) { + if (typeof self.headers[headerName] === 'undefined') { + delete self.headers[headerName] + } + } + + caseless.httpify(self, self.headers) + + if (!self.method) { + self.method = options.method || 'GET' + } + if (!self.localAddress) { + self.localAddress = options.localAddress + } + + self._qs.init(options) + + debug(options) + if (!self.pool && self.pool !== false) { + self.pool = globalPool + } + self.dests = self.dests || [] + self.__isRequestRequest = true + + // Protect against double callback + if (!self._callback && self.callback) { + self._callback = self.callback + self.callback = function () { + if (self._callbackCalled) { + return // Print a warning maybe? + } + self._callbackCalled = true + self._callback.apply(self, arguments) + } + self.on('error', self.callback.bind()) + self.on('complete', self.callback.bind(self, null)) + } + + // People use this property instead all the time, so support it + if (!self.uri && self.url) { + self.uri = self.url + delete self.url + } + + // If there's a baseUrl, then use it as the base URL (i.e. uri must be + // specified as a relative path and is appended to baseUrl). + if (self.baseUrl) { + if (typeof self.baseUrl !== 'string') { + return self.emit('error', new Error('options.baseUrl must be a string')) + } + + if (typeof self.uri !== 'string') { + return self.emit('error', new Error('options.uri must be a string when using options.baseUrl')) + } + + if (self.uri.indexOf('//') === 0 || self.uri.indexOf('://') !== -1) { + return self.emit('error', new Error('options.uri must be a path when using options.baseUrl')) + } + + // Handle all cases to make sure that there's only one slash between + // baseUrl and uri. + var baseUrlEndsWithSlash = self.baseUrl.lastIndexOf('/') === self.baseUrl.length - 1 + var uriStartsWithSlash = self.uri.indexOf('/') === 0 + + if (baseUrlEndsWithSlash && uriStartsWithSlash) { + self.uri = self.baseUrl + self.uri.slice(1) + } else if (baseUrlEndsWithSlash || uriStartsWithSlash) { + self.uri = self.baseUrl + self.uri + } else if (self.uri === '') { + self.uri = self.baseUrl + } else { + self.uri = self.baseUrl + '/' + self.uri + } + delete self.baseUrl + } + + // A URI is needed by this point, emit error if we haven't been able to get one + if (!self.uri) { + return self.emit('error', new Error('options.uri is a required argument')) + } + + // If a string URI/URL was given, parse it into a URL object + if (typeof self.uri === 'string') { + self.uri = url.parse(self.uri) + } + + // Some URL objects are not from a URL parsed string and need href added + if (!self.uri.href) { + self.uri.href = url.format(self.uri) + } + + // DEPRECATED: Warning for users of the old Unix Sockets URL Scheme + if (self.uri.protocol === 'unix:') { + return self.emit('error', new Error('`unix://` URL scheme is no longer supported. Please use the format `http://unix:SOCKET:PATH`')) + } + + // Support Unix Sockets + if (self.uri.host === 'unix') { + self.enableUnixSocket() + } + + if (self.strictSSL === false) { + self.rejectUnauthorized = false + } + + if (!self.uri.pathname) { self.uri.pathname = '/' } + + if (!(self.uri.host || (self.uri.hostname && self.uri.port)) && !self.uri.isUnix) { + // Invalid URI: it may generate lot of bad errors, like 'TypeError: Cannot call method `indexOf` of undefined' in CookieJar + // Detect and reject it as soon as possible + var faultyUri = url.format(self.uri) + var message = 'Invalid URI "' + faultyUri + '"' + if (Object.keys(options).length === 0) { + // No option ? This can be the sign of a redirect + // As this is a case where the user cannot do anything (they didn't call request directly with this URL) + // they should be warned that it can be caused by a redirection (can save some hair) + message += '. This can be caused by a crappy redirection.' + } + // This error was fatal + self.abort() + return self.emit('error', new Error(message)) + } + + if (!self.hasOwnProperty('proxy')) { + self.proxy = getProxyFromURI(self.uri) + } + + self.tunnel = self._tunnel.isEnabled() + if (self.proxy) { + self._tunnel.setup(options) + } + + self._redirect.onRequest(options) + + self.setHost = false + if (!self.hasHeader('host')) { + var hostHeaderName = self.originalHostHeaderName || 'host' + self.setHeader(hostHeaderName, self.uri.host) + // Drop :port suffix from Host header if known protocol. + if (self.uri.port) { + if ((self.uri.port === '80' && self.uri.protocol === 'http:') || + (self.uri.port === '443' && self.uri.protocol === 'https:')) { + self.setHeader(hostHeaderName, self.uri.hostname) + } + } + self.setHost = true + } + + self.jar(self._jar || options.jar) + + if (!self.uri.port) { + if (self.uri.protocol === 'http:') { self.uri.port = 80 } else if (self.uri.protocol === 'https:') { self.uri.port = 443 } + } + + if (self.proxy && !self.tunnel) { + self.port = self.proxy.port + self.host = self.proxy.hostname + } else { + self.port = self.uri.port + self.host = self.uri.hostname + } + + if (options.form) { + self.form(options.form) + } + + if (options.formData) { + var formData = options.formData + var requestForm = self.form() + var appendFormValue = function (key, value) { + if (value && value.hasOwnProperty('value') && value.hasOwnProperty('options')) { + requestForm.append(key, value.value, value.options) + } else { + requestForm.append(key, value) + } + } + for (var formKey in formData) { + if (formData.hasOwnProperty(formKey)) { + var formValue = formData[formKey] + if (formValue instanceof Array) { + for (var j = 0; j < formValue.length; j++) { + appendFormValue(formKey, formValue[j]) + } + } else { + appendFormValue(formKey, formValue) + } + } + } + } + + if (options.qs) { + self.qs(options.qs) + } + + if (self.uri.path) { + self.path = self.uri.path + } else { + self.path = self.uri.pathname + (self.uri.search || '') + } + + if (self.path.length === 0) { + self.path = '/' + } + + // Auth must happen last in case signing is dependent on other headers + if (options.aws) { + self.aws(options.aws) + } + + if (options.hawk) { + self.hawk(options.hawk) + } + + if (options.httpSignature) { + self.httpSignature(options.httpSignature) + } + + if (options.auth) { + if (Object.prototype.hasOwnProperty.call(options.auth, 'username')) { + options.auth.user = options.auth.username + } + if (Object.prototype.hasOwnProperty.call(options.auth, 'password')) { + options.auth.pass = options.auth.password + } + + self.auth( + options.auth.user, + options.auth.pass, + options.auth.sendImmediately, + options.auth.bearer + ) + } + + if (self.gzip && !self.hasHeader('accept-encoding')) { + self.setHeader('accept-encoding', 'gzip, deflate') + } + + if (self.uri.auth && !self.hasHeader('authorization')) { + var uriAuthPieces = self.uri.auth.split(':').map(function (item) { return self._qs.unescape(item) }) + self.auth(uriAuthPieces[0], uriAuthPieces.slice(1).join(':'), true) + } + + if (!self.tunnel && self.proxy && self.proxy.auth && !self.hasHeader('proxy-authorization')) { + var proxyAuthPieces = self.proxy.auth.split(':').map(function (item) { return self._qs.unescape(item) }) + var authHeader = 'Basic ' + toBase64(proxyAuthPieces.join(':')) + self.setHeader('proxy-authorization', authHeader) + } + + if (self.proxy && !self.tunnel) { + self.path = (self.uri.protocol + '//' + self.uri.host + self.path) + } + + if (options.json) { + self.json(options.json) + } + if (options.multipart) { + self.multipart(options.multipart) + } + + if (options.time) { + self.timing = true + + // NOTE: elapsedTime is deprecated in favor of .timings + self.elapsedTime = self.elapsedTime || 0 + } + + function setContentLength () { + if (isTypedArray(self.body)) { + self.body = Buffer.from(self.body) + } + + if (!self.hasHeader('content-length')) { + var length + if (typeof self.body === 'string') { + length = Buffer.byteLength(self.body) + } else if (Array.isArray(self.body)) { + length = self.body.reduce(function (a, b) { return a + b.length }, 0) + } else { + length = self.body.length + } + + if (length) { + self.setHeader('content-length', length) + } else { + self.emit('error', new Error('Argument error, options.body.')) + } + } + } + if (self.body && !isstream(self.body)) { + setContentLength() + } + + if (options.oauth) { + self.oauth(options.oauth) + } else if (self._oauth.params && self.hasHeader('authorization')) { + self.oauth(self._oauth.params) + } + + var protocol = self.proxy && !self.tunnel ? self.proxy.protocol : self.uri.protocol + var defaultModules = {'http:': http, 'https:': https} + var httpModules = self.httpModules || {} + + self.httpModule = httpModules[protocol] || defaultModules[protocol] + + if (!self.httpModule) { + return self.emit('error', new Error('Invalid protocol: ' + protocol)) + } + + if (options.ca) { + self.ca = options.ca + } + + if (!self.agent) { + if (options.agentOptions) { + self.agentOptions = options.agentOptions + } + + if (options.agentClass) { + self.agentClass = options.agentClass + } else if (options.forever) { + var v = version() + // use ForeverAgent in node 0.10- only + if (v.major === 0 && v.minor <= 10) { + self.agentClass = protocol === 'http:' ? ForeverAgent : ForeverAgent.SSL + } else { + self.agentClass = self.httpModule.Agent + self.agentOptions = self.agentOptions || {} + self.agentOptions.keepAlive = true + } + } else { + self.agentClass = self.httpModule.Agent + } + } + + if (self.pool === false) { + self.agent = false + } else { + self.agent = self.agent || self.getNewAgent() + } + + self.on('pipe', function (src) { + if (self.ntick && self._started) { + self.emit('error', new Error('You cannot pipe to this stream after the outbound request has started.')) + } + self.src = src + if (isReadStream(src)) { + if (!self.hasHeader('content-type')) { + self.setHeader('content-type', mime.lookup(src.path)) + } + } else { + if (src.headers) { + for (var i in src.headers) { + if (!self.hasHeader(i)) { + self.setHeader(i, src.headers[i]) + } + } + } + if (self._json && !self.hasHeader('content-type')) { + self.setHeader('content-type', 'application/json') + } + if (src.method && !self.explicitMethod) { + self.method = src.method + } + } + + // self.on('pipe', function () { + // console.error('You have already piped to this stream. Pipeing twice is likely to break the request.') + // }) + }) + + defer(function () { + if (self._aborted) { + return + } + + var end = function () { + if (self._form) { + if (!self._auth.hasAuth) { + self._form.pipe(self) + } else if (self._auth.hasAuth && self._auth.sentAuth) { + self._form.pipe(self) + } + } + if (self._multipart && self._multipart.chunked) { + self._multipart.body.pipe(self) + } + if (self.body) { + if (isstream(self.body)) { + self.body.pipe(self) + } else { + setContentLength() + if (Array.isArray(self.body)) { + self.body.forEach(function (part) { + self.write(part) + }) + } else { + self.write(self.body) + } + self.end() + } + } else if (self.requestBodyStream) { + console.warn('options.requestBodyStream is deprecated, please pass the request object to stream.pipe.') + self.requestBodyStream.pipe(self) + } else if (!self.src) { + if (self._auth.hasAuth && !self._auth.sentAuth) { + self.end() + return + } + if (self.method !== 'GET' && typeof self.method !== 'undefined') { + self.setHeader('content-length', 0) + } + self.end() + } + } + + if (self._form && !self.hasHeader('content-length')) { + // Before ending the request, we had to compute the length of the whole form, asyncly + self.setHeader(self._form.getHeaders(), true) + self._form.getLength(function (err, length) { + if (!err && !isNaN(length)) { + self.setHeader('content-length', length) + } + end() + }) + } else { + end() + } + + self.ntick = true + }) +} + +Request.prototype.getNewAgent = function () { + var self = this + var Agent = self.agentClass + var options = {} + if (self.agentOptions) { + for (var i in self.agentOptions) { + options[i] = self.agentOptions[i] + } + } + if (self.ca) { + options.ca = self.ca + } + if (self.ciphers) { + options.ciphers = self.ciphers + } + if (self.secureProtocol) { + options.secureProtocol = self.secureProtocol + } + if (self.secureOptions) { + options.secureOptions = self.secureOptions + } + if (typeof self.rejectUnauthorized !== 'undefined') { + options.rejectUnauthorized = self.rejectUnauthorized + } + + if (self.cert && self.key) { + options.key = self.key + options.cert = self.cert + } + + if (self.pfx) { + options.pfx = self.pfx + } + + if (self.passphrase) { + options.passphrase = self.passphrase + } + + var poolKey = '' + + // different types of agents are in different pools + if (Agent !== self.httpModule.Agent) { + poolKey += Agent.name + } + + // ca option is only relevant if proxy or destination are https + var proxy = self.proxy + if (typeof proxy === 'string') { + proxy = url.parse(proxy) + } + var isHttps = (proxy && proxy.protocol === 'https:') || this.uri.protocol === 'https:' + + if (isHttps) { + if (options.ca) { + if (poolKey) { + poolKey += ':' + } + poolKey += options.ca + } + + if (typeof options.rejectUnauthorized !== 'undefined') { + if (poolKey) { + poolKey += ':' + } + poolKey += options.rejectUnauthorized + } + + if (options.cert) { + if (poolKey) { + poolKey += ':' + } + poolKey += options.cert.toString('ascii') + options.key.toString('ascii') + } + + if (options.pfx) { + if (poolKey) { + poolKey += ':' + } + poolKey += options.pfx.toString('ascii') + } + + if (options.ciphers) { + if (poolKey) { + poolKey += ':' + } + poolKey += options.ciphers + } + + if (options.secureProtocol) { + if (poolKey) { + poolKey += ':' + } + poolKey += options.secureProtocol + } + + if (options.secureOptions) { + if (poolKey) { + poolKey += ':' + } + poolKey += options.secureOptions + } + } + + if (self.pool === globalPool && !poolKey && Object.keys(options).length === 0 && self.httpModule.globalAgent) { + // not doing anything special. Use the globalAgent + return self.httpModule.globalAgent + } + + // we're using a stored agent. Make sure it's protocol-specific + poolKey = self.uri.protocol + poolKey + + // generate a new agent for this setting if none yet exists + if (!self.pool[poolKey]) { + self.pool[poolKey] = new Agent(options) + // properly set maxSockets on new agents + if (self.pool.maxSockets) { + self.pool[poolKey].maxSockets = self.pool.maxSockets + } + } + + return self.pool[poolKey] +} + +Request.prototype.start = function () { + // start() is called once we are ready to send the outgoing HTTP request. + // this is usually called on the first write(), end() or on nextTick() + var self = this + + if (self.timing) { + // All timings will be relative to this request's startTime. In order to do this, + // we need to capture the wall-clock start time (via Date), immediately followed + // by the high-resolution timer (via now()). While these two won't be set + // at the _exact_ same time, they should be close enough to be able to calculate + // high-resolution, monotonically non-decreasing timestamps relative to startTime. + var startTime = new Date().getTime() + var startTimeNow = now() + } + + if (self._aborted) { + return + } + + self._started = true + self.method = self.method || 'GET' + self.href = self.uri.href + + if (self.src && self.src.stat && self.src.stat.size && !self.hasHeader('content-length')) { + self.setHeader('content-length', self.src.stat.size) + } + if (self._aws) { + self.aws(self._aws, true) + } + + // We have a method named auth, which is completely different from the http.request + // auth option. If we don't remove it, we're gonna have a bad time. + var reqOptions = copy(self) + delete reqOptions.auth + + debug('make request', self.uri.href) + + // node v6.8.0 now supports a `timeout` value in `http.request()`, but we + // should delete it for now since we handle timeouts manually for better + // consistency with node versions before v6.8.0 + delete reqOptions.timeout + + try { + self.req = self.httpModule.request(reqOptions) + } catch (err) { + self.emit('error', err) + return + } + + if (self.timing) { + self.startTime = startTime + self.startTimeNow = startTimeNow + + // Timing values will all be relative to startTime (by comparing to startTimeNow + // so we have an accurate clock) + self.timings = {} + } + + var timeout + if (self.timeout && !self.timeoutTimer) { + if (self.timeout < 0) { + timeout = 0 + } else if (typeof self.timeout === 'number' && isFinite(self.timeout)) { + timeout = self.timeout + } + } + + self.req.on('response', self.onRequestResponse.bind(self)) + self.req.on('error', self.onRequestError.bind(self)) + self.req.on('drain', function () { + self.emit('drain') + }) + + self.req.on('socket', function (socket) { + // `._connecting` was the old property which was made public in node v6.1.0 + var isConnecting = socket._connecting || socket.connecting + if (self.timing) { + self.timings.socket = now() - self.startTimeNow + + if (isConnecting) { + var onLookupTiming = function () { + self.timings.lookup = now() - self.startTimeNow + } + + var onConnectTiming = function () { + self.timings.connect = now() - self.startTimeNow + } + + socket.once('lookup', onLookupTiming) + socket.once('connect', onConnectTiming) + + // clean up timing event listeners if needed on error + self.req.once('error', function () { + socket.removeListener('lookup', onLookupTiming) + socket.removeListener('connect', onConnectTiming) + }) + } + } + + var setReqTimeout = function () { + // This timeout sets the amount of time to wait *between* bytes sent + // from the server once connected. + // + // In particular, it's useful for erroring if the server fails to send + // data halfway through streaming a response. + self.req.setTimeout(timeout, function () { + if (self.req) { + self.abort() + var e = new Error('ESOCKETTIMEDOUT') + e.code = 'ESOCKETTIMEDOUT' + e.connect = false + self.emit('error', e) + } + }) + } + if (timeout !== undefined) { + // Only start the connection timer if we're actually connecting a new + // socket, otherwise if we're already connected (because this is a + // keep-alive connection) do not bother. This is important since we won't + // get a 'connect' event for an already connected socket. + if (isConnecting) { + var onReqSockConnect = function () { + socket.removeListener('connect', onReqSockConnect) + self.clearTimeout() + setReqTimeout() + } + + socket.on('connect', onReqSockConnect) + + self.req.on('error', function (err) { // eslint-disable-line handle-callback-err + socket.removeListener('connect', onReqSockConnect) + }) + + // Set a timeout in memory - this block will throw if the server takes more + // than `timeout` to write the HTTP status and headers (corresponding to + // the on('response') event on the client). NB: this measures wall-clock + // time, not the time between bytes sent by the server. + self.timeoutTimer = setTimeout(function () { + socket.removeListener('connect', onReqSockConnect) + self.abort() + var e = new Error('ETIMEDOUT') + e.code = 'ETIMEDOUT' + e.connect = true + self.emit('error', e) + }, timeout) + } else { + // We're already connected + setReqTimeout() + } + } + self.emit('socket', socket) + }) + + self.emit('request', self.req) +} + +Request.prototype.onRequestError = function (error) { + var self = this + if (self._aborted) { + return + } + if (self.req && self.req._reusedSocket && error.code === 'ECONNRESET' && + self.agent.addRequestNoreuse) { + self.agent = { addRequest: self.agent.addRequestNoreuse.bind(self.agent) } + self.start() + self.req.end() + return + } + self.clearTimeout() + self.emit('error', error) +} + +Request.prototype.onRequestResponse = function (response) { + var self = this + + if (self.timing) { + self.timings.response = now() - self.startTimeNow + } + + debug('onRequestResponse', self.uri.href, response.statusCode, response.headers) + response.on('end', function () { + if (self.timing) { + self.timings.end = now() - self.startTimeNow + response.timingStart = self.startTime + + // fill in the blanks for any periods that didn't trigger, such as + // no lookup or connect due to keep alive + if (!self.timings.socket) { + self.timings.socket = 0 + } + if (!self.timings.lookup) { + self.timings.lookup = self.timings.socket + } + if (!self.timings.connect) { + self.timings.connect = self.timings.lookup + } + if (!self.timings.response) { + self.timings.response = self.timings.connect + } + + debug('elapsed time', self.timings.end) + + // elapsedTime includes all redirects + self.elapsedTime += Math.round(self.timings.end) + + // NOTE: elapsedTime is deprecated in favor of .timings + response.elapsedTime = self.elapsedTime + + // timings is just for the final fetch + response.timings = self.timings + + // pre-calculate phase timings as well + response.timingPhases = { + wait: self.timings.socket, + dns: self.timings.lookup - self.timings.socket, + tcp: self.timings.connect - self.timings.lookup, + firstByte: self.timings.response - self.timings.connect, + download: self.timings.end - self.timings.response, + total: self.timings.end + } + } + debug('response end', self.uri.href, response.statusCode, response.headers) + }) + + if (self._aborted) { + debug('aborted', self.uri.href) + response.resume() + return + } + + self.response = response + response.request = self + response.toJSON = responseToJSON + + // XXX This is different on 0.10, because SSL is strict by default + if (self.httpModule === https && + self.strictSSL && (!response.hasOwnProperty('socket') || + !response.socket.authorized)) { + debug('strict ssl error', self.uri.href) + var sslErr = response.hasOwnProperty('socket') ? response.socket.authorizationError : self.uri.href + ' does not support SSL' + self.emit('error', new Error('SSL Error: ' + sslErr)) + return + } + + // Save the original host before any redirect (if it changes, we need to + // remove any authorization headers). Also remember the case of the header + // name because lots of broken servers expect Host instead of host and we + // want the caller to be able to specify this. + self.originalHost = self.getHeader('host') + if (!self.originalHostHeaderName) { + self.originalHostHeaderName = self.hasHeader('host') + } + if (self.setHost) { + self.removeHeader('host') + } + self.clearTimeout() + + var targetCookieJar = (self._jar && self._jar.setCookie) ? self._jar : globalCookieJar + var addCookie = function (cookie) { + // set the cookie if it's domain in the href's domain. + try { + targetCookieJar.setCookie(cookie, self.uri.href, {ignoreError: true}) + } catch (e) { + self.emit('error', e) + } + } + + response.caseless = caseless(response.headers) + + if (response.caseless.has('set-cookie') && (!self._disableCookies)) { + var headerName = response.caseless.has('set-cookie') + if (Array.isArray(response.headers[headerName])) { + response.headers[headerName].forEach(addCookie) + } else { + addCookie(response.headers[headerName]) + } + } + + if (self._redirect.onResponse(response)) { + return // Ignore the rest of the response + } else { + // Be a good stream and emit end when the response is finished. + // Hack to emit end on close because of a core bug that never fires end + response.on('close', function () { + if (!self._ended) { + self.response.emit('end') + } + }) + + response.once('end', function () { + self._ended = true + }) + + var noBody = function (code) { + return ( + self.method === 'HEAD' || + // Informational + (code >= 100 && code < 200) || + // No Content + code === 204 || + // Not Modified + code === 304 + ) + } + + var responseContent + if (self.gzip && !noBody(response.statusCode)) { + var contentEncoding = response.headers['content-encoding'] || 'identity' + contentEncoding = contentEncoding.trim().toLowerCase() + + // Be more lenient with decoding compressed responses, since (very rarely) + // servers send slightly invalid gzip responses that are still accepted + // by common browsers. + // Always using Z_SYNC_FLUSH is what cURL does. + var zlibOptions = { + flush: zlib.Z_SYNC_FLUSH, + finishFlush: zlib.Z_SYNC_FLUSH + } + + if (contentEncoding === 'gzip') { + responseContent = zlib.createGunzip(zlibOptions) + response.pipe(responseContent) + } else if (contentEncoding === 'deflate') { + responseContent = zlib.createInflate(zlibOptions) + response.pipe(responseContent) + } else { + // Since previous versions didn't check for Content-Encoding header, + // ignore any invalid values to preserve backwards-compatibility + if (contentEncoding !== 'identity') { + debug('ignoring unrecognized Content-Encoding ' + contentEncoding) + } + responseContent = response + } + } else { + responseContent = response + } + + if (self.encoding) { + if (self.dests.length !== 0) { + console.error('Ignoring encoding parameter as this stream is being piped to another stream which makes the encoding option invalid.') + } else { + responseContent.setEncoding(self.encoding) + } + } + + if (self._paused) { + responseContent.pause() + } + + self.responseContent = responseContent + + self.emit('response', response) + + self.dests.forEach(function (dest) { + self.pipeDest(dest) + }) + + responseContent.on('data', function (chunk) { + if (self.timing && !self.responseStarted) { + self.responseStartTime = (new Date()).getTime() + + // NOTE: responseStartTime is deprecated in favor of .timings + response.responseStartTime = self.responseStartTime + } + self._destdata = true + self.emit('data', chunk) + }) + responseContent.once('end', function (chunk) { + self.emit('end', chunk) + }) + responseContent.on('error', function (error) { + self.emit('error', error) + }) + responseContent.on('close', function () { self.emit('close') }) + + if (self.callback) { + self.readResponseBody(response) + } else { // if no callback + self.on('end', function () { + if (self._aborted) { + debug('aborted', self.uri.href) + return + } + self.emit('complete', response) + }) + } + } + debug('finish init function', self.uri.href) +} + +Request.prototype.readResponseBody = function (response) { + var self = this + debug("reading response's body") + var buffers = [] + var bufferLength = 0 + var strings = [] + + self.on('data', function (chunk) { + if (!Buffer.isBuffer(chunk)) { + strings.push(chunk) + } else if (chunk.length) { + bufferLength += chunk.length + buffers.push(chunk) + } + }) + self.on('end', function () { + debug('end event', self.uri.href) + if (self._aborted) { + debug('aborted', self.uri.href) + // `buffer` is defined in the parent scope and used in a closure it exists for the life of the request. + // This can lead to leaky behavior if the user retains a reference to the request object. + buffers = [] + bufferLength = 0 + return + } + + if (bufferLength) { + debug('has body', self.uri.href, bufferLength) + response.body = Buffer.concat(buffers, bufferLength) + if (self.encoding !== null) { + response.body = response.body.toString(self.encoding) + } + // `buffer` is defined in the parent scope and used in a closure it exists for the life of the Request. + // This can lead to leaky behavior if the user retains a reference to the request object. + buffers = [] + bufferLength = 0 + } else if (strings.length) { + // The UTF8 BOM [0xEF,0xBB,0xBF] is converted to [0xFE,0xFF] in the JS UTC16/UCS2 representation. + // Strip this value out when the encoding is set to 'utf8', as upstream consumers won't expect it and it breaks JSON.parse(). + if (self.encoding === 'utf8' && strings[0].length > 0 && strings[0][0] === '\uFEFF') { + strings[0] = strings[0].substring(1) + } + response.body = strings.join('') + } + + if (self._json) { + try { + response.body = JSON.parse(response.body, self._jsonReviver) + } catch (e) { + debug('invalid JSON received', self.uri.href) + } + } + debug('emitting complete', self.uri.href) + if (typeof response.body === 'undefined' && !self._json) { + response.body = self.encoding === null ? Buffer.alloc(0) : '' + } + self.emit('complete', response, response.body) + }) +} + +Request.prototype.abort = function () { + var self = this + self._aborted = true + + if (self.req) { + self.req.abort() + } else if (self.response) { + self.response.destroy() + } + + self.clearTimeout() + self.emit('abort') +} + +Request.prototype.pipeDest = function (dest) { + var self = this + var response = self.response + // Called after the response is received + if (dest.headers && !dest.headersSent) { + if (response.caseless.has('content-type')) { + var ctname = response.caseless.has('content-type') + if (dest.setHeader) { + dest.setHeader(ctname, response.headers[ctname]) + } else { + dest.headers[ctname] = response.headers[ctname] + } + } + + if (response.caseless.has('content-length')) { + var clname = response.caseless.has('content-length') + if (dest.setHeader) { + dest.setHeader(clname, response.headers[clname]) + } else { + dest.headers[clname] = response.headers[clname] + } + } + } + if (dest.setHeader && !dest.headersSent) { + for (var i in response.headers) { + // If the response content is being decoded, the Content-Encoding header + // of the response doesn't represent the piped content, so don't pass it. + if (!self.gzip || i !== 'content-encoding') { + dest.setHeader(i, response.headers[i]) + } + } + dest.statusCode = response.statusCode + } + if (self.pipefilter) { + self.pipefilter(response, dest) + } +} + +Request.prototype.qs = function (q, clobber) { + var self = this + var base + if (!clobber && self.uri.query) { + base = self._qs.parse(self.uri.query) + } else { + base = {} + } + + for (var i in q) { + base[i] = q[i] + } + + var qs = self._qs.stringify(base) + + if (qs === '') { + return self + } + + self.uri = url.parse(self.uri.href.split('?')[0] + '?' + qs) + self.url = self.uri + self.path = self.uri.path + + if (self.uri.host === 'unix') { + self.enableUnixSocket() + } + + return self +} +Request.prototype.form = function (form) { + var self = this + if (form) { + if (!/^application\/x-www-form-urlencoded\b/.test(self.getHeader('content-type'))) { + self.setHeader('content-type', 'application/x-www-form-urlencoded') + } + self.body = (typeof form === 'string') + ? self._qs.rfc3986(form.toString('utf8')) + : self._qs.stringify(form).toString('utf8') + return self + } + // create form-data object + self._form = new FormData() + self._form.on('error', function (err) { + err.message = 'form-data: ' + err.message + self.emit('error', err) + self.abort() + }) + return self._form +} +Request.prototype.multipart = function (multipart) { + var self = this + + self._multipart.onRequest(multipart) + + if (!self._multipart.chunked) { + self.body = self._multipart.body + } + + return self +} +Request.prototype.json = function (val) { + var self = this + + if (!self.hasHeader('accept')) { + self.setHeader('accept', 'application/json') + } + + if (typeof self.jsonReplacer === 'function') { + self._jsonReplacer = self.jsonReplacer + } + + self._json = true + if (typeof val === 'boolean') { + if (self.body !== undefined) { + if (!/^application\/x-www-form-urlencoded\b/.test(self.getHeader('content-type'))) { + self.body = safeStringify(self.body, self._jsonReplacer) + } else { + self.body = self._qs.rfc3986(self.body) + } + if (!self.hasHeader('content-type')) { + self.setHeader('content-type', 'application/json') + } + } + } else { + self.body = safeStringify(val, self._jsonReplacer) + if (!self.hasHeader('content-type')) { + self.setHeader('content-type', 'application/json') + } + } + + if (typeof self.jsonReviver === 'function') { + self._jsonReviver = self.jsonReviver + } + + return self +} +Request.prototype.getHeader = function (name, headers) { + var self = this + var result, re, match + if (!headers) { + headers = self.headers + } + Object.keys(headers).forEach(function (key) { + if (key.length !== name.length) { + return + } + re = new RegExp(name, 'i') + match = key.match(re) + if (match) { + result = headers[key] + } + }) + return result +} +Request.prototype.enableUnixSocket = function () { + // Get the socket & request paths from the URL + var unixParts = this.uri.path.split(':') + var host = unixParts[0] + var path = unixParts[1] + // Apply unix properties to request + this.socketPath = host + this.uri.pathname = path + this.uri.path = path + this.uri.host = host + this.uri.hostname = host + this.uri.isUnix = true +} + +Request.prototype.auth = function (user, pass, sendImmediately, bearer) { + var self = this + + self._auth.onRequest(user, pass, sendImmediately, bearer) + + return self +} +Request.prototype.aws = function (opts, now) { + var self = this + + if (!now) { + self._aws = opts + return self + } + + if (opts.sign_version === 4 || opts.sign_version === '4') { + // use aws4 + var options = { + host: self.uri.host, + path: self.uri.path, + method: self.method, + headers: self.headers, + body: self.body + } + if (opts.service) { + options.service = opts.service + } + var signRes = aws4.sign(options, { + accessKeyId: opts.key, + secretAccessKey: opts.secret, + sessionToken: opts.session + }) + self.setHeader('authorization', signRes.headers.Authorization) + self.setHeader('x-amz-date', signRes.headers['X-Amz-Date']) + if (signRes.headers['X-Amz-Security-Token']) { + self.setHeader('x-amz-security-token', signRes.headers['X-Amz-Security-Token']) + } + } else { + // default: use aws-sign2 + var date = new Date() + self.setHeader('date', date.toUTCString()) + var auth = { + key: opts.key, + secret: opts.secret, + verb: self.method.toUpperCase(), + date: date, + contentType: self.getHeader('content-type') || '', + md5: self.getHeader('content-md5') || '', + amazonHeaders: aws2.canonicalizeHeaders(self.headers) + } + var path = self.uri.path + if (opts.bucket && path) { + auth.resource = '/' + opts.bucket + path + } else if (opts.bucket && !path) { + auth.resource = '/' + opts.bucket + } else if (!opts.bucket && path) { + auth.resource = path + } else if (!opts.bucket && !path) { + auth.resource = '/' + } + auth.resource = aws2.canonicalizeResource(auth.resource) + self.setHeader('authorization', aws2.authorization(auth)) + } + + return self +} +Request.prototype.httpSignature = function (opts) { + var self = this + httpSignature.signRequest({ + getHeader: function (header) { + return self.getHeader(header, self.headers) + }, + setHeader: function (header, value) { + self.setHeader(header, value) + }, + method: self.method, + path: self.path + }, opts) + debug('httpSignature authorization', self.getHeader('authorization')) + + return self +} +Request.prototype.hawk = function (opts) { + var self = this + self.setHeader('Authorization', hawk.header(self.uri, self.method, opts)) +} +Request.prototype.oauth = function (_oauth) { + var self = this + + self._oauth.onRequest(_oauth) + + return self +} + +Request.prototype.jar = function (jar) { + var self = this + var cookies + + if (self._redirect.redirectsFollowed === 0) { + self.originalCookieHeader = self.getHeader('cookie') + } + + if (!jar) { + // disable cookies + cookies = false + self._disableCookies = true + } else { + var targetCookieJar = jar.getCookieString ? jar : globalCookieJar + var urihref = self.uri.href + // fetch cookie in the Specified host + if (targetCookieJar) { + cookies = targetCookieJar.getCookieString(urihref) + } + } + + // if need cookie and cookie is not empty + if (cookies && cookies.length) { + if (self.originalCookieHeader) { + // Don't overwrite existing Cookie header + self.setHeader('cookie', self.originalCookieHeader + '; ' + cookies) + } else { + self.setHeader('cookie', cookies) + } + } + self._jar = jar + return self +} + +// Stream API +Request.prototype.pipe = function (dest, opts) { + var self = this + + if (self.response) { + if (self._destdata) { + self.emit('error', new Error('You cannot pipe after data has been emitted from the response.')) + } else if (self._ended) { + self.emit('error', new Error('You cannot pipe after the response has been ended.')) + } else { + stream.Stream.prototype.pipe.call(self, dest, opts) + self.pipeDest(dest) + return dest + } + } else { + self.dests.push(dest) + stream.Stream.prototype.pipe.call(self, dest, opts) + return dest + } +} +Request.prototype.write = function () { + var self = this + if (self._aborted) { return } + + if (!self._started) { + self.start() + } + if (self.req) { + return self.req.write.apply(self.req, arguments) + } +} +Request.prototype.end = function (chunk) { + var self = this + if (self._aborted) { return } + + if (chunk) { + self.write(chunk) + } + if (!self._started) { + self.start() + } + if (self.req) { + self.req.end() + } +} +Request.prototype.pause = function () { + var self = this + if (!self.responseContent) { + self._paused = true + } else { + self.responseContent.pause.apply(self.responseContent, arguments) + } +} +Request.prototype.resume = function () { + var self = this + if (!self.responseContent) { + self._paused = false + } else { + self.responseContent.resume.apply(self.responseContent, arguments) + } +} +Request.prototype.destroy = function () { + var self = this + this.clearTimeout() + if (!self._ended) { + self.end() + } else if (self.response) { + self.response.destroy() + } +} + +Request.prototype.clearTimeout = function () { + if (this.timeoutTimer) { + clearTimeout(this.timeoutTimer) + this.timeoutTimer = null + } +} + +Request.defaultProxyHeaderWhiteList = + Tunnel.defaultProxyHeaderWhiteList.slice() + +Request.defaultProxyHeaderExclusiveList = + Tunnel.defaultProxyHeaderExclusiveList.slice() + +// Exports + +Request.prototype.toJSON = requestToJSON +module.exports = Request + + +/***/ }), + +/***/ 459: +/***/ (function(module) { + +// generated by genversion +module.exports = '2.5.0' + + +/***/ }), + +/***/ 461: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; + + +var url = __webpack_require__(835) +var tunnel = __webpack_require__(243) + +var defaultProxyHeaderWhiteList = [ + 'accept', + 'accept-charset', + 'accept-encoding', + 'accept-language', + 'accept-ranges', + 'cache-control', + 'content-encoding', + 'content-language', + 'content-location', + 'content-md5', + 'content-range', + 'content-type', + 'connection', + 'date', + 'expect', + 'max-forwards', + 'pragma', + 'referer', + 'te', + 'user-agent', + 'via' +] + +var defaultProxyHeaderExclusiveList = [ + 'proxy-authorization' +] + +function constructProxyHost (uriObject) { + var port = uriObject.port + var protocol = uriObject.protocol + var proxyHost = uriObject.hostname + ':' + + if (port) { + proxyHost += port + } else if (protocol === 'https:') { + proxyHost += '443' + } else { + proxyHost += '80' + } + + return proxyHost +} + +function constructProxyHeaderWhiteList (headers, proxyHeaderWhiteList) { + var whiteList = proxyHeaderWhiteList + .reduce(function (set, header) { + set[header.toLowerCase()] = true + return set + }, {}) + + return Object.keys(headers) + .filter(function (header) { + return whiteList[header.toLowerCase()] + }) + .reduce(function (set, header) { + set[header] = headers[header] + return set + }, {}) +} + +function constructTunnelOptions (request, proxyHeaders) { + var proxy = request.proxy + + var tunnelOptions = { + proxy: { + host: proxy.hostname, + port: +proxy.port, + proxyAuth: proxy.auth, + headers: proxyHeaders + }, + headers: request.headers, + ca: request.ca, + cert: request.cert, + key: request.key, + passphrase: request.passphrase, + pfx: request.pfx, + ciphers: request.ciphers, + rejectUnauthorized: request.rejectUnauthorized, + secureOptions: request.secureOptions, + secureProtocol: request.secureProtocol + } + + return tunnelOptions +} + +function constructTunnelFnName (uri, proxy) { + var uriProtocol = (uri.protocol === 'https:' ? 'https' : 'http') + var proxyProtocol = (proxy.protocol === 'https:' ? 'Https' : 'Http') + return [uriProtocol, proxyProtocol].join('Over') +} + +function getTunnelFn (request) { + var uri = request.uri + var proxy = request.proxy + var tunnelFnName = constructTunnelFnName(uri, proxy) + return tunnel[tunnelFnName] +} + +function Tunnel (request) { + this.request = request + this.proxyHeaderWhiteList = defaultProxyHeaderWhiteList + this.proxyHeaderExclusiveList = [] + if (typeof request.tunnel !== 'undefined') { + this.tunnelOverride = request.tunnel + } +} + +Tunnel.prototype.isEnabled = function () { + var self = this + var request = self.request + // Tunnel HTTPS by default. Allow the user to override this setting. + + // If self.tunnelOverride is set (the user specified a value), use it. + if (typeof self.tunnelOverride !== 'undefined') { + return self.tunnelOverride + } + + // If the destination is HTTPS, tunnel. + if (request.uri.protocol === 'https:') { + return true + } + + // Otherwise, do not use tunnel. + return false +} + +Tunnel.prototype.setup = function (options) { + var self = this + var request = self.request + + options = options || {} + + if (typeof request.proxy === 'string') { + request.proxy = url.parse(request.proxy) + } + + if (!request.proxy || !request.tunnel) { + return false + } + + // Setup Proxy Header Exclusive List and White List + if (options.proxyHeaderWhiteList) { + self.proxyHeaderWhiteList = options.proxyHeaderWhiteList + } + if (options.proxyHeaderExclusiveList) { + self.proxyHeaderExclusiveList = options.proxyHeaderExclusiveList + } + + var proxyHeaderExclusiveList = self.proxyHeaderExclusiveList.concat(defaultProxyHeaderExclusiveList) + var proxyHeaderWhiteList = self.proxyHeaderWhiteList.concat(proxyHeaderExclusiveList) + + // Setup Proxy Headers and Proxy Headers Host + // Only send the Proxy White Listed Header names + var proxyHeaders = constructProxyHeaderWhiteList(request.headers, proxyHeaderWhiteList) + proxyHeaders.host = constructProxyHost(request.uri) + + proxyHeaderExclusiveList.forEach(request.removeHeader, request) + + // Set Agent from Tunnel Data + var tunnelFn = getTunnelFn(request) + var tunnelOptions = constructTunnelOptions(request, proxyHeaders) + request.agent = tunnelFn(tunnelOptions) + + return true +} + +Tunnel.defaultProxyHeaderWhiteList = defaultProxyHeaderWhiteList +Tunnel.defaultProxyHeaderExclusiveList = defaultProxyHeaderExclusiveList +exports.Tunnel = Tunnel + + +/***/ }), + +/***/ 469: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; + + +var uuid = __webpack_require__(826) +var CombinedStream = __webpack_require__(547) +var isstream = __webpack_require__(382) +var Buffer = __webpack_require__(149).Buffer + +function Multipart (request) { + this.request = request + this.boundary = uuid() + this.chunked = false + this.body = null +} + +Multipart.prototype.isChunked = function (options) { + var self = this + var chunked = false + var parts = options.data || options + + if (!parts.forEach) { + self.request.emit('error', new Error('Argument error, options.multipart.')) + } + + if (options.chunked !== undefined) { + chunked = options.chunked + } + + if (self.request.getHeader('transfer-encoding') === 'chunked') { + chunked = true + } + + if (!chunked) { + parts.forEach(function (part) { + if (typeof part.body === 'undefined') { + self.request.emit('error', new Error('Body attribute missing in multipart.')) + } + if (isstream(part.body)) { + chunked = true + } + }) + } + + return chunked +} + +Multipart.prototype.setHeaders = function (chunked) { + var self = this + + if (chunked && !self.request.hasHeader('transfer-encoding')) { + self.request.setHeader('transfer-encoding', 'chunked') + } + + var header = self.request.getHeader('content-type') + + if (!header || header.indexOf('multipart') === -1) { + self.request.setHeader('content-type', 'multipart/related; boundary=' + self.boundary) + } else { + if (header.indexOf('boundary') !== -1) { + self.boundary = header.replace(/.*boundary=([^\s;]+).*/, '$1') + } else { + self.request.setHeader('content-type', header + '; boundary=' + self.boundary) + } + } +} + +Multipart.prototype.build = function (parts, chunked) { + var self = this + var body = chunked ? new CombinedStream() : [] + + function add (part) { + if (typeof part === 'number') { + part = part.toString() + } + return chunked ? body.append(part) : body.push(Buffer.from(part)) + } + + if (self.request.preambleCRLF) { + add('\r\n') + } + + parts.forEach(function (part) { + var preamble = '--' + self.boundary + '\r\n' + Object.keys(part).forEach(function (key) { + if (key === 'body') { return } + preamble += key + ': ' + part[key] + '\r\n' + }) + preamble += '\r\n' + add(preamble) + add(part.body) + add('\r\n') + }) + add('--' + self.boundary + '--') + + if (self.request.postambleCRLF) { + add('\r\n') + } + + return body +} + +Multipart.prototype.onRequest = function (options) { + var self = this + + var chunked = self.isChunked(options) + var parts = options.data || options + + self.setHeaders(chunked) + self.chunked = chunked + self.body = self.build(parts, chunked) +} + +exports.Multipart = Multipart + + +/***/ }), + +/***/ 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 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 + */ +function exportVariable(name, val) { + process.env[name] = val; + command_1.issueCommand('set-env', { name }, val); +} +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) { + 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 + */ +function setOutput(name, value) { + command_1.issueCommand('set-output', { name }, value); +} +exports.setOutput = setOutput; +//----------------------------------------------------------------------- +// 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 +//----------------------------------------------------------------------- +/** + * 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 + */ +function error(message) { + command_1.issue('error', message); +} +exports.error = error; +/** + * Adds an warning issue + * @param message warning issue message + */ +function warning(message) { + command_1.issue('warning', 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 + */ +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 + +/***/ }), + +/***/ 477: +/***/ (function(module, __unusedexports, __webpack_require__) { + +// Copyright (c) 2012, Mark Cavage. All rights reserved. +// Copyright 2015 Joyent, Inc. + +var assert = __webpack_require__(357); +var Stream = __webpack_require__(413).Stream; +var util = __webpack_require__(669); + + +///--- Globals + +/* JSSTYLED */ +var UUID_REGEXP = /^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$/; + + +///--- Internal + +function _capitalize(str) { + return (str.charAt(0).toUpperCase() + str.slice(1)); +} + +function _toss(name, expected, oper, arg, actual) { + throw new assert.AssertionError({ + message: util.format('%s (%s) is required', name, expected), + actual: (actual === undefined) ? typeof (arg) : actual(arg), + expected: expected, + operator: oper || '===', + stackStartFunction: _toss.caller + }); +} + +function _getClass(arg) { + return (Object.prototype.toString.call(arg).slice(8, -1)); +} + +function noop() { + // Why even bother with asserts? +} + + +///--- Exports + +var types = { + bool: { + check: function (arg) { return typeof (arg) === 'boolean'; } + }, + func: { + check: function (arg) { return typeof (arg) === 'function'; } + }, + string: { + check: function (arg) { return typeof (arg) === 'string'; } + }, + object: { + check: function (arg) { + return typeof (arg) === 'object' && arg !== null; + } + }, + number: { + check: function (arg) { + return typeof (arg) === 'number' && !isNaN(arg); + } + }, + finite: { + check: function (arg) { + return typeof (arg) === 'number' && !isNaN(arg) && isFinite(arg); + } + }, + buffer: { + check: function (arg) { return Buffer.isBuffer(arg); }, + operator: 'Buffer.isBuffer' + }, + array: { + check: function (arg) { return Array.isArray(arg); }, + operator: 'Array.isArray' + }, + stream: { + check: function (arg) { return arg instanceof Stream; }, + operator: 'instanceof', + actual: _getClass + }, + date: { + check: function (arg) { return arg instanceof Date; }, + operator: 'instanceof', + actual: _getClass + }, + regexp: { + check: function (arg) { return arg instanceof RegExp; }, + operator: 'instanceof', + actual: _getClass + }, + uuid: { + check: function (arg) { + return typeof (arg) === 'string' && UUID_REGEXP.test(arg); + }, + operator: 'isUUID' + } +}; + +function _setExports(ndebug) { + var keys = Object.keys(types); + var out; + + /* re-export standard assert */ + if (process.env.NODE_NDEBUG) { + out = noop; + } else { + out = function (arg, msg) { + if (!arg) { + _toss(msg, 'true', arg); + } + }; + } + + /* standard checks */ + keys.forEach(function (k) { + if (ndebug) { + out[k] = noop; + return; + } + var type = types[k]; + out[k] = function (arg, msg) { + if (!type.check(arg)) { + _toss(msg, k, type.operator, arg, type.actual); + } + }; + }); + + /* optional checks */ + keys.forEach(function (k) { + var name = 'optional' + _capitalize(k); + if (ndebug) { + out[name] = noop; + return; + } + var type = types[k]; + out[name] = function (arg, msg) { + if (arg === undefined || arg === null) { + return; + } + if (!type.check(arg)) { + _toss(msg, k, type.operator, arg, type.actual); + } + }; + }); + + /* arrayOf checks */ + keys.forEach(function (k) { + var name = 'arrayOf' + _capitalize(k); + if (ndebug) { + out[name] = noop; + return; + } + var type = types[k]; + var expected = '[' + k + ']'; + out[name] = function (arg, msg) { + if (!Array.isArray(arg)) { + _toss(msg, expected, type.operator, arg, type.actual); + } + var i; + for (i = 0; i < arg.length; i++) { + if (!type.check(arg[i])) { + _toss(msg, expected, type.operator, arg, type.actual); + } + } + }; + }); + + /* optionalArrayOf checks */ + keys.forEach(function (k) { + var name = 'optionalArrayOf' + _capitalize(k); + if (ndebug) { + out[name] = noop; + return; + } + var type = types[k]; + var expected = '[' + k + ']'; + out[name] = function (arg, msg) { + if (arg === undefined || arg === null) { + return; + } + if (!Array.isArray(arg)) { + _toss(msg, expected, type.operator, arg, type.actual); + } + var i; + for (i = 0; i < arg.length; i++) { + if (!type.check(arg[i])) { + _toss(msg, expected, type.operator, arg, type.actual); + } + } + }; + }); + + /* re-export built-in assertions */ + Object.keys(assert).forEach(function (k) { + if (k === 'AssertionError') { + out[k] = assert[k]; + return; + } + if (ndebug) { + out[k] = noop; + return; + } + out[k] = assert[k]; + }); + + /* export ourselves (for unit tests _only_) */ + out._setExports = _setExports; + + return out; +} + +module.exports = _setExports(process.env.NODE_NDEBUG); + + +/***/ }), + +/***/ 479: +/***/ (function(module) { + +"use strict"; + +module.exports = function generate_if(it, $keyword, $ruleType) { + var out = ' '; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $data = 'data' + ($dataLvl || ''); + var $valid = 'valid' + $lvl; + var $errs = 'errs__' + $lvl; + var $it = it.util.copy(it); + $it.level++; + var $nextValid = 'valid' + $it.level; + var $thenSch = it.schema['then'], + $elseSch = it.schema['else'], + $thenPresent = $thenSch !== undefined && (it.opts.strictKeywords ? typeof $thenSch == 'object' && Object.keys($thenSch).length > 0 : it.util.schemaHasRules($thenSch, it.RULES.all)), + $elsePresent = $elseSch !== undefined && (it.opts.strictKeywords ? typeof $elseSch == 'object' && Object.keys($elseSch).length > 0 : it.util.schemaHasRules($elseSch, it.RULES.all)), + $currentBaseId = $it.baseId; + if ($thenPresent || $elsePresent) { + var $ifClause; + $it.createErrors = false; + $it.schema = $schema; + $it.schemaPath = $schemaPath; + $it.errSchemaPath = $errSchemaPath; + out += ' var ' + ($errs) + ' = errors; var ' + ($valid) + ' = true; '; + var $wasComposite = it.compositeRule; + it.compositeRule = $it.compositeRule = true; + out += ' ' + (it.validate($it)) + ' '; + $it.baseId = $currentBaseId; + $it.createErrors = true; + out += ' errors = ' + ($errs) + '; if (vErrors !== null) { if (' + ($errs) + ') vErrors.length = ' + ($errs) + '; else vErrors = null; } '; + it.compositeRule = $it.compositeRule = $wasComposite; + if ($thenPresent) { + out += ' if (' + ($nextValid) + ') { '; + $it.schema = it.schema['then']; + $it.schemaPath = it.schemaPath + '.then'; + $it.errSchemaPath = it.errSchemaPath + '/then'; + out += ' ' + (it.validate($it)) + ' '; + $it.baseId = $currentBaseId; + out += ' ' + ($valid) + ' = ' + ($nextValid) + '; '; + if ($thenPresent && $elsePresent) { + $ifClause = 'ifClause' + $lvl; + out += ' var ' + ($ifClause) + ' = \'then\'; '; + } else { + $ifClause = '\'then\''; + } + out += ' } '; + if ($elsePresent) { + out += ' else { '; + } + } else { + out += ' if (!' + ($nextValid) + ') { '; + } + if ($elsePresent) { + $it.schema = it.schema['else']; + $it.schemaPath = it.schemaPath + '.else'; + $it.errSchemaPath = it.errSchemaPath + '/else'; + out += ' ' + (it.validate($it)) + ' '; + $it.baseId = $currentBaseId; + out += ' ' + ($valid) + ' = ' + ($nextValid) + '; '; + if ($thenPresent && $elsePresent) { + $ifClause = 'ifClause' + $lvl; + out += ' var ' + ($ifClause) + ' = \'else\'; '; + } else { + $ifClause = '\'else\''; + } + out += ' } '; + } + out += ' if (!' + ($valid) + ') { var err = '; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('if') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { failingKeyword: ' + ($ifClause) + ' } '; + if (it.opts.messages !== false) { + out += ' , message: \'should match "\' + ' + ($ifClause) + ' + \'" schema\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + out += '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError(vErrors); '; + } else { + out += ' validate.errors = vErrors; return false; '; + } + } + out += ' } '; + if ($breakOnError) { + out += ' else { '; + } + out = it.util.cleanUpCode(out); + } else { + if ($breakOnError) { + out += ' if (true) { '; + } + } + return out; +} + + +/***/ }), + +/***/ 496: +/***/ (function(module, __unusedexports, __webpack_require__) { + +"use strict"; + + +var ruleModules = __webpack_require__(894) + , toHash = __webpack_require__(855).toHash; + +module.exports = function rules() { + var RULES = [ + { type: 'number', + rules: [ { 'maximum': ['exclusiveMaximum'] }, + { 'minimum': ['exclusiveMinimum'] }, 'multipleOf', 'format'] }, + { type: 'string', + rules: [ 'maxLength', 'minLength', 'pattern', 'format' ] }, + { type: 'array', + rules: [ 'maxItems', 'minItems', 'items', 'contains', 'uniqueItems' ] }, + { type: 'object', + rules: [ 'maxProperties', 'minProperties', 'required', 'dependencies', 'propertyNames', + { 'properties': ['additionalProperties', 'patternProperties'] } ] }, + { rules: [ '$ref', 'const', 'enum', 'not', 'anyOf', 'oneOf', 'allOf', 'if' ] } + ]; + + var ALL = [ 'type', '$comment' ]; + var KEYWORDS = [ + '$schema', '$id', 'id', '$data', '$async', 'title', + 'description', 'default', 'definitions', + 'examples', 'readOnly', 'writeOnly', + 'contentMediaType', 'contentEncoding', + 'additionalItems', 'then', 'else' + ]; + var TYPES = [ 'number', 'integer', 'string', 'array', 'object', 'boolean', 'null' ]; + RULES.all = toHash(ALL); + RULES.types = toHash(TYPES); + + RULES.forEach(function (group) { + group.rules = group.rules.map(function (keyword) { + var implKeywords; + if (typeof keyword == 'object') { + var key = Object.keys(keyword)[0]; + implKeywords = keyword[key]; + keyword = key; + implKeywords.forEach(function (k) { + ALL.push(k); + RULES.all[k] = true; + }); + } + ALL.push(keyword); + var rule = RULES.all[keyword] = { + keyword: keyword, + code: ruleModules[keyword], + implements: implKeywords + }; + return rule; + }); + + RULES.all.$comment = { + keyword: '$comment', + code: ruleModules.$comment + }; + + if (group.type) RULES.types[group.type] = group; + }); + + RULES.keywords = toHash(ALL.concat(KEYWORDS)); + RULES.custom = {}; + + return RULES; +}; + + +/***/ }), + +/***/ 500: +/***/ (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); + } +} + + +/***/ }), + +/***/ 502: +/***/ (function(module, __unusedexports, __webpack_require__) { + +// Copyright 2017 Joyent, Inc. + +module.exports = PrivateKey; + +var assert = __webpack_require__(477); +var Buffer = __webpack_require__(215).Buffer; +var algs = __webpack_require__(98); +var crypto = __webpack_require__(417); +var Fingerprint = __webpack_require__(400); +var Signature = __webpack_require__(575); +var errs = __webpack_require__(753); +var util = __webpack_require__(669); +var utils = __webpack_require__(270); +var dhe = __webpack_require__(290); +var generateECDSA = dhe.generateECDSA; +var generateED25519 = dhe.generateED25519; +var edCompat = __webpack_require__(363); +var nacl = __webpack_require__(196); + +var Key = __webpack_require__(852); + +var InvalidAlgorithmError = errs.InvalidAlgorithmError; +var KeyParseError = errs.KeyParseError; +var KeyEncryptedError = errs.KeyEncryptedError; + +var formats = {}; +formats['auto'] = __webpack_require__(241); +formats['pem'] = __webpack_require__(268); +formats['pkcs1'] = __webpack_require__(449); +formats['pkcs8'] = __webpack_require__(707); +formats['rfc4253'] = __webpack_require__(538); +formats['ssh-private'] = __webpack_require__(78); +formats['openssh'] = formats['ssh-private']; +formats['ssh'] = formats['ssh-private']; +formats['dnssec'] = __webpack_require__(982); + +function PrivateKey(opts) { + assert.object(opts, 'options'); + Key.call(this, opts); + + this._pubCache = undefined; +} +util.inherits(PrivateKey, Key); + +PrivateKey.formats = formats; + +PrivateKey.prototype.toBuffer = function (format, options) { + if (format === undefined) + format = 'pkcs1'; + assert.string(format, 'format'); + assert.object(formats[format], 'formats[format]'); + assert.optionalObject(options, 'options'); + + return (formats[format].write(this, options)); +}; + +PrivateKey.prototype.hash = function (algo, type) { + return (this.toPublic().hash(algo, type)); +}; + +PrivateKey.prototype.fingerprint = function (algo, type) { + return (this.toPublic().fingerprint(algo, type)); +}; + +PrivateKey.prototype.toPublic = function () { + if (this._pubCache) + return (this._pubCache); + + var algInfo = algs.info[this.type]; + var pubParts = []; + for (var i = 0; i < algInfo.parts.length; ++i) { + var p = algInfo.parts[i]; + pubParts.push(this.part[p]); + } + + this._pubCache = new Key({ + type: this.type, + source: this, + parts: pubParts + }); + if (this.comment) + this._pubCache.comment = this.comment; + return (this._pubCache); +}; + +PrivateKey.prototype.derive = function (newType) { + assert.string(newType, 'type'); + var priv, pub, pair; + + if (this.type === 'ed25519' && newType === 'curve25519') { + priv = this.part.k.data; + if (priv[0] === 0x00) + priv = priv.slice(1); + + pair = nacl.box.keyPair.fromSecretKey(new Uint8Array(priv)); + pub = Buffer.from(pair.publicKey); + + return (new PrivateKey({ + type: 'curve25519', + parts: [ + { name: 'A', data: utils.mpNormalize(pub) }, + { name: 'k', data: utils.mpNormalize(priv) } + ] + })); + } else if (this.type === 'curve25519' && newType === 'ed25519') { + priv = this.part.k.data; + if (priv[0] === 0x00) + priv = priv.slice(1); + + pair = nacl.sign.keyPair.fromSeed(new Uint8Array(priv)); + pub = Buffer.from(pair.publicKey); + + return (new PrivateKey({ + type: 'ed25519', + parts: [ + { name: 'A', data: utils.mpNormalize(pub) }, + { name: 'k', data: utils.mpNormalize(priv) } + ] + })); + } + throw (new Error('Key derivation not supported from ' + this.type + + ' to ' + newType)); +}; + +PrivateKey.prototype.createVerify = function (hashAlgo) { + return (this.toPublic().createVerify(hashAlgo)); +}; + +PrivateKey.prototype.createSign = function (hashAlgo) { + if (hashAlgo === undefined) + hashAlgo = this.defaultHashAlgorithm(); + assert.string(hashAlgo, 'hash algorithm'); + + /* ED25519 is not supported by OpenSSL, use a javascript impl. */ + if (this.type === 'ed25519' && edCompat !== undefined) + return (new edCompat.Signer(this, hashAlgo)); + if (this.type === 'curve25519') + throw (new Error('Curve25519 keys are not suitable for ' + + 'signing or verification')); + + var v, nm, err; + try { + nm = hashAlgo.toUpperCase(); + v = crypto.createSign(nm); + } catch (e) { + err = e; + } + if (v === undefined || (err instanceof Error && + err.message.match(/Unknown message digest/))) { + nm = 'RSA-'; + nm += hashAlgo.toUpperCase(); + v = crypto.createSign(nm); + } + assert.ok(v, 'failed to create verifier'); + var oldSign = v.sign.bind(v); + var key = this.toBuffer('pkcs1'); + var type = this.type; + var curve = this.curve; + v.sign = function () { + var sig = oldSign(key); + if (typeof (sig) === 'string') + sig = Buffer.from(sig, 'binary'); + sig = Signature.parse(sig, type, 'asn1'); + sig.hashAlgorithm = hashAlgo; + sig.curve = curve; + return (sig); + }; + return (v); +}; + +PrivateKey.parse = function (data, format, options) { + if (typeof (data) !== 'string') + assert.buffer(data, 'data'); + if (format === undefined) + format = 'auto'; + assert.string(format, 'format'); + if (typeof (options) === 'string') + options = { filename: options }; + assert.optionalObject(options, 'options'); + if (options === undefined) + options = {}; + assert.optionalString(options.filename, 'options.filename'); + if (options.filename === undefined) + options.filename = '(unnamed)'; + + assert.object(formats[format], 'formats[format]'); + + try { + var k = formats[format].read(data, options); + assert.ok(k instanceof PrivateKey, 'key is not a private key'); + if (!k.comment) + k.comment = options.filename; + return (k); + } catch (e) { + if (e.name === 'KeyEncryptedError') + throw (e); + throw (new KeyParseError(options.filename, format, e)); + } +}; + +PrivateKey.isPrivateKey = function (obj, ver) { + return (utils.isCompatible(obj, PrivateKey, ver)); +}; + +PrivateKey.generate = function (type, options) { + if (options === undefined) + options = {}; + assert.object(options, 'options'); + + switch (type) { + case 'ecdsa': + if (options.curve === undefined) + options.curve = 'nistp256'; + assert.string(options.curve, 'options.curve'); + return (generateECDSA(options.curve)); + case 'ed25519': + return (generateED25519()); + default: + throw (new Error('Key generation not supported with key ' + + 'type "' + type + '"')); + } +}; + +/* + * API versions for PrivateKey: + * [1,0] -- initial ver + * [1,1] -- added auto, pkcs[18], openssh/ssh-private formats + * [1,2] -- added defaultHashAlgorithm + * [1,3] -- added derive, ed, createDH + * [1,4] -- first tagged version + * [1,5] -- changed ed25519 part names and format + * [1,6] -- type arguments for hash() and fingerprint() + */ +PrivateKey.prototype._sshpkApiVersion = [1, 6]; + +PrivateKey._oldVersionDetect = function (obj) { + assert.func(obj.toPublic); + assert.func(obj.createSign); + if (obj.derive) + return ([1, 3]); + if (obj.defaultHashAlgorithm) + return ([1, 2]); + if (obj.formats['auto']) + return ([1, 1]); + return ([1, 0]); +}; + + +/***/ }), + +/***/ 512: +/***/ (function(module) { + +module.exports = {"application/1d-interleaved-parityfec":{"source":"iana"},"application/3gpdash-qoe-report+xml":{"source":"iana","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/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-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","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/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/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","compressible":true},"application/fhir+xml":{"source":"iana","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","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/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","compressible":true},"application/msc-mixer+xml":{"source":"iana","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"},"application/news-groupinfo":{"source":"iana"},"application/news-transmission":{"source":"iana"},"application/nlsml+xml":{"source":"iana","compressible":true},"application/node":{"source":"iana"},"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","compressible":true},"application/pidf-diff+xml":{"source":"iana","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","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/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/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-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/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.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.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.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","compressible":true},"application/vnd.omads-file+xml":{"source":"iana","compressible":true},"application/vnd.omads-folder+xml":{"source":"iana","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.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.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.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","compressible":true,"extensions":["xsm"]},"application/vnd.syncml.dm+wbxml":{"source":"iana","extensions":["bdm"]},"application/vnd.syncml.dm+xml":{"source":"iana","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","compressible":true,"extensions":["ddf"]},"application/vnd.syncml.dmtnds+wbxml":{"source":"iana"},"application/vnd.syncml.dmtnds+xml":{"source":"iana","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","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-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":"apache","extensions":["der","crt","pem"]},"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/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/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/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","compressible":true,"extensions":["n3"]},"text/parameters":{"source":"iana"},"text/parityfec":{"source":"iana"},"text/plain":{"source":"iana","compressible":true,"extensions":["txt","text","conf","def","list","log","in","ini"]},"text/provenance-notation":{"source":"iana"},"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"},"text/vnd.dmclientscript":{"source":"iana"},"text/vnd.dvb.subtitle":{"source":"iana","extensions":["sub"]},"text/vnd.esmertec.theme-descriptor":{"source":"iana"},"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","extensions":["jad"]},"text/vnd.trolltech.linguist":{"source":"iana"},"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}}; + +/***/ }), + +/***/ 514: +/***/ (function(module, __unusedexports, __webpack_require__) { + +"use strict"; + + +var compileSchema = __webpack_require__(805) + , resolve = __webpack_require__(867) + , Cache = __webpack_require__(921) + , SchemaObject = __webpack_require__(955) + , stableStringify = __webpack_require__(741) + , formats = __webpack_require__(881) + , rules = __webpack_require__(496) + , $dataMetaSchema = __webpack_require__(628) + , util = __webpack_require__(855); + +module.exports = Ajv; + +Ajv.prototype.validate = validate; +Ajv.prototype.compile = compile; +Ajv.prototype.addSchema = addSchema; +Ajv.prototype.addMetaSchema = addMetaSchema; +Ajv.prototype.validateSchema = validateSchema; +Ajv.prototype.getSchema = getSchema; +Ajv.prototype.removeSchema = removeSchema; +Ajv.prototype.addFormat = addFormat; +Ajv.prototype.errorsText = errorsText; + +Ajv.prototype._addSchema = _addSchema; +Ajv.prototype._compile = _compile; + +Ajv.prototype.compileAsync = __webpack_require__(890); +var customKeyword = __webpack_require__(45); +Ajv.prototype.addKeyword = customKeyword.add; +Ajv.prototype.getKeyword = customKeyword.get; +Ajv.prototype.removeKeyword = customKeyword.remove; +Ajv.prototype.validateKeyword = customKeyword.validate; + +var errorClasses = __webpack_require__(844); +Ajv.ValidationError = errorClasses.Validation; +Ajv.MissingRefError = errorClasses.MissingRef; +Ajv.$dataMetaSchema = $dataMetaSchema; + +var META_SCHEMA_ID = 'http://json-schema.org/draft-07/schema'; + +var META_IGNORE_OPTIONS = [ 'removeAdditional', 'useDefaults', 'coerceTypes', 'strictDefaults' ]; +var META_SUPPORT_DATA = ['/properties']; + +/** + * Creates validator instance. + * Usage: `Ajv(opts)` + * @param {Object} opts optional options + * @return {Object} ajv instance + */ +function Ajv(opts) { + if (!(this instanceof Ajv)) return new Ajv(opts); + opts = this._opts = util.copy(opts) || {}; + setLogger(this); + this._schemas = {}; + this._refs = {}; + this._fragments = {}; + this._formats = formats(opts.format); + + this._cache = opts.cache || new Cache; + this._loadingSchemas = {}; + this._compilations = []; + this.RULES = rules(); + this._getId = chooseGetId(opts); + + opts.loopRequired = opts.loopRequired || Infinity; + if (opts.errorDataPath == 'property') opts._errorDataPathProperty = true; + if (opts.serialize === undefined) opts.serialize = stableStringify; + this._metaOpts = getMetaSchemaOptions(this); + + if (opts.formats) addInitialFormats(this); + addDefaultMetaSchema(this); + if (typeof opts.meta == 'object') this.addMetaSchema(opts.meta); + if (opts.nullable) this.addKeyword('nullable', {metaSchema: {type: 'boolean'}}); + addInitialSchemas(this); +} + + + +/** + * Validate data using schema + * Schema will be compiled and cached (using serialized JSON as key. [fast-json-stable-stringify](https://github.com/epoberezkin/fast-json-stable-stringify) is used to serialize. + * @this Ajv + * @param {String|Object} schemaKeyRef key, ref or schema object + * @param {Any} data to be validated + * @return {Boolean} validation result. Errors from the last validation will be available in `ajv.errors` (and also in compiled schema: `schema.errors`). + */ +function validate(schemaKeyRef, data) { + var v; + if (typeof schemaKeyRef == 'string') { + v = this.getSchema(schemaKeyRef); + if (!v) throw new Error('no schema with key or ref "' + schemaKeyRef + '"'); + } else { + var schemaObj = this._addSchema(schemaKeyRef); + v = schemaObj.validate || this._compile(schemaObj); + } + + var valid = v(data); + if (v.$async !== true) this.errors = v.errors; + return valid; +} + + +/** + * Create validating function for passed schema. + * @this Ajv + * @param {Object} schema schema object + * @param {Boolean} _meta true if schema is a meta-schema. Used internally to compile meta schemas of custom keywords. + * @return {Function} validating function + */ +function compile(schema, _meta) { + var schemaObj = this._addSchema(schema, undefined, _meta); + return schemaObj.validate || this._compile(schemaObj); +} + + +/** + * Adds schema to the instance. + * @this Ajv + * @param {Object|Array} schema schema or array of schemas. If array is passed, `key` and other parameters will be ignored. + * @param {String} key Optional schema key. Can be passed to `validate` method instead of schema object or id/ref. One schema per instance can have empty `id` and `key`. + * @param {Boolean} _skipValidation true to skip schema validation. Used internally, option validateSchema should be used instead. + * @param {Boolean} _meta true if schema is a meta-schema. Used internally, addMetaSchema should be used instead. + * @return {Ajv} this for method chaining + */ +function addSchema(schema, key, _skipValidation, _meta) { + if (Array.isArray(schema)){ + for (var i=0; i} errors optional array of validation errors, if not passed errors from the instance are used. + * @param {Object} options optional options with properties `separator` and `dataVar`. + * @return {String} human readable string with all errors descriptions + */ +function errorsText(errors, options) { + errors = errors || this.errors; + if (!errors) return 'No errors'; + options = options || {}; + var separator = options.separator === undefined ? ', ' : options.separator; + var dataVar = options.dataVar === undefined ? 'data' : options.dataVar; + + var text = ''; + for (var i=0; i= 1, + 'key must have at least one part'); + assert.ok(partial || sshbuf.atEnd(), + 'leftover bytes at end of key'); + + var Constructor = Key; + var algInfo = algs.info[key.type]; + if (type === 'private' || algInfo.parts.length !== parts.length) { + algInfo = algs.privInfo[key.type]; + Constructor = PrivateKey; + } + assert.strictEqual(algInfo.parts.length, parts.length); + + if (key.type === 'ecdsa') { + var res = /^ecdsa-sha2-(.+)$/.exec(alg); + assert.ok(res !== null); + assert.strictEqual(res[1], parts[0].data.toString()); + } + + var normalized = true; + for (var i = 0; i < algInfo.parts.length; ++i) { + var p = parts[i]; + p.name = algInfo.parts[i]; + /* + * OpenSSH stores ed25519 "private" keys as seed + public key + * concat'd together (k followed by A). We want to keep them + * separate for other formats that don't do this. + */ + if (key.type === 'ed25519' && p.name === 'k') + p.data = p.data.slice(0, 32); + + if (p.name !== 'curve' && algInfo.normalize !== false) { + var nd; + if (key.type === 'ed25519') { + nd = utils.zeroPadToLength(p.data, 32); + } else { + nd = utils.mpNormalize(p.data); + } + if (nd.toString('binary') !== + p.data.toString('binary')) { + p.data = nd; + normalized = false; + } + } + } + + if (normalized) + key._rfc4253Cache = sshbuf.toBuffer(); + + if (partial && typeof (partial) === 'object') { + partial.remainder = sshbuf.remainder(); + partial.consumed = sshbuf._offset; + } + + return (new Constructor(key)); +} + +function write(key, options) { + assert.object(key); + + var alg = keyTypeToAlg(key); + var i; + + var algInfo = algs.info[key.type]; + if (PrivateKey.isPrivateKey(key)) + algInfo = algs.privInfo[key.type]; + var parts = algInfo.parts; + + var buf = new SSHBuffer({}); + + buf.writeString(alg); + + for (i = 0; i < parts.length; ++i) { + var data = key.part[parts[i]].data; + if (algInfo.normalize !== false) { + if (key.type === 'ed25519') + data = utils.zeroPadToLength(data, 32); + else + data = utils.mpNormalize(data); + } + if (key.type === 'ed25519' && parts[i] === 'k') + data = Buffer.concat([data, key.part.A.data]); + buf.writeBuffer(data); + } + + return (buf.toBuffer()); +} + + +/***/ }), + +/***/ 540: +/***/ (function(module) { + +module.exports = {"_from":"twitter","_id":"twitter@1.7.1","_inBundle":false,"_integrity":"sha1-B2I3jx3BwFDkj2ZqypBOJLGpYvQ=","_location":"/twitter","_phantomChildren":{},"_requested":{"type":"tag","registry":true,"raw":"twitter","name":"twitter","escapedName":"twitter","rawSpec":"","saveSpec":null,"fetchSpec":"latest"},"_requiredBy":["#USER","/"],"_resolved":"https://registry.npmjs.org/twitter/-/twitter-1.7.1.tgz","_shasum":"0762378f1dc1c050e48f666aca904e24b1a962f4","_spec":"twitter","_where":"/usr/local/google/home/hkj/Projects/firebase-admin-node/public/.github/actions/send-tweet","author":{"name":"Desmond Morris","email":"hi@desmondmorris.com"},"bugs":{"url":"https://github.com/desmondmorris/node-twitter/issues"},"bundleDependencies":false,"dependencies":{"deep-extend":"^0.5.0","request":"^2.72.0"},"deprecated":false,"description":"Twitter API client library for node.js","devDependencies":{"eslint":"^3.12.0","mocha":"^3.2.0","nock":"^9.0.2"},"homepage":"https://github.com/desmondmorris/node-twitter","keywords":["twitter","streaming","oauth"],"license":"MIT","main":"./lib/twitter","name":"twitter","repository":{"type":"git","url":"git+https://github.com/desmondmorris/node-twitter.git"},"scripts":{"lint":"eslint test/*.js lib/*.js","test":"npm run lint && mocha"},"version":"1.7.1"}; + +/***/ }), + +/***/ 542: +/***/ (function(module) { + +"use strict"; + +module.exports = function generate_pattern(it, $keyword, $ruleType) { + var out = ' '; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $data = 'data' + ($dataLvl || ''); + var $isData = it.opts.$data && $schema && $schema.$data, + $schemaValue; + if ($isData) { + out += ' var schema' + ($lvl) + ' = ' + (it.util.getData($schema.$data, $dataLvl, it.dataPathArr)) + '; '; + $schemaValue = 'schema' + $lvl; + } else { + $schemaValue = $schema; + } + var $regexp = $isData ? '(new RegExp(' + $schemaValue + '))' : it.usePattern($schema); + out += 'if ( '; + if ($isData) { + out += ' (' + ($schemaValue) + ' !== undefined && typeof ' + ($schemaValue) + ' != \'string\') || '; + } + out += ' !' + ($regexp) + '.test(' + ($data) + ') ) { '; + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('pattern') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { pattern: '; + if ($isData) { + out += '' + ($schemaValue); + } else { + out += '' + (it.util.toQuotedString($schema)); + } + out += ' } '; + if (it.opts.messages !== false) { + out += ' , message: \'should match pattern "'; + if ($isData) { + out += '\' + ' + ($schemaValue) + ' + \''; + } else { + out += '' + (it.util.escapeQuotes($schema)); + } + out += '"\' '; + } + if (it.opts.verbose) { + out += ' , schema: '; + if ($isData) { + out += 'validate.schema' + ($schemaPath); + } else { + out += '' + (it.util.toQuotedString($schema)); + } + out += ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + out += '} '; + if ($breakOnError) { + out += ' else { '; + } + return out; +} + + +/***/ }), + +/***/ 547: +/***/ (function(module, __unusedexports, __webpack_require__) { + +var util = __webpack_require__(669); +var Stream = __webpack_require__(413).Stream; +var DelayedStream = __webpack_require__(152); + +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); +}; + + +/***/ }), + +/***/ 552: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; + + +var url = __webpack_require__(835) +var isUrl = /^https?:/ + +function Redirect (request) { + this.request = request + this.followRedirect = true + this.followRedirects = true + this.followAllRedirects = false + this.followOriginalHttpMethod = false + this.allowRedirect = function () { return true } + this.maxRedirects = 10 + this.redirects = [] + this.redirectsFollowed = 0 + this.removeRefererHeader = false +} + +Redirect.prototype.onRequest = function (options) { + var self = this + + if (options.maxRedirects !== undefined) { + self.maxRedirects = options.maxRedirects + } + if (typeof options.followRedirect === 'function') { + self.allowRedirect = options.followRedirect + } + if (options.followRedirect !== undefined) { + self.followRedirects = !!options.followRedirect + } + if (options.followAllRedirects !== undefined) { + self.followAllRedirects = options.followAllRedirects + } + if (self.followRedirects || self.followAllRedirects) { + self.redirects = self.redirects || [] + } + if (options.removeRefererHeader !== undefined) { + self.removeRefererHeader = options.removeRefererHeader + } + if (options.followOriginalHttpMethod !== undefined) { + self.followOriginalHttpMethod = options.followOriginalHttpMethod + } +} + +Redirect.prototype.redirectTo = function (response) { + var self = this + var request = self.request + + var redirectTo = null + if (response.statusCode >= 300 && response.statusCode < 400 && response.caseless.has('location')) { + var location = response.caseless.get('location') + request.debug('redirect', location) + + if (self.followAllRedirects) { + redirectTo = location + } else if (self.followRedirects) { + switch (request.method) { + case 'PATCH': + case 'PUT': + case 'POST': + case 'DELETE': + // Do not follow redirects + break + default: + redirectTo = location + break + } + } + } else if (response.statusCode === 401) { + var authHeader = request._auth.onResponse(response) + if (authHeader) { + request.setHeader('authorization', authHeader) + redirectTo = request.uri + } + } + return redirectTo +} + +Redirect.prototype.onResponse = function (response) { + var self = this + var request = self.request + + var redirectTo = self.redirectTo(response) + if (!redirectTo || !self.allowRedirect.call(request, response)) { + return false + } + + request.debug('redirect to', redirectTo) + + // ignore any potential response body. it cannot possibly be useful + // to us at this point. + // response.resume should be defined, but check anyway before calling. Workaround for browserify. + if (response.resume) { + response.resume() + } + + if (self.redirectsFollowed >= self.maxRedirects) { + request.emit('error', new Error('Exceeded maxRedirects. Probably stuck in a redirect loop ' + request.uri.href)) + return false + } + self.redirectsFollowed += 1 + + if (!isUrl.test(redirectTo)) { + redirectTo = url.resolve(request.uri.href, redirectTo) + } + + var uriPrev = request.uri + request.uri = url.parse(redirectTo) + + // handle the case where we change protocol from https to http or vice versa + if (request.uri.protocol !== uriPrev.protocol) { + delete request.agent + } + + self.redirects.push({ statusCode: response.statusCode, redirectUri: redirectTo }) + + if (self.followAllRedirects && request.method !== 'HEAD' && + response.statusCode !== 401 && response.statusCode !== 307) { + request.method = self.followOriginalHttpMethod ? request.method : 'GET' + } + // request.method = 'GET' // Force all redirects to use GET || commented out fixes #215 + delete request.src + delete request.req + delete request._started + if (response.statusCode !== 401 && response.statusCode !== 307) { + // Remove parameters from the previous response, unless this is the second request + // for a server that requires digest authentication. + delete request.body + delete request._form + if (request.headers) { + request.removeHeader('host') + request.removeHeader('content-type') + request.removeHeader('content-length') + if (request.uri.hostname !== request.originalHost.split(':')[0]) { + // Remove authorization if changing hostnames (but not if just + // changing ports or protocols). This matches the behavior of curl: + // https://github.com/bagder/curl/blob/6beb0eee/lib/http.c#L710 + request.removeHeader('authorization') + } + } + } + + if (!self.removeRefererHeader) { + request.setHeader('referer', uriPrev.href) + } + + request.emit('redirect') + + request.init() + + return true +} + +exports.Redirect = Redirect + + +/***/ }), + +/***/ 554: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; + + +var caseless = __webpack_require__(254) +var uuid = __webpack_require__(826) +var helpers = __webpack_require__(810) + +var md5 = helpers.md5 +var toBase64 = helpers.toBase64 + +function Auth (request) { + // define all public properties here + this.request = request + this.hasAuth = false + this.sentAuth = false + this.bearerToken = null + this.user = null + this.pass = null +} + +Auth.prototype.basic = function (user, pass, sendImmediately) { + var self = this + if (typeof user !== 'string' || (pass !== undefined && typeof pass !== 'string')) { + self.request.emit('error', new Error('auth() received invalid user or password')) + } + self.user = user + self.pass = pass + self.hasAuth = true + var header = user + ':' + (pass || '') + if (sendImmediately || typeof sendImmediately === 'undefined') { + var authHeader = 'Basic ' + toBase64(header) + self.sentAuth = true + return authHeader + } +} + +Auth.prototype.bearer = function (bearer, sendImmediately) { + var self = this + self.bearerToken = bearer + self.hasAuth = true + if (sendImmediately || typeof sendImmediately === 'undefined') { + if (typeof bearer === 'function') { + bearer = bearer() + } + var authHeader = 'Bearer ' + (bearer || '') + self.sentAuth = true + return authHeader + } +} + +Auth.prototype.digest = function (method, path, authHeader) { + // TODO: More complete implementation of RFC 2617. + // - handle challenge.domain + // - support qop="auth-int" only + // - handle Authentication-Info (not necessarily?) + // - check challenge.stale (not necessarily?) + // - increase nc (not necessarily?) + // For reference: + // http://tools.ietf.org/html/rfc2617#section-3 + // https://github.com/bagder/curl/blob/master/lib/http_digest.c + + var self = this + + var challenge = {} + var re = /([a-z0-9_-]+)=(?:"([^"]+)"|([a-z0-9_-]+))/gi + while (true) { + var match = re.exec(authHeader) + if (!match) { + break + } + challenge[match[1]] = match[2] || match[3] + } + + /** + * RFC 2617: handle both MD5 and MD5-sess algorithms. + * + * If the algorithm directive's value is "MD5" or unspecified, then HA1 is + * HA1=MD5(username:realm:password) + * If the algorithm directive's value is "MD5-sess", then HA1 is + * HA1=MD5(MD5(username:realm:password):nonce:cnonce) + */ + var ha1Compute = function (algorithm, user, realm, pass, nonce, cnonce) { + var ha1 = md5(user + ':' + realm + ':' + pass) + if (algorithm && algorithm.toLowerCase() === 'md5-sess') { + return md5(ha1 + ':' + nonce + ':' + cnonce) + } else { + return ha1 + } + } + + var qop = /(^|,)\s*auth\s*($|,)/.test(challenge.qop) && 'auth' + var nc = qop && '00000001' + var cnonce = qop && uuid().replace(/-/g, '') + var ha1 = ha1Compute(challenge.algorithm, self.user, challenge.realm, self.pass, challenge.nonce, cnonce) + var ha2 = md5(method + ':' + path) + var digestResponse = qop + ? md5(ha1 + ':' + challenge.nonce + ':' + nc + ':' + cnonce + ':' + qop + ':' + ha2) + : md5(ha1 + ':' + challenge.nonce + ':' + ha2) + var authValues = { + username: self.user, + realm: challenge.realm, + nonce: challenge.nonce, + uri: path, + qop: qop, + response: digestResponse, + nc: nc, + cnonce: cnonce, + algorithm: challenge.algorithm, + opaque: challenge.opaque + } + + authHeader = [] + for (var k in authValues) { + if (authValues[k]) { + if (k === 'qop' || k === 'nc' || k === 'algorithm') { + authHeader.push(k + '=' + authValues[k]) + } else { + authHeader.push(k + '="' + authValues[k] + '"') + } + } + } + authHeader = 'Digest ' + authHeader.join(', ') + self.sentAuth = true + return authHeader +} + +Auth.prototype.onRequest = function (user, pass, sendImmediately, bearer) { + var self = this + var request = self.request + + var authHeader + if (bearer === undefined && user === undefined) { + self.request.emit('error', new Error('no auth mechanism defined')) + } else if (bearer !== undefined) { + authHeader = self.bearer(bearer, sendImmediately) + } else { + authHeader = self.basic(user, pass, sendImmediately) + } + if (authHeader) { + request.setHeader('authorization', authHeader) + } +} + +Auth.prototype.onResponse = function (response) { + var self = this + var request = self.request + + if (!self.hasAuth || self.sentAuth) { return null } + + var c = caseless(response.headers) + + var authHeader = c.get('www-authenticate') + var authVerb = authHeader && authHeader.split(' ')[0].toLowerCase() + request.debug('reauth', authVerb) + + switch (authVerb) { + case 'basic': + return self.basic(self.user, self.pass, true) + + case 'bearer': + return self.bearer(self.bearerToken, true) + + case 'digest': + return self.digest(request.method, request.path, authHeader) + } +} + +exports.Auth = Auth + + +/***/ }), + +/***/ 560: +/***/ (function(module) { + +"use strict"; + +module.exports = function generate__limitProperties(it, $keyword, $ruleType) { + var out = ' '; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $errorKeyword; + var $data = 'data' + ($dataLvl || ''); + var $isData = it.opts.$data && $schema && $schema.$data, + $schemaValue; + if ($isData) { + out += ' var schema' + ($lvl) + ' = ' + (it.util.getData($schema.$data, $dataLvl, it.dataPathArr)) + '; '; + $schemaValue = 'schema' + $lvl; + } else { + $schemaValue = $schema; + } + var $op = $keyword == 'maxProperties' ? '>' : '<'; + out += 'if ( '; + if ($isData) { + out += ' (' + ($schemaValue) + ' !== undefined && typeof ' + ($schemaValue) + ' != \'number\') || '; + } + out += ' Object.keys(' + ($data) + ').length ' + ($op) + ' ' + ($schemaValue) + ') { '; + var $errorKeyword = $keyword; + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ($errorKeyword || '_limitProperties') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { limit: ' + ($schemaValue) + ' } '; + if (it.opts.messages !== false) { + out += ' , message: \'should NOT have '; + if ($keyword == 'maxProperties') { + out += 'more'; + } else { + out += 'fewer'; + } + out += ' than '; + if ($isData) { + out += '\' + ' + ($schemaValue) + ' + \''; + } else { + out += '' + ($schema); + } + out += ' properties\' '; + } + if (it.opts.verbose) { + out += ' , schema: '; + if ($isData) { + out += 'validate.schema' + ($schemaPath); + } else { + out += '' + ($schema); + } + out += ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + out += '} '; + if ($breakOnError) { + out += ' else { '; + } + return out; +} + + +/***/ }), + +/***/ 566: +/***/ (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](); + } +} + + +/***/ }), + +/***/ 575: +/***/ (function(module, __unusedexports, __webpack_require__) { + +// Copyright 2015 Joyent, Inc. + +module.exports = Signature; + +var assert = __webpack_require__(477); +var Buffer = __webpack_require__(215).Buffer; +var algs = __webpack_require__(98); +var crypto = __webpack_require__(417); +var errs = __webpack_require__(753); +var utils = __webpack_require__(270); +var asn1 = __webpack_require__(62); +var SSHBuffer = __webpack_require__(940); + +var InvalidAlgorithmError = errs.InvalidAlgorithmError; +var SignatureParseError = errs.SignatureParseError; + +function Signature(opts) { + assert.object(opts, 'options'); + assert.arrayOfObject(opts.parts, 'options.parts'); + assert.string(opts.type, 'options.type'); + + var partLookup = {}; + for (var i = 0; i < opts.parts.length; ++i) { + var part = opts.parts[i]; + partLookup[part.name] = part; + } + + this.type = opts.type; + this.hashAlgorithm = opts.hashAlgo; + this.curve = opts.curve; + this.parts = opts.parts; + this.part = partLookup; +} + +Signature.prototype.toBuffer = function (format) { + if (format === undefined) + format = 'asn1'; + assert.string(format, 'format'); + + var buf; + var stype = 'ssh-' + this.type; + + switch (this.type) { + case 'rsa': + switch (this.hashAlgorithm) { + case 'sha256': + stype = 'rsa-sha2-256'; + break; + case 'sha512': + stype = 'rsa-sha2-512'; + break; + case 'sha1': + case undefined: + break; + default: + throw (new Error('SSH signature ' + + 'format does not support hash ' + + 'algorithm ' + this.hashAlgorithm)); + } + if (format === 'ssh') { + buf = new SSHBuffer({}); + buf.writeString(stype); + buf.writePart(this.part.sig); + return (buf.toBuffer()); + } else { + return (this.part.sig.data); + } + break; + + case 'ed25519': + if (format === 'ssh') { + buf = new SSHBuffer({}); + buf.writeString(stype); + buf.writePart(this.part.sig); + return (buf.toBuffer()); + } else { + return (this.part.sig.data); + } + break; + + case 'dsa': + case 'ecdsa': + var r, s; + if (format === 'asn1') { + var der = new asn1.BerWriter(); + der.startSequence(); + r = utils.mpNormalize(this.part.r.data); + s = utils.mpNormalize(this.part.s.data); + der.writeBuffer(r, asn1.Ber.Integer); + der.writeBuffer(s, asn1.Ber.Integer); + der.endSequence(); + return (der.buffer); + } else if (format === 'ssh' && this.type === 'dsa') { + buf = new SSHBuffer({}); + buf.writeString('ssh-dss'); + r = this.part.r.data; + if (r.length > 20 && r[0] === 0x00) + r = r.slice(1); + s = this.part.s.data; + if (s.length > 20 && s[0] === 0x00) + s = s.slice(1); + if ((this.hashAlgorithm && + this.hashAlgorithm !== 'sha1') || + r.length + s.length !== 40) { + throw (new Error('OpenSSH only supports ' + + 'DSA signatures with SHA1 hash')); + } + buf.writeBuffer(Buffer.concat([r, s])); + return (buf.toBuffer()); + } else if (format === 'ssh' && this.type === 'ecdsa') { + var inner = new SSHBuffer({}); + r = this.part.r.data; + inner.writeBuffer(r); + inner.writePart(this.part.s); + + buf = new SSHBuffer({}); + /* XXX: find a more proper way to do this? */ + var curve; + if (r[0] === 0x00) + r = r.slice(1); + var sz = r.length * 8; + if (sz === 256) + curve = 'nistp256'; + else if (sz === 384) + curve = 'nistp384'; + else if (sz === 528) + curve = 'nistp521'; + buf.writeString('ecdsa-sha2-' + curve); + buf.writeBuffer(inner.toBuffer()); + return (buf.toBuffer()); + } + throw (new Error('Invalid signature format')); + default: + throw (new Error('Invalid signature data')); + } +}; + +Signature.prototype.toString = function (format) { + assert.optionalString(format, 'format'); + return (this.toBuffer(format).toString('base64')); +}; + +Signature.parse = function (data, type, format) { + if (typeof (data) === 'string') + data = Buffer.from(data, 'base64'); + assert.buffer(data, 'data'); + assert.string(format, 'format'); + assert.string(type, 'type'); + + var opts = {}; + opts.type = type.toLowerCase(); + opts.parts = []; + + try { + assert.ok(data.length > 0, 'signature must not be empty'); + switch (opts.type) { + case 'rsa': + return (parseOneNum(data, type, format, opts)); + case 'ed25519': + return (parseOneNum(data, type, format, opts)); + + case 'dsa': + case 'ecdsa': + if (format === 'asn1') + return (parseDSAasn1(data, type, format, opts)); + else if (opts.type === 'dsa') + return (parseDSA(data, type, format, opts)); + else + return (parseECDSA(data, type, format, opts)); + + default: + throw (new InvalidAlgorithmError(type)); + } + + } catch (e) { + if (e instanceof InvalidAlgorithmError) + throw (e); + throw (new SignatureParseError(type, format, e)); + } +}; + +function parseOneNum(data, type, format, opts) { + if (format === 'ssh') { + try { + var buf = new SSHBuffer({buffer: data}); + var head = buf.readString(); + } catch (e) { + /* fall through */ + } + if (buf !== undefined) { + var msg = 'SSH signature does not match expected ' + + 'type (expected ' + type + ', got ' + head + ')'; + switch (head) { + case 'ssh-rsa': + assert.strictEqual(type, 'rsa', msg); + opts.hashAlgo = 'sha1'; + break; + case 'rsa-sha2-256': + assert.strictEqual(type, 'rsa', msg); + opts.hashAlgo = 'sha256'; + break; + case 'rsa-sha2-512': + assert.strictEqual(type, 'rsa', msg); + opts.hashAlgo = 'sha512'; + break; + case 'ssh-ed25519': + assert.strictEqual(type, 'ed25519', msg); + opts.hashAlgo = 'sha512'; + break; + default: + throw (new Error('Unknown SSH signature ' + + 'type: ' + head)); + } + var sig = buf.readPart(); + assert.ok(buf.atEnd(), 'extra trailing bytes'); + sig.name = 'sig'; + opts.parts.push(sig); + return (new Signature(opts)); + } + } + opts.parts.push({name: 'sig', data: data}); + return (new Signature(opts)); +} + +function parseDSAasn1(data, type, format, opts) { + var der = new asn1.BerReader(data); + der.readSequence(); + var r = der.readString(asn1.Ber.Integer, true); + var s = der.readString(asn1.Ber.Integer, true); + + opts.parts.push({name: 'r', data: utils.mpNormalize(r)}); + opts.parts.push({name: 's', data: utils.mpNormalize(s)}); + + return (new Signature(opts)); +} + +function parseDSA(data, type, format, opts) { + if (data.length != 40) { + var buf = new SSHBuffer({buffer: data}); + var d = buf.readBuffer(); + if (d.toString('ascii') === 'ssh-dss') + d = buf.readBuffer(); + assert.ok(buf.atEnd(), 'extra trailing bytes'); + assert.strictEqual(d.length, 40, 'invalid inner length'); + data = d; + } + opts.parts.push({name: 'r', data: data.slice(0, 20)}); + opts.parts.push({name: 's', data: data.slice(20, 40)}); + return (new Signature(opts)); +} + +function parseECDSA(data, type, format, opts) { + var buf = new SSHBuffer({buffer: data}); + + var r, s; + var inner = buf.readBuffer(); + var stype = inner.toString('ascii'); + if (stype.slice(0, 6) === 'ecdsa-') { + var parts = stype.split('-'); + assert.strictEqual(parts[0], 'ecdsa'); + assert.strictEqual(parts[1], 'sha2'); + opts.curve = parts[2]; + switch (opts.curve) { + case 'nistp256': + opts.hashAlgo = 'sha256'; + break; + case 'nistp384': + opts.hashAlgo = 'sha384'; + break; + case 'nistp521': + opts.hashAlgo = 'sha512'; + break; + default: + throw (new Error('Unsupported ECDSA curve: ' + + opts.curve)); + } + inner = buf.readBuffer(); + assert.ok(buf.atEnd(), 'extra trailing bytes on outer'); + buf = new SSHBuffer({buffer: inner}); + r = buf.readPart(); + } else { + r = {data: inner}; + } + + s = buf.readPart(); + assert.ok(buf.atEnd(), 'extra trailing bytes'); + + r.name = 'r'; + s.name = 's'; + + opts.parts.push(r); + opts.parts.push(s); + return (new Signature(opts)); +} + +Signature.isSignature = function (obj, ver) { + return (utils.isCompatible(obj, Signature, ver)); +}; + +/* + * API versions for Signature: + * [1,0] -- initial ver + * [2,0] -- support for rsa in full ssh format, compat with sshpk-agent + * hashAlgorithm property + * [2,1] -- first tagged version + */ +Signature.prototype._sshpkApiVersion = [2, 1]; + +Signature._oldVersionDetect = function (obj) { + assert.func(obj.toBuffer); + if (obj.hasOwnProperty('hashAlgorithm')) + return ([2, 0]); + return ([1, 0]); +}; + + +/***/ }), + +/***/ 581: +/***/ (function(module) { + +"use strict"; + + +var has = Object.prototype.hasOwnProperty; + +var hexTable = (function () { + var array = []; + for (var i = 0; i < 256; ++i) { + array.push('%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase()); + } + + return array; +}()); + +var compactQueue = function compactQueue(queue) { + var obj; + + while (queue.length) { + var item = queue.pop(); + obj = item.obj[item.prop]; + + if (Array.isArray(obj)) { + var compacted = []; + + for (var j = 0; j < obj.length; ++j) { + if (typeof obj[j] !== 'undefined') { + compacted.push(obj[j]); + } + } + + item.obj[item.prop] = compacted; + } + } + + return obj; +}; + +var arrayToObject = function arrayToObject(source, options) { + var obj = options && options.plainObjects ? Object.create(null) : {}; + for (var i = 0; i < source.length; ++i) { + if (typeof source[i] !== 'undefined') { + obj[i] = source[i]; + } + } + + return obj; +}; + +var merge = function merge(target, source, options) { + if (!source) { + return target; + } + + if (typeof source !== 'object') { + if (Array.isArray(target)) { + target.push(source); + } else if (typeof target === 'object') { + if (options.plainObjects || options.allowPrototypes || !has.call(Object.prototype, source)) { + target[source] = true; + } + } else { + return [target, source]; + } + + return target; + } + + if (typeof target !== 'object') { + return [target].concat(source); + } + + var mergeTarget = target; + if (Array.isArray(target) && !Array.isArray(source)) { + mergeTarget = arrayToObject(target, options); + } + + if (Array.isArray(target) && Array.isArray(source)) { + source.forEach(function (item, i) { + if (has.call(target, i)) { + if (target[i] && typeof target[i] === 'object') { + target[i] = merge(target[i], item, options); + } else { + target.push(item); + } + } else { + target[i] = item; + } + }); + return target; + } + + return Object.keys(source).reduce(function (acc, key) { + var value = source[key]; + + if (has.call(acc, key)) { + acc[key] = merge(acc[key], value, options); + } else { + acc[key] = value; + } + return acc; + }, mergeTarget); +}; + +var assign = function assignSingleSource(target, source) { + return Object.keys(source).reduce(function (acc, key) { + acc[key] = source[key]; + return acc; + }, target); +}; + +var decode = function (str) { + try { + return decodeURIComponent(str.replace(/\+/g, ' ')); + } catch (e) { + return str; + } +}; + +var encode = function encode(str) { + // This code was originally written by Brian White (mscdex) for the io.js core querystring library. + // It has been adapted here for stricter adherence to RFC 3986 + if (str.length === 0) { + return str; + } + + var string = typeof str === 'string' ? str : String(str); + + var out = ''; + for (var i = 0; i < string.length; ++i) { + var c = string.charCodeAt(i); + + if ( + c === 0x2D // - + || c === 0x2E // . + || c === 0x5F // _ + || c === 0x7E // ~ + || (c >= 0x30 && c <= 0x39) // 0-9 + || (c >= 0x41 && c <= 0x5A) // a-z + || (c >= 0x61 && c <= 0x7A) // A-Z + ) { + out += string.charAt(i); + continue; + } + + if (c < 0x80) { + out = out + hexTable[c]; + continue; + } + + if (c < 0x800) { + out = out + (hexTable[0xC0 | (c >> 6)] + hexTable[0x80 | (c & 0x3F)]); + continue; + } + + if (c < 0xD800 || c >= 0xE000) { + out = out + (hexTable[0xE0 | (c >> 12)] + hexTable[0x80 | ((c >> 6) & 0x3F)] + hexTable[0x80 | (c & 0x3F)]); + continue; + } + + i += 1; + c = 0x10000 + (((c & 0x3FF) << 10) | (string.charCodeAt(i) & 0x3FF)); + out += hexTable[0xF0 | (c >> 18)] + + hexTable[0x80 | ((c >> 12) & 0x3F)] + + hexTable[0x80 | ((c >> 6) & 0x3F)] + + hexTable[0x80 | (c & 0x3F)]; + } + + return out; +}; + +var compact = function compact(value) { + var queue = [{ obj: { o: value }, prop: 'o' }]; + var refs = []; + + for (var i = 0; i < queue.length; ++i) { + var item = queue[i]; + var obj = item.obj[item.prop]; + + var keys = Object.keys(obj); + for (var j = 0; j < keys.length; ++j) { + var key = keys[j]; + var val = obj[key]; + if (typeof val === 'object' && val !== null && refs.indexOf(val) === -1) { + queue.push({ obj: obj, prop: key }); + refs.push(val); + } + } + } + + return compactQueue(queue); +}; + +var isRegExp = function isRegExp(obj) { + return Object.prototype.toString.call(obj) === '[object RegExp]'; +}; + +var isBuffer = function isBuffer(obj) { + if (obj === null || typeof obj === 'undefined') { + return false; + } + + return !!(obj.constructor && obj.constructor.isBuffer && obj.constructor.isBuffer(obj)); +}; + +module.exports = { + arrayToObject: arrayToObject, + assign: assign, + compact: compact, + decode: decode, + encode: encode, + isBuffer: isBuffer, + isRegExp: isRegExp, + merge: merge +}; + + +/***/ }), + +/***/ 584: +/***/ (function(module) { + +// Copyright 2011 Mark Cavage All rights reserved. + + +module.exports = { + + newInvalidAsn1Error: function (msg) { + var e = new Error(); + e.name = 'InvalidAsn1Error'; + e.message = msg || ''; + return e; + } + +}; + + +/***/ }), + +/***/ 602: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; + + +var tough = __webpack_require__(701) + +var Cookie = tough.Cookie +var CookieJar = tough.CookieJar + +exports.parse = function (str) { + if (str && str.uri) { + str = str.uri + } + if (typeof str !== 'string') { + throw new Error('The cookie function only accepts STRING as param') + } + return Cookie.parse(str, {loose: true}) +} + +// Adapt the sometimes-Async api of tough.CookieJar to our requirements +function RequestJar (store) { + var self = this + self._jar = new CookieJar(store, {looseMode: true}) +} +RequestJar.prototype.setCookie = function (cookieOrStr, uri, options) { + var self = this + return self._jar.setCookieSync(cookieOrStr, uri, options || {}) +} +RequestJar.prototype.getCookieString = function (uri) { + var self = this + return self._jar.getCookieStringSync(uri) +} +RequestJar.prototype.getCookies = function (uri) { + var self = this + return self._jar.getCookiesSync(uri) +} + +exports.jar = function (store) { + return new RequestJar(store) +} + + +/***/ }), + +/***/ 603: +/***/ (function(module, __unusedexports, __webpack_require__) { + +// Copyright 2015 Joyent, Inc. + +module.exports = { + read: read, + write: write +}; + +var assert = __webpack_require__(477); +var Buffer = __webpack_require__(215).Buffer; +var rfc4253 = __webpack_require__(538); +var utils = __webpack_require__(270); +var Key = __webpack_require__(852); +var PrivateKey = __webpack_require__(502); + +var sshpriv = __webpack_require__(78); + +/*JSSTYLED*/ +var SSHKEY_RE = /^([a-z0-9-]+)[ \t]+([a-zA-Z0-9+\/]+[=]*)([ \t]+([^ \t][^\n]*[\n]*)?)?$/; +/*JSSTYLED*/ +var SSHKEY_RE2 = /^([a-z0-9-]+)[ \t\n]+([a-zA-Z0-9+\/][a-zA-Z0-9+\/ \t\n=]*)([^a-zA-Z0-9+\/ \t\n=].*)?$/; + +function read(buf, options) { + if (typeof (buf) !== 'string') { + assert.buffer(buf, 'buf'); + buf = buf.toString('ascii'); + } + + var trimmed = buf.trim().replace(/[\\\r]/g, ''); + var m = trimmed.match(SSHKEY_RE); + if (!m) + m = trimmed.match(SSHKEY_RE2); + assert.ok(m, 'key must match regex'); + + var type = rfc4253.algToKeyType(m[1]); + var kbuf = Buffer.from(m[2], 'base64'); + + /* + * This is a bit tricky. If we managed to parse the key and locate the + * key comment with the regex, then do a non-partial read and assert + * that we have consumed all bytes. If we couldn't locate the key + * comment, though, there may be whitespace shenanigans going on that + * have conjoined the comment to the rest of the key. We do a partial + * read in this case to try to make the best out of a sorry situation. + */ + var key; + var ret = {}; + if (m[4]) { + try { + key = rfc4253.read(kbuf); + + } catch (e) { + m = trimmed.match(SSHKEY_RE2); + assert.ok(m, 'key must match regex'); + kbuf = Buffer.from(m[2], 'base64'); + key = rfc4253.readInternal(ret, 'public', kbuf); + } + } else { + key = rfc4253.readInternal(ret, 'public', kbuf); + } + + assert.strictEqual(type, key.type); + + if (m[4] && m[4].length > 0) { + key.comment = m[4]; + + } else if (ret.consumed) { + /* + * Now the magic: trying to recover the key comment when it's + * gotten conjoined to the key or otherwise shenanigan'd. + * + * Work out how much base64 we used, then drop all non-base64 + * chars from the beginning up to this point in the the string. + * Then offset in this and try to make up for missing = chars. + */ + var data = m[2] + (m[3] ? m[3] : ''); + var realOffset = Math.ceil(ret.consumed / 3) * 4; + data = data.slice(0, realOffset - 2). /*JSSTYLED*/ + replace(/[^a-zA-Z0-9+\/=]/g, '') + + data.slice(realOffset - 2); + + var padding = ret.consumed % 3; + if (padding > 0 && + data.slice(realOffset - 1, realOffset) !== '=') + realOffset--; + while (data.slice(realOffset, realOffset + 1) === '=') + realOffset++; + + /* Finally, grab what we think is the comment & clean it up. */ + var trailer = data.slice(realOffset); + trailer = trailer.replace(/[\r\n]/g, ' '). + replace(/^\s+/, ''); + if (trailer.match(/^[a-zA-Z0-9]/)) + key.comment = trailer; + } + + return (key); +} + +function write(key, options) { + assert.object(key); + if (!Key.isKey(key)) + throw (new Error('Must be a public key')); + + var parts = []; + var alg = rfc4253.keyTypeToAlg(key); + parts.push(alg); + + var buf = rfc4253.write(key); + parts.push(buf.toString('base64')); + + if (key.comment) + parts.push(key.comment); + + return (Buffer.from(parts.join(' '))); +} + + +/***/ }), + +/***/ 605: +/***/ (function(module) { + +module.exports = require("http"); + +/***/ }), + +/***/ 614: +/***/ (function(module) { + +module.exports = require("events"); + +/***/ }), + +/***/ 616: +/***/ (function(module) { + +module.exports = {"$id":"beforeRequest.json#","$schema":"http://json-schema.org/draft-06/schema#","type":"object","optional":true,"required":["lastAccess","eTag","hitCount"],"properties":{"expires":{"type":"string","pattern":"^(\\d{4})(-)?(\\d\\d)(-)?(\\d\\d)(T)?(\\d\\d)(:)?(\\d\\d)(:)?(\\d\\d)(\\.\\d+)?(Z|([+-])(\\d\\d)(:)?(\\d\\d))?"},"lastAccess":{"type":"string","pattern":"^(\\d{4})(-)?(\\d\\d)(-)?(\\d\\d)(T)?(\\d\\d)(:)?(\\d\\d)(:)?(\\d\\d)(\\.\\d+)?(Z|([+-])(\\d\\d)(:)?(\\d\\d))?"},"eTag":{"type":"string"},"hitCount":{"type":"integer"},"comment":{"type":"string"}}}; + +/***/ }), + +/***/ 622: +/***/ (function(module) { + +module.exports = require("path"); + +/***/ }), + +/***/ 624: +/***/ (function(module, __unusedexports, __webpack_require__) { + +// Copyright 2018 Joyent, Inc. + +module.exports = { + read: read, + write: write +}; + +var assert = __webpack_require__(477); +var Buffer = __webpack_require__(215).Buffer; +var rfc4253 = __webpack_require__(538); +var Key = __webpack_require__(852); + +var errors = __webpack_require__(753); + +function read(buf, options) { + var lines = buf.toString('ascii').split(/[\r\n]+/); + var found = false; + var parts; + var si = 0; + while (si < lines.length) { + parts = splitHeader(lines[si++]); + if (parts && + parts[0].toLowerCase() === 'putty-user-key-file-2') { + found = true; + break; + } + } + if (!found) { + throw (new Error('No PuTTY format first line found')); + } + var alg = parts[1]; + + parts = splitHeader(lines[si++]); + assert.equal(parts[0].toLowerCase(), 'encryption'); + + parts = splitHeader(lines[si++]); + assert.equal(parts[0].toLowerCase(), 'comment'); + var comment = parts[1]; + + parts = splitHeader(lines[si++]); + assert.equal(parts[0].toLowerCase(), 'public-lines'); + var publicLines = parseInt(parts[1], 10); + if (!isFinite(publicLines) || publicLines < 0 || + publicLines > lines.length) { + throw (new Error('Invalid public-lines count')); + } + + var publicBuf = Buffer.from( + lines.slice(si, si + publicLines).join(''), 'base64'); + var keyType = rfc4253.algToKeyType(alg); + var key = rfc4253.read(publicBuf); + if (key.type !== keyType) { + throw (new Error('Outer key algorithm mismatch')); + } + key.comment = comment; + return (key); +} + +function splitHeader(line) { + var idx = line.indexOf(':'); + if (idx === -1) + return (null); + var header = line.slice(0, idx); + ++idx; + while (line[idx] === ' ') + ++idx; + var rest = line.slice(idx); + return ([header, rest]); +} + +function write(key, options) { + assert.object(key); + if (!Key.isKey(key)) + throw (new Error('Must be a public key')); + + var alg = rfc4253.keyTypeToAlg(key); + var buf = rfc4253.write(key); + var comment = key.comment || ''; + + var b64 = buf.toString('base64'); + var lines = wrap(b64, 64); + + lines.unshift('Public-Lines: ' + lines.length); + lines.unshift('Comment: ' + comment); + lines.unshift('Encryption: none'); + lines.unshift('PuTTY-User-Key-File-2: ' + alg); + + return (Buffer.from(lines.join('\n') + '\n')); +} + +function wrap(txt, len) { + var lines = []; + var pos = 0; + while (pos < txt.length) { + lines.push(txt.slice(pos, pos + 64)); + pos += 64; + } + return (lines); +} + + +/***/ }), + +/***/ 627: +/***/ (function(__unusedmodule, exports) { + +"use strict"; +/*! + * Copyright (c) 2015, Salesforce.com, Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of Salesforce.com nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/*jshint unused:false */ + +function Store() { +} +exports.Store = Store; + +// Stores may be synchronous, but are still required to use a +// Continuation-Passing Style API. The CookieJar itself will expose a "*Sync" +// API that converts from synchronous-callbacks to imperative style. +Store.prototype.synchronous = false; + +Store.prototype.findCookie = function(domain, path, key, cb) { + throw new Error('findCookie is not implemented'); +}; + +Store.prototype.findCookies = function(domain, path, cb) { + throw new Error('findCookies is not implemented'); +}; + +Store.prototype.putCookie = function(cookie, cb) { + throw new Error('putCookie is not implemented'); +}; + +Store.prototype.updateCookie = function(oldCookie, newCookie, cb) { + // recommended default implementation: + // return this.putCookie(newCookie, cb); + throw new Error('updateCookie is not implemented'); +}; + +Store.prototype.removeCookie = function(domain, path, key, cb) { + throw new Error('removeCookie is not implemented'); +}; + +Store.prototype.removeCookies = function(domain, path, cb) { + throw new Error('removeCookies is not implemented'); +}; + +Store.prototype.removeAllCookies = function(cb) { + throw new Error('removeAllCookies is not implemented'); +} + +Store.prototype.getAllCookies = function(cb) { + throw new Error('getAllCookies is not implemented (therefore jar cannot be serialized)'); +}; + + +/***/ }), + +/***/ 628: +/***/ (function(module) { + +"use strict"; + + +var KEYWORDS = [ + 'multipleOf', + 'maximum', + 'exclusiveMaximum', + 'minimum', + 'exclusiveMinimum', + 'maxLength', + 'minLength', + 'pattern', + 'additionalItems', + 'maxItems', + 'minItems', + 'uniqueItems', + 'maxProperties', + 'minProperties', + 'required', + 'additionalProperties', + 'enum', + 'format', + 'const' +]; + +module.exports = function (metaSchema, keywordsJsonPointers) { + for (var i=0; i + */ + +/* + * The Blowfish portions are under the following license: + * + * Blowfish block cipher for OpenBSD + * Copyright 1997 Niels Provos + * All rights reserved. + * + * Implementation advice by David Mazieres . + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * The bcrypt_pbkdf portions are under the following license: + * + * Copyright (c) 2013 Ted Unangst + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* + * Performance improvements (Javascript-specific): + * + * Copyright 2016, Joyent Inc + * Author: Alex Wilson + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +// Ported from OpenBSD bcrypt_pbkdf.c v1.9 + +var BLF_J = 0; + +var Blowfish = function() { + this.S = [ + new Uint32Array([ + 0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7, + 0xb8e1afed, 0x6a267e96, 0xba7c9045, 0xf12c7f99, + 0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16, + 0x636920d8, 0x71574e69, 0xa458fea3, 0xf4933d7e, + 0x0d95748f, 0x728eb658, 0x718bcd58, 0x82154aee, + 0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013, + 0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef, + 0x8e79dcb0, 0x603a180e, 0x6c9e0e8b, 0xb01e8a3e, + 0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60, + 0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440, + 0x55ca396a, 0x2aab10b6, 0xb4cc5c34, 0x1141e8ce, + 0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a, + 0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e, + 0xafd6ba33, 0x6c24cf5c, 0x7a325381, 0x28958677, + 0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193, + 0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032, + 0xef845d5d, 0xe98575b1, 0xdc262302, 0xeb651b88, + 0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239, + 0x2e0b4482, 0xa4842004, 0x69c8f04a, 0x9e1f9b5e, + 0x21c66842, 0xf6e96c9a, 0x670c9c61, 0xabd388f0, + 0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3, + 0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98, + 0xa1f1651d, 0x39af0176, 0x66ca593e, 0x82430e88, + 0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe, + 0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6, + 0x4ed3aa62, 0x363f7706, 0x1bfedf72, 0x429b023d, + 0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b, + 0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7, + 0xe3fe501a, 0xb6794c3b, 0x976ce0bd, 0x04c006ba, + 0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463, + 0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f, + 0x6dfc511f, 0x9b30952c, 0xcc814544, 0xaf5ebd09, + 0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3, + 0xc0cba857, 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb, + 0x5579c0bd, 0x1a60320a, 0xd6a100c6, 0x402c7279, + 0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8, + 0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab, + 0x323db5fa, 0xfd238760, 0x53317b48, 0x3e00df82, + 0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db, + 0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573, + 0x695b27b0, 0xbbca58c8, 0xe1ffa35d, 0xb8f011a0, + 0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b, + 0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790, + 0xe1ddf2da, 0xa4cb7e33, 0x62fb1341, 0xcee4c6e8, + 0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4, + 0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0, + 0xd08ed1d0, 0xafc725e0, 0x8e3c5b2f, 0x8e7594b7, + 0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c, + 0x4fad5ea0, 0x688fc31c, 0xd1cff191, 0xb3a8c1ad, + 0x2f2f2218, 0xbe0e1777, 0xea752dfe, 0x8b021fa1, + 0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299, + 0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9, + 0x165fa266, 0x80957705, 0x93cc7314, 0x211a1477, + 0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf, + 0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49, + 0x00250e2d, 0x2071b35e, 0x226800bb, 0x57b8e0af, + 0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa, + 0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5, + 0x83260376, 0x6295cfa9, 0x11c81968, 0x4e734a41, + 0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915, + 0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400, + 0x08ba6fb5, 0x571be91f, 0xf296ec6b, 0x2a0dd915, + 0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664, + 0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 0x6e85076a]), + new Uint32Array([ + 0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623, + 0xad6ea6b0, 0x49a7df7d, 0x9cee60b8, 0x8fedb266, + 0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1, + 0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e, + 0x3f54989a, 0x5b429d65, 0x6b8fe4d6, 0x99f73fd6, + 0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1, + 0x4cdd2086, 0x8470eb26, 0x6382e9c6, 0x021ecc5e, + 0x09686b3f, 0x3ebaefc9, 0x3c971814, 0x6b6a70a1, + 0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737, + 0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8, + 0xb03ada37, 0xf0500c0d, 0xf01c1f04, 0x0200b3ff, + 0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd, + 0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701, + 0x3ae5e581, 0x37c2dadc, 0xc8b57634, 0x9af3dda7, + 0xa9446146, 0x0fd0030e, 0xecc8c73e, 0xa4751e41, + 0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331, + 0x4e548b38, 0x4f6db908, 0x6f420d03, 0xf60a04bf, + 0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af, + 0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e, + 0x5512721f, 0x2e6b7124, 0x501adde6, 0x9f84cd87, + 0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c, + 0xec7aec3a, 0xdb851dfa, 0x63094366, 0xc464c3d2, + 0xef1c1847, 0x3215d908, 0xdd433b37, 0x24c2ba16, + 0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd, + 0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b, + 0x043556f1, 0xd7a3c76b, 0x3c11183b, 0x5924a509, + 0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e, + 0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3, + 0x771fe71c, 0x4e3d06fa, 0x2965dcb9, 0x99e71d0f, + 0x803e89d6, 0x5266c825, 0x2e4cc978, 0x9c10b36a, + 0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4, + 0xf2f74ea7, 0x361d2b3d, 0x1939260f, 0x19c27960, + 0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66, + 0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28, + 0xc332ddef, 0xbe6c5aa5, 0x65582185, 0x68ab9802, + 0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84, + 0x1521b628, 0x29076170, 0xecdd4775, 0x619f1510, + 0x13cca830, 0xeb61bd96, 0x0334fe1e, 0xaa0363cf, + 0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14, + 0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e, + 0x648b1eaf, 0x19bdf0ca, 0xa02369b9, 0x655abb50, + 0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7, + 0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8, + 0xf837889a, 0x97e32d77, 0x11ed935f, 0x16681281, + 0x0e358829, 0xc7e61fd6, 0x96dedfa1, 0x7858ba99, + 0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696, + 0xcdb30aeb, 0x532e3054, 0x8fd948e4, 0x6dbc3128, + 0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73, + 0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0, + 0x45eee2b6, 0xa3aaabea, 0xdb6c4f15, 0xfacb4fd0, + 0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105, + 0xd81e799e, 0x86854dc7, 0xe44b476a, 0x3d816250, + 0xcf62a1f2, 0x5b8d2646, 0xfc8883a0, 0xc1c7b6a3, + 0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285, + 0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00, + 0x58428d2a, 0x0c55f5ea, 0x1dadf43e, 0x233f7061, + 0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb, + 0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e, + 0xa6078084, 0x19f8509e, 0xe8efd855, 0x61d99735, + 0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 0x800bcadc, + 0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9, + 0xdb73dbd3, 0x105588cd, 0x675fda79, 0xe3674340, + 0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20, + 0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7]), + new Uint32Array([ + 0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934, + 0x411520f7, 0x7602d4f7, 0xbcf46b2e, 0xd4a20068, + 0xd4082471, 0x3320f46a, 0x43b7d4b7, 0x500061af, + 0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840, + 0x4d95fc1d, 0x96b591af, 0x70f4ddd3, 0x66a02f45, + 0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504, + 0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a, + 0x28507825, 0x530429f4, 0x0a2c86da, 0xe9b66dfb, + 0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee, + 0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6, + 0xaace1e7c, 0xd3375fec, 0xce78a399, 0x406b2a42, + 0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b, + 0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2, + 0x3a6efa74, 0xdd5b4332, 0x6841e7f7, 0xca7820fb, + 0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527, + 0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b, + 0x55a867bc, 0xa1159a58, 0xcca92963, 0x99e1db33, + 0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, 0x9029317c, + 0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3, + 0x95c11548, 0xe4c66d22, 0x48c1133f, 0xc70f86dc, + 0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17, + 0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564, + 0x257b7834, 0x602a9c60, 0xdff8e8a3, 0x1f636c1b, + 0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115, + 0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922, + 0x85b2a20e, 0xe6ba0d99, 0xde720c8c, 0x2da2f728, + 0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0, + 0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e, + 0x0a476341, 0x992eff74, 0x3a6f6eab, 0xf4f8fd37, + 0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d, + 0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804, + 0xf1290dc7, 0xcc00ffa3, 0xb5390f92, 0x690fed0b, + 0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, 0xd9155ea3, + 0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb, + 0x37392eb3, 0xcc115979, 0x8026e297, 0xf42e312d, + 0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c, + 0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350, + 0x1a6b1018, 0x11caedfa, 0x3d25bdd8, 0xe2e1c3c9, + 0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a, + 0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe, + 0x9dbc8057, 0xf0f7c086, 0x60787bf8, 0x6003604d, + 0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc, + 0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f, + 0x77a057be, 0xbde8ae24, 0x55464299, 0xbf582e61, + 0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2, + 0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9, + 0x7aeb2661, 0x8b1ddf84, 0x846a0e79, 0x915f95e2, + 0x466e598e, 0x20b45770, 0x8cd55591, 0xc902de4c, + 0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e, + 0xb77f19b6, 0xe0a9dc09, 0x662d09a1, 0xc4324633, + 0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10, + 0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169, + 0xdcb7da83, 0x573906fe, 0xa1e2ce9b, 0x4fcd7f52, + 0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027, + 0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5, + 0xf0177a28, 0xc0f586e0, 0x006058aa, 0x30dc7d62, + 0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634, + 0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76, + 0x6f05e409, 0x4b7c0188, 0x39720a3d, 0x7c927c24, + 0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc, + 0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4, + 0x1e50ef5e, 0xb161e6f8, 0xa28514d9, 0x6c51133c, + 0x6fd5c7e7, 0x56e14ec4, 0x362abfce, 0xddc6c837, + 0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0]), + new Uint32Array([ + 0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b, + 0x5cb0679e, 0x4fa33742, 0xd3822740, 0x99bc9bbe, + 0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b, + 0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4, + 0x5748ab2f, 0xbc946e79, 0xc6a376d2, 0x6549c2c8, + 0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6, + 0x2939bbdb, 0xa9ba4650, 0xac9526e8, 0xbe5ee304, + 0xa1fad5f0, 0x6a2d519a, 0x63ef8ce2, 0x9a86ee22, + 0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4, + 0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6, + 0x2826a2f9, 0xa73a3ae1, 0x4ba99586, 0xef5562e9, + 0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59, + 0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593, + 0xe990fd5a, 0x9e34d797, 0x2cf0b7d9, 0x022b8b51, + 0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28, + 0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c, + 0xe029ac71, 0xe019a5e6, 0x47b0acfd, 0xed93fa9b, + 0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28, + 0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c, + 0x15056dd4, 0x88f46dba, 0x03a16125, 0x0564f0bd, + 0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a, + 0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 0x26dcf319, + 0x7533d928, 0xb155fdf5, 0x03563482, 0x8aba3cbb, + 0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f, + 0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991, + 0xea7a90c2, 0xfb3e7bce, 0x5121ce64, 0x774fbe32, + 0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680, + 0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166, + 0xb39a460a, 0x6445c0dd, 0x586cdecf, 0x1c20c8ae, + 0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb, + 0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5, + 0x72eacea8, 0xfa6484bb, 0x8d6612ae, 0xbf3c6f47, + 0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370, + 0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d, + 0x4040cb08, 0x4eb4e2cc, 0x34d2466a, 0x0115af84, + 0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048, + 0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 0x277227f8, + 0x611560b1, 0xe7933fdc, 0xbb3a792b, 0x344525bd, + 0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9, + 0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7, + 0x1a908749, 0xd44fbd9a, 0xd0dadecb, 0xd50ada38, + 0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f, + 0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c, + 0xbf97222c, 0x15e6fc2a, 0x0f91fc71, 0x9b941525, + 0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1, + 0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442, + 0xe0ec6e0e, 0x1698db3b, 0x4c98a0be, 0x3278e964, + 0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e, + 0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8, + 0xdf359f8d, 0x9b992f2e, 0xe60b6f47, 0x0fe3f11d, + 0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f, + 0x1618b166, 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299, + 0xf523f357, 0xa6327623, 0x93a83531, 0x56cccd02, + 0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc, + 0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614, + 0xe6c6c7bd, 0x327a140a, 0x45e1d006, 0xc3f27b9a, + 0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6, + 0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b, + 0x53113ec0, 0x1640e3d3, 0x38abbd60, 0x2547adf0, + 0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060, + 0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e, + 0x1948c25c, 0x02fb8a8c, 0x01c36ae4, 0xd6ebe1f9, + 0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f, + 0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6]) + ]; + this.P = new Uint32Array([ + 0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344, + 0xa4093822, 0x299f31d0, 0x082efa98, 0xec4e6c89, + 0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c, + 0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917, + 0x9216d5d9, 0x8979fb1b]); +}; + +function F(S, x8, i) { + return (((S[0][x8[i+3]] + + S[1][x8[i+2]]) ^ + S[2][x8[i+1]]) + + S[3][x8[i]]); +}; + +Blowfish.prototype.encipher = function(x, x8) { + if (x8 === undefined) { + x8 = new Uint8Array(x.buffer); + if (x.byteOffset !== 0) + x8 = x8.subarray(x.byteOffset); + } + x[0] ^= this.P[0]; + for (var i = 1; i < 16; i += 2) { + x[1] ^= F(this.S, x8, 0) ^ this.P[i]; + x[0] ^= F(this.S, x8, 4) ^ this.P[i+1]; + } + var t = x[0]; + x[0] = x[1] ^ this.P[17]; + x[1] = t; +}; + +Blowfish.prototype.decipher = function(x) { + var x8 = new Uint8Array(x.buffer); + if (x.byteOffset !== 0) + x8 = x8.subarray(x.byteOffset); + x[0] ^= this.P[17]; + for (var i = 16; i > 0; i -= 2) { + x[1] ^= F(this.S, x8, 0) ^ this.P[i]; + x[0] ^= F(this.S, x8, 4) ^ this.P[i-1]; + } + var t = x[0]; + x[0] = x[1] ^ this.P[0]; + x[1] = t; +}; + +function stream2word(data, databytes){ + var i, temp = 0; + for (i = 0; i < 4; i++, BLF_J++) { + if (BLF_J >= databytes) BLF_J = 0; + temp = (temp << 8) | data[BLF_J]; + } + return temp; +}; + +Blowfish.prototype.expand0state = function(key, keybytes) { + var d = new Uint32Array(2), i, k; + var d8 = new Uint8Array(d.buffer); + + for (i = 0, BLF_J = 0; i < 18; i++) { + this.P[i] ^= stream2word(key, keybytes); + } + BLF_J = 0; + + for (i = 0; i < 18; i += 2) { + this.encipher(d, d8); + this.P[i] = d[0]; + this.P[i+1] = d[1]; + } + + for (i = 0; i < 4; i++) { + for (k = 0; k < 256; k += 2) { + this.encipher(d, d8); + this.S[i][k] = d[0]; + this.S[i][k+1] = d[1]; + } + } +}; + +Blowfish.prototype.expandstate = function(data, databytes, key, keybytes) { + var d = new Uint32Array(2), i, k; + + for (i = 0, BLF_J = 0; i < 18; i++) { + this.P[i] ^= stream2word(key, keybytes); + } + + for (i = 0, BLF_J = 0; i < 18; i += 2) { + d[0] ^= stream2word(data, databytes); + d[1] ^= stream2word(data, databytes); + this.encipher(d); + this.P[i] = d[0]; + this.P[i+1] = d[1]; + } + + for (i = 0; i < 4; i++) { + for (k = 0; k < 256; k += 2) { + d[0] ^= stream2word(data, databytes); + d[1] ^= stream2word(data, databytes); + this.encipher(d); + this.S[i][k] = d[0]; + this.S[i][k+1] = d[1]; + } + } + BLF_J = 0; +}; + +Blowfish.prototype.enc = function(data, blocks) { + for (var i = 0; i < blocks; i++) { + this.encipher(data.subarray(i*2)); + } +}; + +Blowfish.prototype.dec = function(data, blocks) { + for (var i = 0; i < blocks; i++) { + this.decipher(data.subarray(i*2)); + } +}; + +var BCRYPT_BLOCKS = 8, + BCRYPT_HASHSIZE = 32; + +function bcrypt_hash(sha2pass, sha2salt, out) { + var state = new Blowfish(), + cdata = new Uint32Array(BCRYPT_BLOCKS), i, + ciphertext = new Uint8Array([79,120,121,99,104,114,111,109,97,116,105, + 99,66,108,111,119,102,105,115,104,83,119,97,116,68,121,110,97,109, + 105,116,101]); //"OxychromaticBlowfishSwatDynamite" + + state.expandstate(sha2salt, 64, sha2pass, 64); + for (i = 0; i < 64; i++) { + state.expand0state(sha2salt, 64); + state.expand0state(sha2pass, 64); + } + + for (i = 0; i < BCRYPT_BLOCKS; i++) + cdata[i] = stream2word(ciphertext, ciphertext.byteLength); + for (i = 0; i < 64; i++) + state.enc(cdata, cdata.byteLength / 8); + + for (i = 0; i < BCRYPT_BLOCKS; i++) { + out[4*i+3] = cdata[i] >>> 24; + out[4*i+2] = cdata[i] >>> 16; + out[4*i+1] = cdata[i] >>> 8; + out[4*i+0] = cdata[i]; + } +}; + +function bcrypt_pbkdf(pass, passlen, salt, saltlen, key, keylen, rounds) { + var sha2pass = new Uint8Array(64), + sha2salt = new Uint8Array(64), + out = new Uint8Array(BCRYPT_HASHSIZE), + tmpout = new Uint8Array(BCRYPT_HASHSIZE), + countsalt = new Uint8Array(saltlen+4), + i, j, amt, stride, dest, count, + origkeylen = keylen; + + if (rounds < 1) + return -1; + if (passlen === 0 || saltlen === 0 || keylen === 0 || + keylen > (out.byteLength * out.byteLength) || saltlen > (1<<20)) + return -1; + + stride = Math.floor((keylen + out.byteLength - 1) / out.byteLength); + amt = Math.floor((keylen + stride - 1) / stride); + + for (i = 0; i < saltlen; i++) + countsalt[i] = salt[i]; + + crypto_hash_sha512(sha2pass, pass, passlen); + + for (count = 1; keylen > 0; count++) { + countsalt[saltlen+0] = count >>> 24; + countsalt[saltlen+1] = count >>> 16; + countsalt[saltlen+2] = count >>> 8; + countsalt[saltlen+3] = count; + + crypto_hash_sha512(sha2salt, countsalt, saltlen + 4); + bcrypt_hash(sha2pass, sha2salt, tmpout); + for (i = out.byteLength; i--;) + out[i] = tmpout[i]; + + for (i = 1; i < rounds; i++) { + crypto_hash_sha512(sha2salt, tmpout, tmpout.byteLength); + bcrypt_hash(sha2pass, sha2salt, tmpout); + for (j = 0; j < out.byteLength; j++) + out[j] ^= tmpout[j]; + } + + amt = Math.min(amt, keylen); + for (i = 0; i < amt; i++) { + dest = i * stride + (count - 1); + if (dest >= origkeylen) + break; + key[dest] = out[i]; + } + keylen -= i; + } + + return 0; +}; + +module.exports = { + BLOCKS: BCRYPT_BLOCKS, + HASHSIZE: BCRYPT_HASHSIZE, + hash: bcrypt_hash, + pbkdf: bcrypt_pbkdf +}; + + +/***/ }), + +/***/ 643: +/***/ (function(module) { + +"use strict"; + +module.exports = function generate_items(it, $keyword, $ruleType) { + var out = ' '; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $data = 'data' + ($dataLvl || ''); + var $valid = 'valid' + $lvl; + var $errs = 'errs__' + $lvl; + var $it = it.util.copy(it); + var $closingBraces = ''; + $it.level++; + var $nextValid = 'valid' + $it.level; + var $idx = 'i' + $lvl, + $dataNxt = $it.dataLevel = it.dataLevel + 1, + $nextData = 'data' + $dataNxt, + $currentBaseId = it.baseId; + out += 'var ' + ($errs) + ' = errors;var ' + ($valid) + ';'; + if (Array.isArray($schema)) { + var $additionalItems = it.schema.additionalItems; + if ($additionalItems === false) { + out += ' ' + ($valid) + ' = ' + ($data) + '.length <= ' + ($schema.length) + '; '; + var $currErrSchemaPath = $errSchemaPath; + $errSchemaPath = it.errSchemaPath + '/additionalItems'; + out += ' if (!' + ($valid) + ') { '; + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('additionalItems') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { limit: ' + ($schema.length) + ' } '; + if (it.opts.messages !== false) { + out += ' , message: \'should NOT have more than ' + ($schema.length) + ' items\' '; + } + if (it.opts.verbose) { + out += ' , schema: false , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + out += ' } '; + $errSchemaPath = $currErrSchemaPath; + if ($breakOnError) { + $closingBraces += '}'; + out += ' else { '; + } + } + var arr1 = $schema; + if (arr1) { + var $sch, $i = -1, + l1 = arr1.length - 1; + while ($i < l1) { + $sch = arr1[$i += 1]; + if ((it.opts.strictKeywords ? typeof $sch == 'object' && Object.keys($sch).length > 0 : it.util.schemaHasRules($sch, it.RULES.all))) { + out += ' ' + ($nextValid) + ' = true; if (' + ($data) + '.length > ' + ($i) + ') { '; + var $passData = $data + '[' + $i + ']'; + $it.schema = $sch; + $it.schemaPath = $schemaPath + '[' + $i + ']'; + $it.errSchemaPath = $errSchemaPath + '/' + $i; + $it.errorPath = it.util.getPathExpr(it.errorPath, $i, it.opts.jsonPointers, true); + $it.dataPathArr[$dataNxt] = $i; + var $code = it.validate($it); + $it.baseId = $currentBaseId; + if (it.util.varOccurences($code, $nextData) < 2) { + out += ' ' + (it.util.varReplace($code, $nextData, $passData)) + ' '; + } else { + out += ' var ' + ($nextData) + ' = ' + ($passData) + '; ' + ($code) + ' '; + } + out += ' } '; + if ($breakOnError) { + out += ' if (' + ($nextValid) + ') { '; + $closingBraces += '}'; + } + } + } + } + if (typeof $additionalItems == 'object' && (it.opts.strictKeywords ? typeof $additionalItems == 'object' && Object.keys($additionalItems).length > 0 : it.util.schemaHasRules($additionalItems, it.RULES.all))) { + $it.schema = $additionalItems; + $it.schemaPath = it.schemaPath + '.additionalItems'; + $it.errSchemaPath = it.errSchemaPath + '/additionalItems'; + out += ' ' + ($nextValid) + ' = true; if (' + ($data) + '.length > ' + ($schema.length) + ') { for (var ' + ($idx) + ' = ' + ($schema.length) + '; ' + ($idx) + ' < ' + ($data) + '.length; ' + ($idx) + '++) { '; + $it.errorPath = it.util.getPathExpr(it.errorPath, $idx, it.opts.jsonPointers, true); + var $passData = $data + '[' + $idx + ']'; + $it.dataPathArr[$dataNxt] = $idx; + var $code = it.validate($it); + $it.baseId = $currentBaseId; + if (it.util.varOccurences($code, $nextData) < 2) { + out += ' ' + (it.util.varReplace($code, $nextData, $passData)) + ' '; + } else { + out += ' var ' + ($nextData) + ' = ' + ($passData) + '; ' + ($code) + ' '; + } + if ($breakOnError) { + out += ' if (!' + ($nextValid) + ') break; '; + } + out += ' } } '; + if ($breakOnError) { + out += ' if (' + ($nextValid) + ') { '; + $closingBraces += '}'; + } + } + } else if ((it.opts.strictKeywords ? typeof $schema == 'object' && Object.keys($schema).length > 0 : it.util.schemaHasRules($schema, it.RULES.all))) { + $it.schema = $schema; + $it.schemaPath = $schemaPath; + $it.errSchemaPath = $errSchemaPath; + out += ' for (var ' + ($idx) + ' = ' + (0) + '; ' + ($idx) + ' < ' + ($data) + '.length; ' + ($idx) + '++) { '; + $it.errorPath = it.util.getPathExpr(it.errorPath, $idx, it.opts.jsonPointers, true); + var $passData = $data + '[' + $idx + ']'; + $it.dataPathArr[$dataNxt] = $idx; + var $code = it.validate($it); + $it.baseId = $currentBaseId; + if (it.util.varOccurences($code, $nextData) < 2) { + out += ' ' + (it.util.varReplace($code, $nextData, $passData)) + ' '; + } else { + out += ' var ' + ($nextData) + ' = ' + ($passData) + '; ' + ($code) + ' '; + } + if ($breakOnError) { + out += ' if (!' + ($nextValid) + ') break; '; + } + out += ' }'; + } + if ($breakOnError) { + out += ' ' + ($closingBraces) + ' if (' + ($errs) + ' == errors) {'; + } + out = it.util.cleanUpCode(out); + return out; +} + + +/***/ }), + +/***/ 650: +/***/ (function(module, __unusedexports, __webpack_require__) { + +// Copyright 2015 Joyent, Inc. + +var Key = __webpack_require__(852); +var Fingerprint = __webpack_require__(400); +var Signature = __webpack_require__(575); +var PrivateKey = __webpack_require__(502); +var Certificate = __webpack_require__(752); +var Identity = __webpack_require__(378); +var errs = __webpack_require__(753); + +module.exports = { + /* top-level classes */ + Key: Key, + parseKey: Key.parse, + Fingerprint: Fingerprint, + parseFingerprint: Fingerprint.parse, + Signature: Signature, + parseSignature: Signature.parse, + PrivateKey: PrivateKey, + parsePrivateKey: PrivateKey.parse, + generatePrivateKey: PrivateKey.generate, + Certificate: Certificate, + parseCertificate: Certificate.parse, + createSelfSignedCertificate: Certificate.createSelfSigned, + createCertificate: Certificate.create, + Identity: Identity, + identityFromDN: Identity.parseDN, + identityForHost: Identity.forHost, + identityForUser: Identity.forUser, + identityForEmail: Identity.forEmail, + identityFromArray: Identity.fromArray, + + /* errors */ + FingerprintFormatError: errs.FingerprintFormatError, + InvalidAlgorithmError: errs.InvalidAlgorithmError, + KeyParseError: errs.KeyParseError, + SignatureParseError: errs.SignatureParseError, + KeyEncryptedError: errs.KeyEncryptedError, + CertificateParseError: errs.CertificateParseError +}; + + +/***/ }), + +/***/ 653: +/***/ (function(module) { + +"use strict"; + +module.exports = function generate_oneOf(it, $keyword, $ruleType) { + var out = ' '; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $data = 'data' + ($dataLvl || ''); + var $valid = 'valid' + $lvl; + var $errs = 'errs__' + $lvl; + var $it = it.util.copy(it); + var $closingBraces = ''; + $it.level++; + var $nextValid = 'valid' + $it.level; + var $currentBaseId = $it.baseId, + $prevValid = 'prevValid' + $lvl, + $passingSchemas = 'passingSchemas' + $lvl; + out += 'var ' + ($errs) + ' = errors , ' + ($prevValid) + ' = false , ' + ($valid) + ' = false , ' + ($passingSchemas) + ' = null; '; + var $wasComposite = it.compositeRule; + it.compositeRule = $it.compositeRule = true; + var arr1 = $schema; + if (arr1) { + var $sch, $i = -1, + l1 = arr1.length - 1; + while ($i < l1) { + $sch = arr1[$i += 1]; + if ((it.opts.strictKeywords ? typeof $sch == 'object' && Object.keys($sch).length > 0 : it.util.schemaHasRules($sch, it.RULES.all))) { + $it.schema = $sch; + $it.schemaPath = $schemaPath + '[' + $i + ']'; + $it.errSchemaPath = $errSchemaPath + '/' + $i; + out += ' ' + (it.validate($it)) + ' '; + $it.baseId = $currentBaseId; + } else { + out += ' var ' + ($nextValid) + ' = true; '; + } + if ($i) { + out += ' if (' + ($nextValid) + ' && ' + ($prevValid) + ') { ' + ($valid) + ' = false; ' + ($passingSchemas) + ' = [' + ($passingSchemas) + ', ' + ($i) + ']; } else { '; + $closingBraces += '}'; + } + out += ' if (' + ($nextValid) + ') { ' + ($valid) + ' = ' + ($prevValid) + ' = true; ' + ($passingSchemas) + ' = ' + ($i) + '; }'; + } + } + it.compositeRule = $it.compositeRule = $wasComposite; + out += '' + ($closingBraces) + 'if (!' + ($valid) + ') { var err = '; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('oneOf') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { passingSchemas: ' + ($passingSchemas) + ' } '; + if (it.opts.messages !== false) { + out += ' , message: \'should match exactly one schema in oneOf\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + out += '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError(vErrors); '; + } else { + out += ' validate.errors = vErrors; return false; '; + } + } + out += '} else { errors = ' + ($errs) + '; if (vErrors !== null) { if (' + ($errs) + ') vErrors.length = ' + ($errs) + '; else vErrors = null; }'; + if (it.opts.allErrors) { + out += ' } '; + } + return out; +} + + +/***/ }), + +/***/ 658: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +var aws4 = exports, + url = __webpack_require__(835), + querystring = __webpack_require__(191), + crypto = __webpack_require__(417), + lru = __webpack_require__(985), + credentialsCache = lru(1000) + +// http://docs.amazonwebservices.com/general/latest/gr/signature-version-4.html + +function hmac(key, string, encoding) { + return crypto.createHmac('sha256', key).update(string, 'utf8').digest(encoding) +} + +function hash(string, encoding) { + return crypto.createHash('sha256').update(string, 'utf8').digest(encoding) +} + +// This function assumes the string has already been percent encoded +function encodeRfc3986(urlEncodedString) { + return urlEncodedString.replace(/[!'()*]/g, function(c) { + return '%' + c.charCodeAt(0).toString(16).toUpperCase() + }) +} + +function encodeRfc3986Full(str) { + return encodeRfc3986(encodeURIComponent(str)) +} + +// request: { path | body, [host], [method], [headers], [service], [region] } +// credentials: { accessKeyId, secretAccessKey, [sessionToken] } +function RequestSigner(request, credentials) { + + if (typeof request === 'string') request = url.parse(request) + + var headers = request.headers = (request.headers || {}), + hostParts = this.matchHost(request.hostname || request.host || headers.Host || headers.host) + + this.request = request + this.credentials = credentials || this.defaultCredentials() + + this.service = request.service || hostParts[0] || '' + this.region = request.region || hostParts[1] || 'us-east-1' + + // SES uses a different domain from the service name + if (this.service === 'email') this.service = 'ses' + + if (!request.method && request.body) + request.method = 'POST' + + if (!headers.Host && !headers.host) { + headers.Host = request.hostname || request.host || this.createHost() + + // If a port is specified explicitly, use it as is + if (request.port) + headers.Host += ':' + request.port + } + if (!request.hostname && !request.host) + request.hostname = headers.Host || headers.host + + this.isCodeCommitGit = this.service === 'codecommit' && request.method === 'GIT' +} + +RequestSigner.prototype.matchHost = function(host) { + var match = (host || '').match(/([^\.]+)\.(?:([^\.]*)\.)?amazonaws\.com(\.cn)?$/) + var hostParts = (match || []).slice(1, 3) + + // ES's hostParts are sometimes the other way round, if the value that is expected + // to be region equals ‘es’ switch them back + // e.g. search-cluster-name-aaaa00aaaa0aaa0aaaaaaa0aaa.us-east-1.es.amazonaws.com + if (hostParts[1] === 'es') + hostParts = hostParts.reverse() + + return hostParts +} + +// http://docs.aws.amazon.com/general/latest/gr/rande.html +RequestSigner.prototype.isSingleRegion = function() { + // Special case for S3 and SimpleDB in us-east-1 + if (['s3', 'sdb'].indexOf(this.service) >= 0 && this.region === 'us-east-1') return true + + return ['cloudfront', 'ls', 'route53', 'iam', 'importexport', 'sts'] + .indexOf(this.service) >= 0 +} + +RequestSigner.prototype.createHost = function() { + var region = this.isSingleRegion() ? '' : + (this.service === 's3' && this.region !== 'us-east-1' ? '-' : '.') + this.region, + service = this.service === 'ses' ? 'email' : this.service + return service + region + '.amazonaws.com' +} + +RequestSigner.prototype.prepareRequest = function() { + this.parsePath() + + var request = this.request, headers = request.headers, query + + if (request.signQuery) { + + this.parsedPath.query = query = this.parsedPath.query || {} + + if (this.credentials.sessionToken) + query['X-Amz-Security-Token'] = this.credentials.sessionToken + + if (this.service === 's3' && !query['X-Amz-Expires']) + query['X-Amz-Expires'] = 86400 + + if (query['X-Amz-Date']) + this.datetime = query['X-Amz-Date'] + else + query['X-Amz-Date'] = this.getDateTime() + + query['X-Amz-Algorithm'] = 'AWS4-HMAC-SHA256' + query['X-Amz-Credential'] = this.credentials.accessKeyId + '/' + this.credentialString() + query['X-Amz-SignedHeaders'] = this.signedHeaders() + + } else { + + if (!request.doNotModifyHeaders && !this.isCodeCommitGit) { + if (request.body && !headers['Content-Type'] && !headers['content-type']) + headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8' + + if (request.body && !headers['Content-Length'] && !headers['content-length']) + headers['Content-Length'] = Buffer.byteLength(request.body) + + if (this.credentials.sessionToken && !headers['X-Amz-Security-Token'] && !headers['x-amz-security-token']) + headers['X-Amz-Security-Token'] = this.credentials.sessionToken + + if (this.service === 's3' && !headers['X-Amz-Content-Sha256'] && !headers['x-amz-content-sha256']) + headers['X-Amz-Content-Sha256'] = hash(this.request.body || '', 'hex') + + if (headers['X-Amz-Date'] || headers['x-amz-date']) + this.datetime = headers['X-Amz-Date'] || headers['x-amz-date'] + else + headers['X-Amz-Date'] = this.getDateTime() + } + + delete headers.Authorization + delete headers.authorization + } +} + +RequestSigner.prototype.sign = function() { + if (!this.parsedPath) this.prepareRequest() + + if (this.request.signQuery) { + this.parsedPath.query['X-Amz-Signature'] = this.signature() + } else { + this.request.headers.Authorization = this.authHeader() + } + + this.request.path = this.formatPath() + + return this.request +} + +RequestSigner.prototype.getDateTime = function() { + if (!this.datetime) { + var headers = this.request.headers, + date = new Date(headers.Date || headers.date || new Date) + + this.datetime = date.toISOString().replace(/[:\-]|\.\d{3}/g, '') + + // Remove the trailing 'Z' on the timestamp string for CodeCommit git access + if (this.isCodeCommitGit) this.datetime = this.datetime.slice(0, -1) + } + return this.datetime +} + +RequestSigner.prototype.getDate = function() { + return this.getDateTime().substr(0, 8) +} + +RequestSigner.prototype.authHeader = function() { + return [ + 'AWS4-HMAC-SHA256 Credential=' + this.credentials.accessKeyId + '/' + this.credentialString(), + 'SignedHeaders=' + this.signedHeaders(), + 'Signature=' + this.signature(), + ].join(', ') +} + +RequestSigner.prototype.signature = function() { + var date = this.getDate(), + cacheKey = [this.credentials.secretAccessKey, date, this.region, this.service].join(), + kDate, kRegion, kService, kCredentials = credentialsCache.get(cacheKey) + if (!kCredentials) { + kDate = hmac('AWS4' + this.credentials.secretAccessKey, date) + kRegion = hmac(kDate, this.region) + kService = hmac(kRegion, this.service) + kCredentials = hmac(kService, 'aws4_request') + credentialsCache.set(cacheKey, kCredentials) + } + return hmac(kCredentials, this.stringToSign(), 'hex') +} + +RequestSigner.prototype.stringToSign = function() { + return [ + 'AWS4-HMAC-SHA256', + this.getDateTime(), + this.credentialString(), + hash(this.canonicalString(), 'hex'), + ].join('\n') +} + +RequestSigner.prototype.canonicalString = function() { + if (!this.parsedPath) this.prepareRequest() + + var pathStr = this.parsedPath.path, + query = this.parsedPath.query, + headers = this.request.headers, + queryStr = '', + normalizePath = this.service !== 's3', + decodePath = this.service === 's3' || this.request.doNotEncodePath, + decodeSlashesInPath = this.service === 's3', + firstValOnly = this.service === 's3', + bodyHash + + if (this.service === 's3' && this.request.signQuery) { + bodyHash = 'UNSIGNED-PAYLOAD' + } else if (this.isCodeCommitGit) { + bodyHash = '' + } else { + bodyHash = headers['X-Amz-Content-Sha256'] || headers['x-amz-content-sha256'] || + hash(this.request.body || '', 'hex') + } + + if (query) { + var reducedQuery = Object.keys(query).reduce(function(obj, key) { + if (!key) return obj + obj[encodeRfc3986Full(key)] = !Array.isArray(query[key]) ? query[key] : + (firstValOnly ? query[key][0] : query[key]) + return obj + }, {}) + var encodedQueryPieces = [] + Object.keys(reducedQuery).sort().forEach(function(key) { + if (!Array.isArray(reducedQuery[key])) { + encodedQueryPieces.push(key + '=' + encodeRfc3986Full(reducedQuery[key])) + } else { + reducedQuery[key].map(encodeRfc3986Full).sort() + .forEach(function(val) { encodedQueryPieces.push(key + '=' + val) }) + } + }) + queryStr = encodedQueryPieces.join('&') + } + if (pathStr !== '/') { + if (normalizePath) pathStr = pathStr.replace(/\/{2,}/g, '/') + pathStr = pathStr.split('/').reduce(function(path, piece) { + if (normalizePath && piece === '..') { + path.pop() + } else if (!normalizePath || piece !== '.') { + if (decodePath) piece = decodeURIComponent(piece).replace(/\+/g, ' ') + path.push(encodeRfc3986Full(piece)) + } + return path + }, []).join('/') + if (pathStr[0] !== '/') pathStr = '/' + pathStr + if (decodeSlashesInPath) pathStr = pathStr.replace(/%2F/g, '/') + } + + return [ + this.request.method || 'GET', + pathStr, + queryStr, + this.canonicalHeaders() + '\n', + this.signedHeaders(), + bodyHash, + ].join('\n') +} + +RequestSigner.prototype.canonicalHeaders = function() { + var headers = this.request.headers + function trimAll(header) { + return header.toString().trim().replace(/\s+/g, ' ') + } + return Object.keys(headers) + .sort(function(a, b) { return a.toLowerCase() < b.toLowerCase() ? -1 : 1 }) + .map(function(key) { return key.toLowerCase() + ':' + trimAll(headers[key]) }) + .join('\n') +} + +RequestSigner.prototype.signedHeaders = function() { + return Object.keys(this.request.headers) + .map(function(key) { return key.toLowerCase() }) + .sort() + .join(';') +} + +RequestSigner.prototype.credentialString = function() { + return [ + this.getDate(), + this.region, + this.service, + 'aws4_request', + ].join('/') +} + +RequestSigner.prototype.defaultCredentials = function() { + var env = process.env + return { + accessKeyId: env.AWS_ACCESS_KEY_ID || env.AWS_ACCESS_KEY, + secretAccessKey: env.AWS_SECRET_ACCESS_KEY || env.AWS_SECRET_KEY, + sessionToken: env.AWS_SESSION_TOKEN, + } +} + +RequestSigner.prototype.parsePath = function() { + var path = this.request.path || '/' + + // S3 doesn't always encode characters > 127 correctly and + // all services don't encode characters > 255 correctly + // So if there are non-reserved chars (and it's not already all % encoded), just encode them all + if (/[^0-9A-Za-z;,/?:@&=+$\-_.!~*'()#%]/.test(path)) { + path = encodeURI(decodeURI(path)) + } + + var queryIx = path.indexOf('?'), + query = null + + if (queryIx >= 0) { + query = querystring.parse(path.slice(queryIx + 1)) + path = path.slice(0, queryIx) + } + + this.parsedPath = { + path: path, + query: query, + } +} + +RequestSigner.prototype.formatPath = function() { + var path = this.parsedPath.path, + query = this.parsedPath.query + + if (!query) return path + + // Services don't support empty query string keys + if (query[''] != null) delete query[''] + + return path + '?' + encodeRfc3986(querystring.stringify(query)) +} + +aws4.RequestSigner = RequestSigner + +aws4.sign = function(request, credentials) { + return new RequestSigner(request, credentials).sign() +} + + +/***/ }), + +/***/ 662: +/***/ (function(module) { + +"use strict"; + +module.exports = function generate_const(it, $keyword, $ruleType) { + var out = ' '; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $data = 'data' + ($dataLvl || ''); + var $valid = 'valid' + $lvl; + var $isData = it.opts.$data && $schema && $schema.$data, + $schemaValue; + if ($isData) { + out += ' var schema' + ($lvl) + ' = ' + (it.util.getData($schema.$data, $dataLvl, it.dataPathArr)) + '; '; + $schemaValue = 'schema' + $lvl; + } else { + $schemaValue = $schema; + } + if (!$isData) { + out += ' var schema' + ($lvl) + ' = validate.schema' + ($schemaPath) + ';'; + } + out += 'var ' + ($valid) + ' = equal(' + ($data) + ', schema' + ($lvl) + '); if (!' + ($valid) + ') { '; + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('const') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { allowedValue: schema' + ($lvl) + ' } '; + if (it.opts.messages !== false) { + out += ' , message: \'should be equal to constant\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + out += ' }'; + if ($breakOnError) { + out += ' else { '; + } + return out; +} + + +/***/ }), + +/***/ 669: +/***/ (function(module) { + +module.exports = require("util"); + +/***/ }), + +/***/ 671: +/***/ (function(module, __unusedexports, __webpack_require__) { + +"use strict"; + + +module.exports = { + afterRequest: __webpack_require__(672), + beforeRequest: __webpack_require__(616), + browser: __webpack_require__(222), + cache: __webpack_require__(993), + content: __webpack_require__(162), + cookie: __webpack_require__(326), + creator: __webpack_require__(776), + entry: __webpack_require__(919), + har: __webpack_require__(41), + header: __webpack_require__(883), + log: __webpack_require__(319), + page: __webpack_require__(744), + pageTimings: __webpack_require__(181), + postData: __webpack_require__(740), + query: __webpack_require__(813), + request: __webpack_require__(380), + response: __webpack_require__(226), + timings: __webpack_require__(758) +} + + +/***/ }), + +/***/ 672: +/***/ (function(module) { + +module.exports = {"$id":"afterRequest.json#","$schema":"http://json-schema.org/draft-06/schema#","type":"object","optional":true,"required":["lastAccess","eTag","hitCount"],"properties":{"expires":{"type":"string","pattern":"^(\\d{4})(-)?(\\d\\d)(-)?(\\d\\d)(T)?(\\d\\d)(:)?(\\d\\d)(:)?(\\d\\d)(\\.\\d+)?(Z|([+-])(\\d\\d)(:)?(\\d\\d))?"},"lastAccess":{"type":"string","pattern":"^(\\d{4})(-)?(\\d\\d)(-)?(\\d\\d)(T)?(\\d\\d)(:)?(\\d\\d)(:)?(\\d\\d)(\\.\\d+)?(Z|([+-])(\\d\\d)(:)?(\\d\\d))?"},"eTag":{"type":"string"},"hitCount":{"type":"integer"},"comment":{"type":"string"}}}; + +/***/ }), + +/***/ 673: +/***/ (function(module) { + +"use strict"; + +module.exports = function generate_not(it, $keyword, $ruleType) { + var out = ' '; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $data = 'data' + ($dataLvl || ''); + var $errs = 'errs__' + $lvl; + var $it = it.util.copy(it); + $it.level++; + var $nextValid = 'valid' + $it.level; + if ((it.opts.strictKeywords ? typeof $schema == 'object' && Object.keys($schema).length > 0 : it.util.schemaHasRules($schema, it.RULES.all))) { + $it.schema = $schema; + $it.schemaPath = $schemaPath; + $it.errSchemaPath = $errSchemaPath; + out += ' var ' + ($errs) + ' = errors; '; + var $wasComposite = it.compositeRule; + it.compositeRule = $it.compositeRule = true; + $it.createErrors = false; + var $allErrorsOption; + if ($it.opts.allErrors) { + $allErrorsOption = $it.opts.allErrors; + $it.opts.allErrors = false; + } + out += ' ' + (it.validate($it)) + ' '; + $it.createErrors = true; + if ($allErrorsOption) $it.opts.allErrors = $allErrorsOption; + it.compositeRule = $it.compositeRule = $wasComposite; + out += ' if (' + ($nextValid) + ') { '; + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('not') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: {} '; + if (it.opts.messages !== false) { + out += ' , message: \'should NOT be valid\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + out += ' } else { errors = ' + ($errs) + '; if (vErrors !== null) { if (' + ($errs) + ') vErrors.length = ' + ($errs) + '; else vErrors = null; } '; + if (it.opts.allErrors) { + out += ' } '; + } + } else { + out += ' var err = '; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('not') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: {} '; + if (it.opts.messages !== false) { + out += ' , message: \'should NOT be valid\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + out += '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + if ($breakOnError) { + out += ' if (false) { '; + } + } + return out; +} + + +/***/ }), + +/***/ 680: +/***/ (function(module, __unusedexports, __webpack_require__) { + +// Copyright 2016 Joyent, Inc. + +var x509 = __webpack_require__(866); + +module.exports = { + read: read, + verify: x509.verify, + sign: x509.sign, + write: write +}; + +var assert = __webpack_require__(477); +var asn1 = __webpack_require__(62); +var Buffer = __webpack_require__(215).Buffer; +var algs = __webpack_require__(98); +var utils = __webpack_require__(270); +var Key = __webpack_require__(852); +var PrivateKey = __webpack_require__(502); +var pem = __webpack_require__(268); +var Identity = __webpack_require__(378); +var Signature = __webpack_require__(575); +var Certificate = __webpack_require__(752); + +function read(buf, options) { + if (typeof (buf) !== 'string') { + assert.buffer(buf, 'buf'); + buf = buf.toString('ascii'); + } + + var lines = buf.trim().split(/[\r\n]+/g); + + var m; + var si = -1; + while (!m && si < lines.length) { + m = lines[++si].match(/*JSSTYLED*/ + /[-]+[ ]*BEGIN CERTIFICATE[ ]*[-]+/); + } + assert.ok(m, 'invalid PEM header'); + + var m2; + var ei = lines.length; + while (!m2 && ei > 0) { + m2 = lines[--ei].match(/*JSSTYLED*/ + /[-]+[ ]*END CERTIFICATE[ ]*[-]+/); + } + assert.ok(m2, 'invalid PEM footer'); + + lines = lines.slice(si, ei + 1); + + var headers = {}; + while (true) { + lines = lines.slice(1); + m = lines[0].match(/*JSSTYLED*/ + /^([A-Za-z0-9-]+): (.+)$/); + if (!m) + break; + headers[m[1].toLowerCase()] = m[2]; + } + + /* Chop off the first and last lines */ + lines = lines.slice(0, -1).join(''); + buf = Buffer.from(lines, 'base64'); + + return (x509.read(buf, options)); +} + +function write(cert, options) { + var dbuf = x509.write(cert, options); + + var header = 'CERTIFICATE'; + var tmp = dbuf.toString('base64'); + var len = tmp.length + (tmp.length / 64) + + 18 + 16 + header.length*2 + 10; + var buf = Buffer.alloc(len); + var o = 0; + o += buf.write('-----BEGIN ' + header + '-----\n', o); + for (var i = 0; i < tmp.length; ) { + var limit = i + 64; + if (limit > tmp.length) + limit = tmp.length; + o += buf.write(tmp.slice(i, limit), o); + buf[o++] = 10; + i = limit; + } + o += buf.write('-----END ' + header + '-----\n', o); + + return (buf.slice(0, o)); +} + + +/***/ }), + +/***/ 687: +/***/ (function(module) { + +"use strict"; + +module.exports = function generate_format(it, $keyword, $ruleType) { + var out = ' '; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $data = 'data' + ($dataLvl || ''); + if (it.opts.format === false) { + if ($breakOnError) { + out += ' if (true) { '; + } + return out; + } + var $isData = it.opts.$data && $schema && $schema.$data, + $schemaValue; + if ($isData) { + out += ' var schema' + ($lvl) + ' = ' + (it.util.getData($schema.$data, $dataLvl, it.dataPathArr)) + '; '; + $schemaValue = 'schema' + $lvl; + } else { + $schemaValue = $schema; + } + var $unknownFormats = it.opts.unknownFormats, + $allowUnknown = Array.isArray($unknownFormats); + if ($isData) { + var $format = 'format' + $lvl, + $isObject = 'isObject' + $lvl, + $formatType = 'formatType' + $lvl; + out += ' var ' + ($format) + ' = formats[' + ($schemaValue) + ']; var ' + ($isObject) + ' = typeof ' + ($format) + ' == \'object\' && !(' + ($format) + ' instanceof RegExp) && ' + ($format) + '.validate; var ' + ($formatType) + ' = ' + ($isObject) + ' && ' + ($format) + '.type || \'string\'; if (' + ($isObject) + ') { '; + if (it.async) { + out += ' var async' + ($lvl) + ' = ' + ($format) + '.async; '; + } + out += ' ' + ($format) + ' = ' + ($format) + '.validate; } if ( '; + if ($isData) { + out += ' (' + ($schemaValue) + ' !== undefined && typeof ' + ($schemaValue) + ' != \'string\') || '; + } + out += ' ('; + if ($unknownFormats != 'ignore') { + out += ' (' + ($schemaValue) + ' && !' + ($format) + ' '; + if ($allowUnknown) { + out += ' && self._opts.unknownFormats.indexOf(' + ($schemaValue) + ') == -1 '; + } + out += ') || '; + } + out += ' (' + ($format) + ' && ' + ($formatType) + ' == \'' + ($ruleType) + '\' && !(typeof ' + ($format) + ' == \'function\' ? '; + if (it.async) { + out += ' (async' + ($lvl) + ' ? await ' + ($format) + '(' + ($data) + ') : ' + ($format) + '(' + ($data) + ')) '; + } else { + out += ' ' + ($format) + '(' + ($data) + ') '; + } + out += ' : ' + ($format) + '.test(' + ($data) + '))))) {'; + } else { + var $format = it.formats[$schema]; + if (!$format) { + if ($unknownFormats == 'ignore') { + it.logger.warn('unknown format "' + $schema + '" ignored in schema at path "' + it.errSchemaPath + '"'); + if ($breakOnError) { + out += ' if (true) { '; + } + return out; + } else if ($allowUnknown && $unknownFormats.indexOf($schema) >= 0) { + if ($breakOnError) { + out += ' if (true) { '; + } + return out; + } else { + throw new Error('unknown format "' + $schema + '" is used in schema at path "' + it.errSchemaPath + '"'); + } + } + var $isObject = typeof $format == 'object' && !($format instanceof RegExp) && $format.validate; + var $formatType = $isObject && $format.type || 'string'; + if ($isObject) { + var $async = $format.async === true; + $format = $format.validate; + } + if ($formatType != $ruleType) { + if ($breakOnError) { + out += ' if (true) { '; + } + return out; + } + if ($async) { + if (!it.async) throw new Error('async format in sync schema'); + var $formatRef = 'formats' + it.util.getProperty($schema) + '.validate'; + out += ' if (!(await ' + ($formatRef) + '(' + ($data) + '))) { '; + } else { + out += ' if (! '; + var $formatRef = 'formats' + it.util.getProperty($schema); + if ($isObject) $formatRef += '.validate'; + if (typeof $format == 'function') { + out += ' ' + ($formatRef) + '(' + ($data) + ') '; + } else { + out += ' ' + ($formatRef) + '.test(' + ($data) + ') '; + } + out += ') { '; + } + } + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('format') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { format: '; + if ($isData) { + out += '' + ($schemaValue); + } else { + out += '' + (it.util.toQuotedString($schema)); + } + out += ' } '; + if (it.opts.messages !== false) { + out += ' , message: \'should match format "'; + if ($isData) { + out += '\' + ' + ($schemaValue) + ' + \''; + } else { + out += '' + (it.util.escapeQuotes($schema)); + } + out += '"\' '; + } + if (it.opts.verbose) { + out += ' , schema: '; + if ($isData) { + out += 'validate.schema' + ($schemaPath); + } else { + out += '' + (it.util.toQuotedString($schema)); + } + out += ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + out += ' } '; + if ($breakOnError) { + out += ' else { '; + } + return out; +} + + +/***/ }), + +/***/ 691: +/***/ (function(module) { + +"use strict"; + + +// https://mathiasbynens.be/notes/javascript-encoding +// https://github.com/bestiejs/punycode.js - punycode.ucs2.decode +module.exports = function ucs2length(str) { + var length = 0 + , len = str.length + , pos = 0 + , value; + while (pos < len) { + length++; + value = str.charCodeAt(pos++); + if (value >= 0xD800 && value <= 0xDBFF && pos < len) { + // high surrogate, and there is a next character + value = str.charCodeAt(pos); + if ((value & 0xFC00) == 0xDC00) pos++; // low surrogate + } + } + return length; +}; + + +/***/ }), + +/***/ 697: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +/* + * extsprintf.js: extended POSIX-style sprintf + */ + +var mod_assert = __webpack_require__(357); +var mod_util = __webpack_require__(669); + +/* + * Public interface + */ +exports.sprintf = jsSprintf; +exports.printf = jsPrintf; +exports.fprintf = jsFprintf; + +/* + * Stripped down version of s[n]printf(3c). We make a best effort to throw an + * exception when given a format string we don't understand, rather than + * ignoring it, so that we won't break existing programs if/when we go implement + * the rest of this. + * + * This implementation currently supports specifying + * - field alignment ('-' flag), + * - zero-pad ('0' flag) + * - always show numeric sign ('+' flag), + * - field width + * - conversions for strings, decimal integers, and floats (numbers). + * - argument size specifiers. These are all accepted but ignored, since + * Javascript has no notion of the physical size of an argument. + * + * Everything else is currently unsupported, most notably precision, unsigned + * numbers, non-decimal numbers, and characters. + */ +function jsSprintf(fmt) +{ + var regex = [ + '([^%]*)', /* normal text */ + '%', /* start of format */ + '([\'\\-+ #0]*?)', /* flags (optional) */ + '([1-9]\\d*)?', /* width (optional) */ + '(\\.([1-9]\\d*))?', /* precision (optional) */ + '[lhjztL]*?', /* length mods (ignored) */ + '([diouxXfFeEgGaAcCsSp%jr])' /* conversion */ + ].join(''); + + var re = new RegExp(regex); + var args = Array.prototype.slice.call(arguments, 1); + var flags, width, precision, conversion; + var left, pad, sign, arg, match; + var ret = ''; + var argn = 1; + + mod_assert.equal('string', typeof (fmt)); + + while ((match = re.exec(fmt)) !== null) { + ret += match[1]; + fmt = fmt.substring(match[0].length); + + flags = match[2] || ''; + width = match[3] || 0; + precision = match[4] || ''; + conversion = match[6]; + left = false; + sign = false; + pad = ' '; + + if (conversion == '%') { + ret += '%'; + continue; + } + + if (args.length === 0) + throw (new Error('too few args to sprintf')); + + arg = args.shift(); + argn++; + + if (flags.match(/[\' #]/)) + throw (new Error( + 'unsupported flags: ' + flags)); + + if (precision.length > 0) + throw (new Error( + 'non-zero precision not supported')); + + if (flags.match(/-/)) + left = true; + + if (flags.match(/0/)) + pad = '0'; + + if (flags.match(/\+/)) + sign = true; + + switch (conversion) { + case 's': + if (arg === undefined || arg === null) + throw (new Error('argument ' + argn + + ': attempted to print undefined or null ' + + 'as a string')); + ret += doPad(pad, width, left, arg.toString()); + break; + + case 'd': + arg = Math.floor(arg); + /*jsl:fallthru*/ + case 'f': + sign = sign && arg > 0 ? '+' : ''; + ret += sign + doPad(pad, width, left, + arg.toString()); + break; + + case 'x': + ret += doPad(pad, width, left, arg.toString(16)); + break; + + case 'j': /* non-standard */ + if (width === 0) + width = 10; + ret += mod_util.inspect(arg, false, width); + break; + + case 'r': /* non-standard */ + ret += dumpException(arg); + break; + + default: + throw (new Error('unsupported conversion: ' + + conversion)); + } + } + + ret += fmt; + return (ret); +} + +function jsPrintf() { + var args = Array.prototype.slice.call(arguments); + args.unshift(process.stdout); + jsFprintf.apply(null, args); +} + +function jsFprintf(stream) { + var args = Array.prototype.slice.call(arguments, 1); + return (stream.write(jsSprintf.apply(this, args))); +} + +function doPad(chr, width, left, str) +{ + var ret = str; + + while (ret.length < width) { + if (left) + ret += chr; + else + ret = chr + ret; + } + + return (ret); +} + +/* + * This function dumps long stack traces for exceptions having a cause() method. + * See node-verror for an example. + */ +function dumpException(ex) +{ + var ret; + + if (!(ex instanceof Error)) + throw (new Error(jsSprintf('invalid type for %%r: %j', ex))); + + /* Note that V8 prepends "ex.stack" with ex.toString(). */ + ret = 'EXCEPTION: ' + ex.constructor.name + ': ' + ex.stack; + + if (ex.cause && typeof (ex.cause) === 'function') { + var cex = ex.cause(); + if (cex) { + ret += '\nCaused by: ' + dumpException(cex); + } + } + + return (ret); +} + + +/***/ }), + +/***/ 701: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; +/*! + * Copyright (c) 2015, Salesforce.com, Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of Salesforce.com nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +var net = __webpack_require__(631); +var urlParse = __webpack_require__(835).parse; +var util = __webpack_require__(669); +var pubsuffix = __webpack_require__(519); +var Store = __webpack_require__(627).Store; +var MemoryCookieStore = __webpack_require__(349).MemoryCookieStore; +var pathMatch = __webpack_require__(54).pathMatch; +var VERSION = __webpack_require__(459); + +var punycode; +try { + punycode = __webpack_require__(213); +} catch(e) { + console.warn("tough-cookie: can't load punycode; won't use punycode for domain normalization"); +} + +// From RFC6265 S4.1.1 +// note that it excludes \x3B ";" +var COOKIE_OCTETS = /^[\x21\x23-\x2B\x2D-\x3A\x3C-\x5B\x5D-\x7E]+$/; + +var CONTROL_CHARS = /[\x00-\x1F]/; + +// From Chromium // '\r', '\n' and '\0' should be treated as a terminator in +// the "relaxed" mode, see: +// https://github.com/ChromiumWebApps/chromium/blob/b3d3b4da8bb94c1b2e061600df106d590fda3620/net/cookies/parsed_cookie.cc#L60 +var TERMINATORS = ['\n', '\r', '\0']; + +// RFC6265 S4.1.1 defines path value as 'any CHAR except CTLs or ";"' +// Note ';' is \x3B +var PATH_VALUE = /[\x20-\x3A\x3C-\x7E]+/; + +// date-time parsing constants (RFC6265 S5.1.1) + +var DATE_DELIM = /[\x09\x20-\x2F\x3B-\x40\x5B-\x60\x7B-\x7E]/; + +var MONTH_TO_NUM = { + jan:0, feb:1, mar:2, apr:3, may:4, jun:5, + jul:6, aug:7, sep:8, oct:9, nov:10, dec:11 +}; +var NUM_TO_MONTH = [ + 'Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec' +]; +var NUM_TO_DAY = [ + 'Sun','Mon','Tue','Wed','Thu','Fri','Sat' +]; + +var MAX_TIME = 2147483647000; // 31-bit max +var MIN_TIME = 0; // 31-bit min + +/* + * Parses a Natural number (i.e., non-negative integer) with either the + * *DIGIT ( non-digit *OCTET ) + * or + * *DIGIT + * grammar (RFC6265 S5.1.1). + * + * The "trailingOK" boolean controls if the grammar accepts a + * "( non-digit *OCTET )" trailer. + */ +function parseDigits(token, minDigits, maxDigits, trailingOK) { + var count = 0; + while (count < token.length) { + var c = token.charCodeAt(count); + // "non-digit = %x00-2F / %x3A-FF" + if (c <= 0x2F || c >= 0x3A) { + break; + } + count++; + } + + // constrain to a minimum and maximum number of digits. + if (count < minDigits || count > maxDigits) { + return null; + } + + if (!trailingOK && count != token.length) { + return null; + } + + return parseInt(token.substr(0,count), 10); +} + +function parseTime(token) { + var parts = token.split(':'); + var result = [0,0,0]; + + /* RF6256 S5.1.1: + * time = hms-time ( non-digit *OCTET ) + * hms-time = time-field ":" time-field ":" time-field + * time-field = 1*2DIGIT + */ + + if (parts.length !== 3) { + return null; + } + + for (var i = 0; i < 3; i++) { + // "time-field" must be strictly "1*2DIGIT", HOWEVER, "hms-time" can be + // followed by "( non-digit *OCTET )" so therefore the last time-field can + // have a trailer + var trailingOK = (i == 2); + var num = parseDigits(parts[i], 1, 2, trailingOK); + if (num === null) { + return null; + } + result[i] = num; + } + + return result; +} + +function parseMonth(token) { + token = String(token).substr(0,3).toLowerCase(); + var num = MONTH_TO_NUM[token]; + return num >= 0 ? num : null; +} + +/* + * RFC6265 S5.1.1 date parser (see RFC for full grammar) + */ +function parseDate(str) { + if (!str) { + return; + } + + /* RFC6265 S5.1.1: + * 2. Process each date-token sequentially in the order the date-tokens + * appear in the cookie-date + */ + var tokens = str.split(DATE_DELIM); + if (!tokens) { + return; + } + + var hour = null; + var minute = null; + var second = null; + var dayOfMonth = null; + var month = null; + var year = null; + + for (var i=0; i= 70 && year <= 99) { + year += 1900; + } else if (year >= 0 && year <= 69) { + year += 2000; + } + } + } + } + + /* RFC 6265 S5.1.1 + * "5. Abort these steps and fail to parse the cookie-date if: + * * at least one of the found-day-of-month, found-month, found- + * year, or found-time flags is not set, + * * the day-of-month-value is less than 1 or greater than 31, + * * the year-value is less than 1601, + * * the hour-value is greater than 23, + * * the minute-value is greater than 59, or + * * the second-value is greater than 59. + * (Note that leap seconds cannot be represented in this syntax.)" + * + * So, in order as above: + */ + if ( + dayOfMonth === null || month === null || year === null || second === null || + dayOfMonth < 1 || dayOfMonth > 31 || + year < 1601 || + hour > 23 || + minute > 59 || + second > 59 + ) { + return; + } + + return new Date(Date.UTC(year, month, dayOfMonth, hour, minute, second)); +} + +function formatDate(date) { + var d = date.getUTCDate(); d = d >= 10 ? d : '0'+d; + var h = date.getUTCHours(); h = h >= 10 ? h : '0'+h; + var m = date.getUTCMinutes(); m = m >= 10 ? m : '0'+m; + var s = date.getUTCSeconds(); s = s >= 10 ? s : '0'+s; + return NUM_TO_DAY[date.getUTCDay()] + ', ' + + d+' '+ NUM_TO_MONTH[date.getUTCMonth()] +' '+ date.getUTCFullYear() +' '+ + h+':'+m+':'+s+' GMT'; +} + +// S5.1.2 Canonicalized Host Names +function canonicalDomain(str) { + if (str == null) { + return null; + } + str = str.trim().replace(/^\./,''); // S4.1.2.3 & S5.2.3: ignore leading . + + // convert to IDN if any non-ASCII characters + if (punycode && /[^\u0001-\u007f]/.test(str)) { + str = punycode.toASCII(str); + } + + return str.toLowerCase(); +} + +// S5.1.3 Domain Matching +function domainMatch(str, domStr, canonicalize) { + if (str == null || domStr == null) { + return null; + } + if (canonicalize !== false) { + str = canonicalDomain(str); + domStr = canonicalDomain(domStr); + } + + /* + * "The domain string and the string are identical. (Note that both the + * domain string and the string will have been canonicalized to lower case at + * this point)" + */ + if (str == domStr) { + return true; + } + + /* "All of the following [three] conditions hold:" (order adjusted from the RFC) */ + + /* "* The string is a host name (i.e., not an IP address)." */ + if (net.isIP(str)) { + return false; + } + + /* "* The domain string is a suffix of the string" */ + var idx = str.indexOf(domStr); + if (idx <= 0) { + return false; // it's a non-match (-1) or prefix (0) + } + + // e.g "a.b.c".indexOf("b.c") === 2 + // 5 === 3+2 + if (str.length !== domStr.length + idx) { // it's not a suffix + return false; + } + + /* "* The last character of the string that is not included in the domain + * string is a %x2E (".") character." */ + if (str.substr(idx-1,1) !== '.') { + return false; + } + + return true; +} + + +// RFC6265 S5.1.4 Paths and Path-Match + +/* + * "The user agent MUST use an algorithm equivalent to the following algorithm + * to compute the default-path of a cookie:" + * + * Assumption: the path (and not query part or absolute uri) is passed in. + */ +function defaultPath(path) { + // "2. If the uri-path is empty or if the first character of the uri-path is not + // a %x2F ("/") character, output %x2F ("/") and skip the remaining steps. + if (!path || path.substr(0,1) !== "/") { + return "/"; + } + + // "3. If the uri-path contains no more than one %x2F ("/") character, output + // %x2F ("/") and skip the remaining step." + if (path === "/") { + return path; + } + + var rightSlash = path.lastIndexOf("/"); + if (rightSlash === 0) { + return "/"; + } + + // "4. Output the characters of the uri-path from the first character up to, + // but not including, the right-most %x2F ("/")." + return path.slice(0, rightSlash); +} + +function trimTerminator(str) { + for (var t = 0; t < TERMINATORS.length; t++) { + var terminatorIdx = str.indexOf(TERMINATORS[t]); + if (terminatorIdx !== -1) { + str = str.substr(0,terminatorIdx); + } + } + + return str; +} + +function parseCookiePair(cookiePair, looseMode) { + cookiePair = trimTerminator(cookiePair); + + var firstEq = cookiePair.indexOf('='); + if (looseMode) { + if (firstEq === 0) { // '=' is immediately at start + cookiePair = cookiePair.substr(1); + firstEq = cookiePair.indexOf('='); // might still need to split on '=' + } + } else { // non-loose mode + if (firstEq <= 0) { // no '=' or is at start + return; // needs to have non-empty "cookie-name" + } + } + + var cookieName, cookieValue; + if (firstEq <= 0) { + cookieName = ""; + cookieValue = cookiePair.trim(); + } else { + cookieName = cookiePair.substr(0, firstEq).trim(); + cookieValue = cookiePair.substr(firstEq+1).trim(); + } + + if (CONTROL_CHARS.test(cookieName) || CONTROL_CHARS.test(cookieValue)) { + return; + } + + var c = new Cookie(); + c.key = cookieName; + c.value = cookieValue; + return c; +} + +function parse(str, options) { + if (!options || typeof options !== 'object') { + options = {}; + } + str = str.trim(); + + // We use a regex to parse the "name-value-pair" part of S5.2 + var firstSemi = str.indexOf(';'); // S5.2 step 1 + var cookiePair = (firstSemi === -1) ? str : str.substr(0, firstSemi); + var c = parseCookiePair(cookiePair, !!options.loose); + if (!c) { + return; + } + + if (firstSemi === -1) { + return c; + } + + // S5.2.3 "unparsed-attributes consist of the remainder of the set-cookie-string + // (including the %x3B (";") in question)." plus later on in the same section + // "discard the first ";" and trim". + var unparsed = str.slice(firstSemi + 1).trim(); + + // "If the unparsed-attributes string is empty, skip the rest of these + // steps." + if (unparsed.length === 0) { + return c; + } + + /* + * S5.2 says that when looping over the items "[p]rocess the attribute-name + * and attribute-value according to the requirements in the following + * subsections" for every item. Plus, for many of the individual attributes + * in S5.3 it says to use the "attribute-value of the last attribute in the + * cookie-attribute-list". Therefore, in this implementation, we overwrite + * the previous value. + */ + var cookie_avs = unparsed.split(';'); + while (cookie_avs.length) { + var av = cookie_avs.shift().trim(); + if (av.length === 0) { // happens if ";;" appears + continue; + } + var av_sep = av.indexOf('='); + var av_key, av_value; + + if (av_sep === -1) { + av_key = av; + av_value = null; + } else { + av_key = av.substr(0,av_sep); + av_value = av.substr(av_sep+1); + } + + av_key = av_key.trim().toLowerCase(); + + if (av_value) { + av_value = av_value.trim(); + } + + switch(av_key) { + case 'expires': // S5.2.1 + if (av_value) { + var exp = parseDate(av_value); + // "If the attribute-value failed to parse as a cookie date, ignore the + // cookie-av." + if (exp) { + // over and underflow not realistically a concern: V8's getTime() seems to + // store something larger than a 32-bit time_t (even with 32-bit node) + c.expires = exp; + } + } + break; + + case 'max-age': // S5.2.2 + if (av_value) { + // "If the first character of the attribute-value is not a DIGIT or a "-" + // character ...[or]... If the remainder of attribute-value contains a + // non-DIGIT character, ignore the cookie-av." + if (/^-?[0-9]+$/.test(av_value)) { + var delta = parseInt(av_value, 10); + // "If delta-seconds is less than or equal to zero (0), let expiry-time + // be the earliest representable date and time." + c.setMaxAge(delta); + } + } + break; + + case 'domain': // S5.2.3 + // "If the attribute-value is empty, the behavior is undefined. However, + // the user agent SHOULD ignore the cookie-av entirely." + if (av_value) { + // S5.2.3 "Let cookie-domain be the attribute-value without the leading %x2E + // (".") character." + var domain = av_value.trim().replace(/^\./, ''); + if (domain) { + // "Convert the cookie-domain to lower case." + c.domain = domain.toLowerCase(); + } + } + break; + + case 'path': // S5.2.4 + /* + * "If the attribute-value is empty or if the first character of the + * attribute-value is not %x2F ("/"): + * Let cookie-path be the default-path. + * Otherwise: + * Let cookie-path be the attribute-value." + * + * We'll represent the default-path as null since it depends on the + * context of the parsing. + */ + c.path = av_value && av_value[0] === "/" ? av_value : null; + break; + + case 'secure': // S5.2.5 + /* + * "If the attribute-name case-insensitively matches the string "Secure", + * the user agent MUST append an attribute to the cookie-attribute-list + * with an attribute-name of Secure and an empty attribute-value." + */ + c.secure = true; + break; + + case 'httponly': // S5.2.6 -- effectively the same as 'secure' + c.httpOnly = true; + break; + + default: + c.extensions = c.extensions || []; + c.extensions.push(av); + break; + } + } + + return c; +} + +// avoid the V8 deoptimization monster! +function jsonParse(str) { + var obj; + try { + obj = JSON.parse(str); + } catch (e) { + return e; + } + return obj; +} + +function fromJSON(str) { + if (!str) { + return null; + } + + var obj; + if (typeof str === 'string') { + obj = jsonParse(str); + if (obj instanceof Error) { + return null; + } + } else { + // assume it's an Object + obj = str; + } + + var c = new Cookie(); + for (var i=0; i 1) { + var lindex = path.lastIndexOf('/'); + if (lindex === 0) { + break; + } + path = path.substr(0,lindex); + permutations.push(path); + } + permutations.push('/'); + return permutations; +} + +function getCookieContext(url) { + if (url instanceof Object) { + return url; + } + // NOTE: decodeURI will throw on malformed URIs (see GH-32). + // Therefore, we will just skip decoding for such URIs. + try { + url = decodeURI(url); + } + catch(err) { + // Silently swallow error + } + + return urlParse(url); +} + +function Cookie(options) { + options = options || {}; + + Object.keys(options).forEach(function(prop) { + if (Cookie.prototype.hasOwnProperty(prop) && + Cookie.prototype[prop] !== options[prop] && + prop.substr(0,1) !== '_') + { + this[prop] = options[prop]; + } + }, this); + + this.creation = this.creation || new Date(); + + // used to break creation ties in cookieCompare(): + Object.defineProperty(this, 'creationIndex', { + configurable: false, + enumerable: false, // important for assert.deepEqual checks + writable: true, + value: ++Cookie.cookiesCreated + }); +} + +Cookie.cookiesCreated = 0; // incremented each time a cookie is created + +Cookie.parse = parse; +Cookie.fromJSON = fromJSON; + +Cookie.prototype.key = ""; +Cookie.prototype.value = ""; + +// the order in which the RFC has them: +Cookie.prototype.expires = "Infinity"; // coerces to literal Infinity +Cookie.prototype.maxAge = null; // takes precedence over expires for TTL +Cookie.prototype.domain = null; +Cookie.prototype.path = null; +Cookie.prototype.secure = false; +Cookie.prototype.httpOnly = false; +Cookie.prototype.extensions = null; + +// set by the CookieJar: +Cookie.prototype.hostOnly = null; // boolean when set +Cookie.prototype.pathIsDefault = null; // boolean when set +Cookie.prototype.creation = null; // Date when set; defaulted by Cookie.parse +Cookie.prototype.lastAccessed = null; // Date when set +Object.defineProperty(Cookie.prototype, 'creationIndex', { + configurable: true, + enumerable: false, + writable: true, + value: 0 +}); + +Cookie.serializableProperties = Object.keys(Cookie.prototype) + .filter(function(prop) { + return !( + Cookie.prototype[prop] instanceof Function || + prop === 'creationIndex' || + prop.substr(0,1) === '_' + ); + }); + +Cookie.prototype.inspect = function inspect() { + var now = Date.now(); + return 'Cookie="'+this.toString() + + '; hostOnly='+(this.hostOnly != null ? this.hostOnly : '?') + + '; aAge='+(this.lastAccessed ? (now-this.lastAccessed.getTime())+'ms' : '?') + + '; cAge='+(this.creation ? (now-this.creation.getTime())+'ms' : '?') + + '"'; +}; + +// Use the new custom inspection symbol to add the custom inspect function if +// available. +if (util.inspect.custom) { + Cookie.prototype[util.inspect.custom] = Cookie.prototype.inspect; +} + +Cookie.prototype.toJSON = function() { + var obj = {}; + + var props = Cookie.serializableProperties; + for (var i=0; i schema.maxItems){ + addError("There must be a maximum of " + schema.maxItems + " in the array"); + } + }else if(schema.properties || schema.additionalProperties){ + errors.concat(checkObj(value, schema.properties, path, schema.additionalProperties)); + } + if(schema.pattern && typeof value == 'string' && !value.match(schema.pattern)){ + addError("does not match the regex pattern " + schema.pattern); + } + if(schema.maxLength && typeof value == 'string' && value.length > schema.maxLength){ + addError("may only be " + schema.maxLength + " characters long"); + } + if(schema.minLength && typeof value == 'string' && value.length < schema.minLength){ + addError("must be at least " + schema.minLength + " characters long"); + } + if(typeof schema.minimum !== undefined && typeof value == typeof schema.minimum && + schema.minimum > value){ + addError("must have a minimum value of " + schema.minimum); + } + if(typeof schema.maximum !== undefined && typeof value == typeof schema.maximum && + schema.maximum < value){ + addError("must have a maximum value of " + schema.maximum); + } + if(schema['enum']){ + var enumer = schema['enum']; + l = enumer.length; + var found; + for(var j = 0; j < l; j++){ + if(enumer[j]===value){ + found=1; + break; + } + } + if(!found){ + addError("does not have a value in the enumeration " + enumer.join(", ")); + } + } + if(typeof schema.maxDecimal == 'number' && + (value.toString().match(new RegExp("\\.[0-9]{" + (schema.maxDecimal + 1) + ",}")))){ + addError("may only have " + schema.maxDecimal + " digits of decimal places"); + } + } + } + return null; + } + // validate an object against a schema + function checkObj(instance,objTypeDef,path,additionalProp){ + + if(typeof objTypeDef =='object'){ + if(typeof instance != 'object' || instance instanceof Array){ + errors.push({property:path,message:"an object is required"}); + } + + for(var i in objTypeDef){ + if(objTypeDef.hasOwnProperty(i)){ + var value = instance[i]; + // skip _not_ specified properties + if (value === undefined && options.existingOnly) continue; + var propDef = objTypeDef[i]; + // set default + if(value === undefined && propDef["default"]){ + value = instance[i] = propDef["default"]; + } + if(options.coerce && i in instance){ + value = instance[i] = options.coerce(value, propDef); + } + checkProp(value,propDef,path,i); + } + } + } + for(i in instance){ + if(instance.hasOwnProperty(i) && !(i.charAt(0) == '_' && i.charAt(1) == '_') && objTypeDef && !objTypeDef[i] && additionalProp===false){ + if (options.filter) { + delete instance[i]; + continue; + } else { + errors.push({property:path,message:(typeof value) + "The property " + i + + " is not defined in the schema and the schema does not allow additional properties"}); + } + } + var requires = objTypeDef && objTypeDef[i] && objTypeDef[i].requires; + if(requires && !(requires in instance)){ + errors.push({property:path,message:"the presence of the property " + i + " requires that " + requires + " also be present"}); + } + value = instance[i]; + if(additionalProp && (!(objTypeDef && typeof objTypeDef == 'object') || !(i in objTypeDef))){ + if(options.coerce){ + value = instance[i] = options.coerce(value, additionalProp); + } + checkProp(value,additionalProp,path,i); + } + if(!_changing && value && value.$schema){ + errors = errors.concat(checkProp(value,value.$schema,path,i)); + } + } + return errors; + } + if(schema){ + checkProp(instance,schema,'',_changing || ''); + } + if(!_changing && instance && instance.$schema){ + checkProp(instance,instance.$schema,'',''); + } + return {valid:!errors.length,errors:errors}; +}; +exports.mustBeValid = function(result){ + // summary: + // This checks to ensure that the result is valid and will throw an appropriate error message if it is not + // result: the result returned from checkPropertyChange or validate + if(!result.valid){ + throw new TypeError(result.errors.map(function(error){return "for property " + error.property + ': ' + error.message;}).join(", \n")); + } +} + +return exports; +})); + + +/***/ }), + +/***/ 704: +/***/ (function(module, exports) { + +exports = module.exports = stringify +exports.getSerialize = serializer + +function stringify(obj, replacer, spaces, cycleReplacer) { + return JSON.stringify(obj, serializer(replacer, cycleReplacer), spaces) +} + +function serializer(replacer, cycleReplacer) { + var stack = [], keys = [] + + if (cycleReplacer == null) cycleReplacer = function(key, value) { + if (stack[0] === value) return "[Circular ~]" + return "[Circular ~." + keys.slice(0, stack.indexOf(value)).join(".") + "]" + } + + return function(key, value) { + if (stack.length > 0) { + var thisPos = stack.indexOf(this) + ~thisPos ? stack.splice(thisPos + 1) : stack.push(this) + ~thisPos ? keys.splice(thisPos, Infinity, key) : keys.push(key) + if (~stack.indexOf(value)) value = cycleReplacer.call(this, key, value) + } + else stack.push(value) + + return replacer == null ? value : replacer.call(this, key, value) + } +} + + +/***/ }), + +/***/ 707: +/***/ (function(module, __unusedexports, __webpack_require__) { + +// Copyright 2018 Joyent, Inc. + +module.exports = { + read: read, + readPkcs8: readPkcs8, + write: write, + writePkcs8: writePkcs8, + pkcs8ToBuffer: pkcs8ToBuffer, + + readECDSACurve: readECDSACurve, + writeECDSACurve: writeECDSACurve +}; + +var assert = __webpack_require__(477); +var asn1 = __webpack_require__(62); +var Buffer = __webpack_require__(215).Buffer; +var algs = __webpack_require__(98); +var utils = __webpack_require__(270); +var Key = __webpack_require__(852); +var PrivateKey = __webpack_require__(502); +var pem = __webpack_require__(268); + +function read(buf, options) { + return (pem.read(buf, options, 'pkcs8')); +} + +function write(key, options) { + return (pem.write(key, options, 'pkcs8')); +} + +/* Helper to read in a single mpint */ +function readMPInt(der, nm) { + assert.strictEqual(der.peek(), asn1.Ber.Integer, + nm + ' is not an Integer'); + return (utils.mpNormalize(der.readString(asn1.Ber.Integer, true))); +} + +function readPkcs8(alg, type, der) { + /* Private keys in pkcs#8 format have a weird extra int */ + if (der.peek() === asn1.Ber.Integer) { + assert.strictEqual(type, 'private', + 'unexpected Integer at start of public key'); + der.readString(asn1.Ber.Integer, true); + } + + der.readSequence(); + var next = der.offset + der.length; + + var oid = der.readOID(); + switch (oid) { + case '1.2.840.113549.1.1.1': + der._offset = next; + if (type === 'public') + return (readPkcs8RSAPublic(der)); + else + return (readPkcs8RSAPrivate(der)); + case '1.2.840.10040.4.1': + if (type === 'public') + return (readPkcs8DSAPublic(der)); + else + return (readPkcs8DSAPrivate(der)); + case '1.2.840.10045.2.1': + if (type === 'public') + return (readPkcs8ECDSAPublic(der)); + else + return (readPkcs8ECDSAPrivate(der)); + case '1.3.101.112': + if (type === 'public') { + return (readPkcs8EdDSAPublic(der)); + } else { + return (readPkcs8EdDSAPrivate(der)); + } + case '1.3.101.110': + if (type === 'public') { + return (readPkcs8X25519Public(der)); + } else { + return (readPkcs8X25519Private(der)); + } + default: + throw (new Error('Unknown key type OID ' + oid)); + } +} + +function readPkcs8RSAPublic(der) { + // bit string sequence + der.readSequence(asn1.Ber.BitString); + der.readByte(); + der.readSequence(); + + // modulus + var n = readMPInt(der, 'modulus'); + var e = readMPInt(der, 'exponent'); + + // now, make the key + var key = { + type: 'rsa', + source: der.originalInput, + parts: [ + { name: 'e', data: e }, + { name: 'n', data: n } + ] + }; + + return (new Key(key)); +} + +function readPkcs8RSAPrivate(der) { + der.readSequence(asn1.Ber.OctetString); + der.readSequence(); + + var ver = readMPInt(der, 'version'); + assert.equal(ver[0], 0x0, 'unknown RSA private key version'); + + // modulus then public exponent + var n = readMPInt(der, 'modulus'); + var e = readMPInt(der, 'public exponent'); + var d = readMPInt(der, 'private exponent'); + var p = readMPInt(der, 'prime1'); + var q = readMPInt(der, 'prime2'); + var dmodp = readMPInt(der, 'exponent1'); + var dmodq = readMPInt(der, 'exponent2'); + var iqmp = readMPInt(der, 'iqmp'); + + // now, make the key + var key = { + type: 'rsa', + parts: [ + { name: 'n', data: n }, + { name: 'e', data: e }, + { name: 'd', data: d }, + { name: 'iqmp', data: iqmp }, + { name: 'p', data: p }, + { name: 'q', data: q }, + { name: 'dmodp', data: dmodp }, + { name: 'dmodq', data: dmodq } + ] + }; + + return (new PrivateKey(key)); +} + +function readPkcs8DSAPublic(der) { + der.readSequence(); + + var p = readMPInt(der, 'p'); + var q = readMPInt(der, 'q'); + var g = readMPInt(der, 'g'); + + // bit string sequence + der.readSequence(asn1.Ber.BitString); + der.readByte(); + + var y = readMPInt(der, 'y'); + + // now, make the key + var key = { + type: 'dsa', + parts: [ + { name: 'p', data: p }, + { name: 'q', data: q }, + { name: 'g', data: g }, + { name: 'y', data: y } + ] + }; + + return (new Key(key)); +} + +function readPkcs8DSAPrivate(der) { + der.readSequence(); + + var p = readMPInt(der, 'p'); + var q = readMPInt(der, 'q'); + var g = readMPInt(der, 'g'); + + der.readSequence(asn1.Ber.OctetString); + var x = readMPInt(der, 'x'); + + /* The pkcs#8 format does not include the public key */ + var y = utils.calculateDSAPublic(g, p, x); + + var key = { + type: 'dsa', + parts: [ + { name: 'p', data: p }, + { name: 'q', data: q }, + { name: 'g', data: g }, + { name: 'y', data: y }, + { name: 'x', data: x } + ] + }; + + return (new PrivateKey(key)); +} + +function readECDSACurve(der) { + var curveName, curveNames; + var j, c, cd; + + if (der.peek() === asn1.Ber.OID) { + var oid = der.readOID(); + + curveNames = Object.keys(algs.curves); + for (j = 0; j < curveNames.length; ++j) { + c = curveNames[j]; + cd = algs.curves[c]; + if (cd.pkcs8oid === oid) { + curveName = c; + break; + } + } + + } else { + // ECParameters sequence + der.readSequence(); + var version = der.readString(asn1.Ber.Integer, true); + assert.strictEqual(version[0], 1, 'ECDSA key not version 1'); + + var curve = {}; + + // FieldID sequence + der.readSequence(); + var fieldTypeOid = der.readOID(); + assert.strictEqual(fieldTypeOid, '1.2.840.10045.1.1', + 'ECDSA key is not from a prime-field'); + var p = curve.p = utils.mpNormalize( + der.readString(asn1.Ber.Integer, true)); + /* + * p always starts with a 1 bit, so count the zeros to get its + * real size. + */ + curve.size = p.length * 8 - utils.countZeros(p); + + // Curve sequence + der.readSequence(); + curve.a = utils.mpNormalize( + der.readString(asn1.Ber.OctetString, true)); + curve.b = utils.mpNormalize( + der.readString(asn1.Ber.OctetString, true)); + if (der.peek() === asn1.Ber.BitString) + curve.s = der.readString(asn1.Ber.BitString, true); + + // Combined Gx and Gy + curve.G = der.readString(asn1.Ber.OctetString, true); + assert.strictEqual(curve.G[0], 0x4, + 'uncompressed G is required'); + + curve.n = utils.mpNormalize( + der.readString(asn1.Ber.Integer, true)); + curve.h = utils.mpNormalize( + der.readString(asn1.Ber.Integer, true)); + assert.strictEqual(curve.h[0], 0x1, 'a cofactor=1 curve is ' + + 'required'); + + curveNames = Object.keys(algs.curves); + var ks = Object.keys(curve); + for (j = 0; j < curveNames.length; ++j) { + c = curveNames[j]; + cd = algs.curves[c]; + var equal = true; + for (var i = 0; i < ks.length; ++i) { + var k = ks[i]; + if (cd[k] === undefined) + continue; + if (typeof (cd[k]) === 'object' && + cd[k].equals !== undefined) { + if (!cd[k].equals(curve[k])) { + equal = false; + break; + } + } else if (Buffer.isBuffer(cd[k])) { + if (cd[k].toString('binary') + !== curve[k].toString('binary')) { + equal = false; + break; + } + } else { + if (cd[k] !== curve[k]) { + equal = false; + break; + } + } + } + if (equal) { + curveName = c; + break; + } + } + } + return (curveName); +} + +function readPkcs8ECDSAPrivate(der) { + var curveName = readECDSACurve(der); + assert.string(curveName, 'a known elliptic curve'); + + der.readSequence(asn1.Ber.OctetString); + der.readSequence(); + + var version = readMPInt(der, 'version'); + assert.equal(version[0], 1, 'unknown version of ECDSA key'); + + var d = der.readString(asn1.Ber.OctetString, true); + var Q; + + if (der.peek() == 0xa0) { + der.readSequence(0xa0); + der._offset += der.length; + } + if (der.peek() == 0xa1) { + der.readSequence(0xa1); + Q = der.readString(asn1.Ber.BitString, true); + Q = utils.ecNormalize(Q); + } + + if (Q === undefined) { + var pub = utils.publicFromPrivateECDSA(curveName, d); + Q = pub.part.Q.data; + } + + var key = { + type: 'ecdsa', + parts: [ + { name: 'curve', data: Buffer.from(curveName) }, + { name: 'Q', data: Q }, + { name: 'd', data: d } + ] + }; + + return (new PrivateKey(key)); +} + +function readPkcs8ECDSAPublic(der) { + var curveName = readECDSACurve(der); + assert.string(curveName, 'a known elliptic curve'); + + var Q = der.readString(asn1.Ber.BitString, true); + Q = utils.ecNormalize(Q); + + var key = { + type: 'ecdsa', + parts: [ + { name: 'curve', data: Buffer.from(curveName) }, + { name: 'Q', data: Q } + ] + }; + + return (new Key(key)); +} + +function readPkcs8EdDSAPublic(der) { + if (der.peek() === 0x00) + der.readByte(); + + var A = utils.readBitString(der); + + var key = { + type: 'ed25519', + parts: [ + { name: 'A', data: utils.zeroPadToLength(A, 32) } + ] + }; + + return (new Key(key)); +} + +function readPkcs8X25519Public(der) { + var A = utils.readBitString(der); + + var key = { + type: 'curve25519', + parts: [ + { name: 'A', data: utils.zeroPadToLength(A, 32) } + ] + }; + + return (new Key(key)); +} + +function readPkcs8EdDSAPrivate(der) { + if (der.peek() === 0x00) + der.readByte(); + + der.readSequence(asn1.Ber.OctetString); + var k = der.readString(asn1.Ber.OctetString, true); + k = utils.zeroPadToLength(k, 32); + + var A; + if (der.peek() === asn1.Ber.BitString) { + A = utils.readBitString(der); + A = utils.zeroPadToLength(A, 32); + } else { + A = utils.calculateED25519Public(k); + } + + var key = { + type: 'ed25519', + parts: [ + { name: 'A', data: utils.zeroPadToLength(A, 32) }, + { name: 'k', data: utils.zeroPadToLength(k, 32) } + ] + }; + + return (new PrivateKey(key)); +} + +function readPkcs8X25519Private(der) { + if (der.peek() === 0x00) + der.readByte(); + + der.readSequence(asn1.Ber.OctetString); + var k = der.readString(asn1.Ber.OctetString, true); + k = utils.zeroPadToLength(k, 32); + + var A = utils.calculateX25519Public(k); + + var key = { + type: 'curve25519', + parts: [ + { name: 'A', data: utils.zeroPadToLength(A, 32) }, + { name: 'k', data: utils.zeroPadToLength(k, 32) } + ] + }; + + return (new PrivateKey(key)); +} + +function pkcs8ToBuffer(key) { + var der = new asn1.BerWriter(); + writePkcs8(der, key); + return (der.buffer); +} + +function writePkcs8(der, key) { + der.startSequence(); + + if (PrivateKey.isPrivateKey(key)) { + var sillyInt = Buffer.from([0]); + der.writeBuffer(sillyInt, asn1.Ber.Integer); + } + + der.startSequence(); + switch (key.type) { + case 'rsa': + der.writeOID('1.2.840.113549.1.1.1'); + if (PrivateKey.isPrivateKey(key)) + writePkcs8RSAPrivate(key, der); + else + writePkcs8RSAPublic(key, der); + break; + case 'dsa': + der.writeOID('1.2.840.10040.4.1'); + if (PrivateKey.isPrivateKey(key)) + writePkcs8DSAPrivate(key, der); + else + writePkcs8DSAPublic(key, der); + break; + case 'ecdsa': + der.writeOID('1.2.840.10045.2.1'); + if (PrivateKey.isPrivateKey(key)) + writePkcs8ECDSAPrivate(key, der); + else + writePkcs8ECDSAPublic(key, der); + break; + case 'ed25519': + der.writeOID('1.3.101.112'); + if (PrivateKey.isPrivateKey(key)) + throw (new Error('Ed25519 private keys in pkcs8 ' + + 'format are not supported')); + writePkcs8EdDSAPublic(key, der); + break; + default: + throw (new Error('Unsupported key type: ' + key.type)); + } + + der.endSequence(); +} + +function writePkcs8RSAPrivate(key, der) { + der.writeNull(); + der.endSequence(); + + der.startSequence(asn1.Ber.OctetString); + der.startSequence(); + + var version = Buffer.from([0]); + der.writeBuffer(version, asn1.Ber.Integer); + + der.writeBuffer(key.part.n.data, asn1.Ber.Integer); + der.writeBuffer(key.part.e.data, asn1.Ber.Integer); + der.writeBuffer(key.part.d.data, asn1.Ber.Integer); + der.writeBuffer(key.part.p.data, asn1.Ber.Integer); + der.writeBuffer(key.part.q.data, asn1.Ber.Integer); + if (!key.part.dmodp || !key.part.dmodq) + utils.addRSAMissing(key); + der.writeBuffer(key.part.dmodp.data, asn1.Ber.Integer); + der.writeBuffer(key.part.dmodq.data, asn1.Ber.Integer); + der.writeBuffer(key.part.iqmp.data, asn1.Ber.Integer); + + der.endSequence(); + der.endSequence(); +} + +function writePkcs8RSAPublic(key, der) { + der.writeNull(); + der.endSequence(); + + der.startSequence(asn1.Ber.BitString); + der.writeByte(0x00); + + der.startSequence(); + der.writeBuffer(key.part.n.data, asn1.Ber.Integer); + der.writeBuffer(key.part.e.data, asn1.Ber.Integer); + der.endSequence(); + + der.endSequence(); +} + +function writePkcs8DSAPrivate(key, der) { + der.startSequence(); + der.writeBuffer(key.part.p.data, asn1.Ber.Integer); + der.writeBuffer(key.part.q.data, asn1.Ber.Integer); + der.writeBuffer(key.part.g.data, asn1.Ber.Integer); + der.endSequence(); + + der.endSequence(); + + der.startSequence(asn1.Ber.OctetString); + der.writeBuffer(key.part.x.data, asn1.Ber.Integer); + der.endSequence(); +} + +function writePkcs8DSAPublic(key, der) { + der.startSequence(); + der.writeBuffer(key.part.p.data, asn1.Ber.Integer); + der.writeBuffer(key.part.q.data, asn1.Ber.Integer); + der.writeBuffer(key.part.g.data, asn1.Ber.Integer); + der.endSequence(); + der.endSequence(); + + der.startSequence(asn1.Ber.BitString); + der.writeByte(0x00); + der.writeBuffer(key.part.y.data, asn1.Ber.Integer); + der.endSequence(); +} + +function writeECDSACurve(key, der) { + var curve = algs.curves[key.curve]; + if (curve.pkcs8oid) { + /* This one has a name in pkcs#8, so just write the oid */ + der.writeOID(curve.pkcs8oid); + + } else { + // ECParameters sequence + der.startSequence(); + + var version = Buffer.from([1]); + der.writeBuffer(version, asn1.Ber.Integer); + + // FieldID sequence + der.startSequence(); + der.writeOID('1.2.840.10045.1.1'); // prime-field + der.writeBuffer(curve.p, asn1.Ber.Integer); + der.endSequence(); + + // Curve sequence + der.startSequence(); + var a = curve.p; + if (a[0] === 0x0) + a = a.slice(1); + der.writeBuffer(a, asn1.Ber.OctetString); + der.writeBuffer(curve.b, asn1.Ber.OctetString); + der.writeBuffer(curve.s, asn1.Ber.BitString); + der.endSequence(); + + der.writeBuffer(curve.G, asn1.Ber.OctetString); + der.writeBuffer(curve.n, asn1.Ber.Integer); + var h = curve.h; + if (!h) { + h = Buffer.from([1]); + } + der.writeBuffer(h, asn1.Ber.Integer); + + // ECParameters + der.endSequence(); + } +} + +function writePkcs8ECDSAPublic(key, der) { + writeECDSACurve(key, der); + der.endSequence(); + + var Q = utils.ecNormalize(key.part.Q.data, true); + der.writeBuffer(Q, asn1.Ber.BitString); +} + +function writePkcs8ECDSAPrivate(key, der) { + writeECDSACurve(key, der); + der.endSequence(); + + der.startSequence(asn1.Ber.OctetString); + der.startSequence(); + + var version = Buffer.from([1]); + der.writeBuffer(version, asn1.Ber.Integer); + + der.writeBuffer(key.part.d.data, asn1.Ber.OctetString); + + der.startSequence(0xa1); + var Q = utils.ecNormalize(key.part.Q.data, true); + der.writeBuffer(Q, asn1.Ber.BitString); + der.endSequence(); + + der.endSequence(); + der.endSequence(); +} + +function writePkcs8EdDSAPublic(key, der) { + der.endSequence(); + + utils.writeBitString(der, key.part.A.data); +} + +function writePkcs8EdDSAPrivate(key, der) { + der.endSequence(); + + var k = utils.mpNormalize(key.part.k.data, true); + der.startSequence(asn1.Ber.OctetString); + der.writeBuffer(k, asn1.Ber.OctetString); + der.endSequence(); +} + + +/***/ }), + +/***/ 721: +/***/ (function(module) { + +"use strict"; + + +function formatHostname (hostname) { + // canonicalize the hostname, so that 'oogle.com' won't match 'google.com' + return hostname.replace(/^\.*/, '.').toLowerCase() +} + +function parseNoProxyZone (zone) { + zone = zone.trim().toLowerCase() + + var zoneParts = zone.split(':', 2) + var zoneHost = formatHostname(zoneParts[0]) + var zonePort = zoneParts[1] + var hasPort = zone.indexOf(':') > -1 + + return {hostname: zoneHost, port: zonePort, hasPort: hasPort} +} + +function uriInNoProxy (uri, noProxy) { + var port = uri.port || (uri.protocol === 'https:' ? '443' : '80') + var hostname = formatHostname(uri.hostname) + var noProxyList = noProxy.split(',') + + // iterate through the noProxyList until it finds a match. + return noProxyList.map(parseNoProxyZone).some(function (noProxyZone) { + var isMatchedAt = hostname.indexOf(noProxyZone.hostname) + var hostnameMatched = ( + isMatchedAt > -1 && + (isMatchedAt === hostname.length - noProxyZone.hostname.length) + ) + + if (noProxyZone.hasPort) { + return (port === noProxyZone.port) && hostnameMatched + } + + return hostnameMatched + }) +} + +function getProxyFromURI (uri) { + // Decide the proper request proxy to use based on the request URI object and the + // environmental variables (NO_PROXY, HTTP_PROXY, etc.) + // respect NO_PROXY environment variables (see: https://lynx.invisible-island.net/lynx2.8.7/breakout/lynx_help/keystrokes/environments.html) + + var noProxy = process.env.NO_PROXY || process.env.no_proxy || '' + + // if the noProxy is a wildcard then return null + + if (noProxy === '*') { + return null + } + + // if the noProxy is not empty and the uri is found return null + + if (noProxy !== '' && uriInNoProxy(uri, noProxy)) { + return null + } + + // Check for HTTP or HTTPS Proxy in environment Else default to null + + if (uri.protocol === 'http:') { + return process.env.HTTP_PROXY || + process.env.http_proxy || null + } + + if (uri.protocol === 'https:') { + return process.env.HTTPS_PROXY || + process.env.https_proxy || + process.env.HTTP_PROXY || + process.env.http_proxy || null + } + + // if none of that works, return null + // (What uri protocol are you using then?) + + return null +} + +module.exports = getProxyFromURI + + +/***/ }), + +/***/ 722: +/***/ (function(module) { + +/** + * Convert array of 16 byte values to UUID string format of the form: + * XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX + */ +var byteToHex = []; +for (var i = 0; i < 256; ++i) { + byteToHex[i] = (i + 0x100).toString(16).substr(1); +} + +function bytesToUuid(buf, offset) { + var i = offset || 0; + var bth = byteToHex; + // join used to fix memory issue caused by concatenation: https://bugs.chromium.org/p/v8/issues/detail?id=3175#c4 + return ([ + bth[buf[i++]], bth[buf[i++]], + bth[buf[i++]], bth[buf[i++]], '-', + bth[buf[i++]], bth[buf[i++]], '-', + bth[buf[i++]], bth[buf[i++]], '-', + bth[buf[i++]], bth[buf[i++]], '-', + bth[buf[i++]], bth[buf[i++]], + bth[buf[i++]], bth[buf[i++]], + bth[buf[i++]], bth[buf[i++]] + ]).join(''); +} + +module.exports = bytesToUuid; + + +/***/ }), + +/***/ 729: +/***/ (function(module, __unusedexports, __webpack_require__) { + +// Basic Javascript Elliptic Curve implementation +// Ported loosely from BouncyCastle's Java EC code +// Only Fp curves implemented for now + +// Requires jsbn.js and jsbn2.js +var BigInteger = __webpack_require__(242).BigInteger +var Barrett = BigInteger.prototype.Barrett + +// ---------------- +// ECFieldElementFp + +// constructor +function ECFieldElementFp(q,x) { + this.x = x; + // TODO if(x.compareTo(q) >= 0) error + this.q = q; +} + +function feFpEquals(other) { + if(other == this) return true; + return (this.q.equals(other.q) && this.x.equals(other.x)); +} + +function feFpToBigInteger() { + return this.x; +} + +function feFpNegate() { + return new ECFieldElementFp(this.q, this.x.negate().mod(this.q)); +} + +function feFpAdd(b) { + return new ECFieldElementFp(this.q, this.x.add(b.toBigInteger()).mod(this.q)); +} + +function feFpSubtract(b) { + return new ECFieldElementFp(this.q, this.x.subtract(b.toBigInteger()).mod(this.q)); +} + +function feFpMultiply(b) { + return new ECFieldElementFp(this.q, this.x.multiply(b.toBigInteger()).mod(this.q)); +} + +function feFpSquare() { + return new ECFieldElementFp(this.q, this.x.square().mod(this.q)); +} + +function feFpDivide(b) { + return new ECFieldElementFp(this.q, this.x.multiply(b.toBigInteger().modInverse(this.q)).mod(this.q)); +} + +ECFieldElementFp.prototype.equals = feFpEquals; +ECFieldElementFp.prototype.toBigInteger = feFpToBigInteger; +ECFieldElementFp.prototype.negate = feFpNegate; +ECFieldElementFp.prototype.add = feFpAdd; +ECFieldElementFp.prototype.subtract = feFpSubtract; +ECFieldElementFp.prototype.multiply = feFpMultiply; +ECFieldElementFp.prototype.square = feFpSquare; +ECFieldElementFp.prototype.divide = feFpDivide; + +// ---------------- +// ECPointFp + +// constructor +function ECPointFp(curve,x,y,z) { + this.curve = curve; + this.x = x; + this.y = y; + // Projective coordinates: either zinv == null or z * zinv == 1 + // z and zinv are just BigIntegers, not fieldElements + if(z == null) { + this.z = BigInteger.ONE; + } + else { + this.z = z; + } + this.zinv = null; + //TODO: compression flag +} + +function pointFpGetX() { + if(this.zinv == null) { + this.zinv = this.z.modInverse(this.curve.q); + } + var r = this.x.toBigInteger().multiply(this.zinv); + this.curve.reduce(r); + return this.curve.fromBigInteger(r); +} + +function pointFpGetY() { + if(this.zinv == null) { + this.zinv = this.z.modInverse(this.curve.q); + } + var r = this.y.toBigInteger().multiply(this.zinv); + this.curve.reduce(r); + return this.curve.fromBigInteger(r); +} + +function pointFpEquals(other) { + if(other == this) return true; + if(this.isInfinity()) return other.isInfinity(); + if(other.isInfinity()) return this.isInfinity(); + var u, v; + // u = Y2 * Z1 - Y1 * Z2 + u = other.y.toBigInteger().multiply(this.z).subtract(this.y.toBigInteger().multiply(other.z)).mod(this.curve.q); + if(!u.equals(BigInteger.ZERO)) return false; + // v = X2 * Z1 - X1 * Z2 + v = other.x.toBigInteger().multiply(this.z).subtract(this.x.toBigInteger().multiply(other.z)).mod(this.curve.q); + return v.equals(BigInteger.ZERO); +} + +function pointFpIsInfinity() { + if((this.x == null) && (this.y == null)) return true; + return this.z.equals(BigInteger.ZERO) && !this.y.toBigInteger().equals(BigInteger.ZERO); +} + +function pointFpNegate() { + return new ECPointFp(this.curve, this.x, this.y.negate(), this.z); +} + +function pointFpAdd(b) { + if(this.isInfinity()) return b; + if(b.isInfinity()) return this; + + // u = Y2 * Z1 - Y1 * Z2 + var u = b.y.toBigInteger().multiply(this.z).subtract(this.y.toBigInteger().multiply(b.z)).mod(this.curve.q); + // v = X2 * Z1 - X1 * Z2 + var v = b.x.toBigInteger().multiply(this.z).subtract(this.x.toBigInteger().multiply(b.z)).mod(this.curve.q); + + if(BigInteger.ZERO.equals(v)) { + if(BigInteger.ZERO.equals(u)) { + return this.twice(); // this == b, so double + } + return this.curve.getInfinity(); // this = -b, so infinity + } + + var THREE = new BigInteger("3"); + var x1 = this.x.toBigInteger(); + var y1 = this.y.toBigInteger(); + var x2 = b.x.toBigInteger(); + var y2 = b.y.toBigInteger(); + + var v2 = v.square(); + var v3 = v2.multiply(v); + var x1v2 = x1.multiply(v2); + var zu2 = u.square().multiply(this.z); + + // x3 = v * (z2 * (z1 * u^2 - 2 * x1 * v^2) - v^3) + var x3 = zu2.subtract(x1v2.shiftLeft(1)).multiply(b.z).subtract(v3).multiply(v).mod(this.curve.q); + // y3 = z2 * (3 * x1 * u * v^2 - y1 * v^3 - z1 * u^3) + u * v^3 + var y3 = x1v2.multiply(THREE).multiply(u).subtract(y1.multiply(v3)).subtract(zu2.multiply(u)).multiply(b.z).add(u.multiply(v3)).mod(this.curve.q); + // z3 = v^3 * z1 * z2 + var z3 = v3.multiply(this.z).multiply(b.z).mod(this.curve.q); + + return new ECPointFp(this.curve, this.curve.fromBigInteger(x3), this.curve.fromBigInteger(y3), z3); +} + +function pointFpTwice() { + if(this.isInfinity()) return this; + if(this.y.toBigInteger().signum() == 0) return this.curve.getInfinity(); + + // TODO: optimized handling of constants + var THREE = new BigInteger("3"); + var x1 = this.x.toBigInteger(); + var y1 = this.y.toBigInteger(); + + var y1z1 = y1.multiply(this.z); + var y1sqz1 = y1z1.multiply(y1).mod(this.curve.q); + var a = this.curve.a.toBigInteger(); + + // w = 3 * x1^2 + a * z1^2 + var w = x1.square().multiply(THREE); + if(!BigInteger.ZERO.equals(a)) { + w = w.add(this.z.square().multiply(a)); + } + w = w.mod(this.curve.q); + //this.curve.reduce(w); + // x3 = 2 * y1 * z1 * (w^2 - 8 * x1 * y1^2 * z1) + var x3 = w.square().subtract(x1.shiftLeft(3).multiply(y1sqz1)).shiftLeft(1).multiply(y1z1).mod(this.curve.q); + // y3 = 4 * y1^2 * z1 * (3 * w * x1 - 2 * y1^2 * z1) - w^3 + var y3 = w.multiply(THREE).multiply(x1).subtract(y1sqz1.shiftLeft(1)).shiftLeft(2).multiply(y1sqz1).subtract(w.square().multiply(w)).mod(this.curve.q); + // z3 = 8 * (y1 * z1)^3 + var z3 = y1z1.square().multiply(y1z1).shiftLeft(3).mod(this.curve.q); + + return new ECPointFp(this.curve, this.curve.fromBigInteger(x3), this.curve.fromBigInteger(y3), z3); +} + +// Simple NAF (Non-Adjacent Form) multiplication algorithm +// TODO: modularize the multiplication algorithm +function pointFpMultiply(k) { + if(this.isInfinity()) return this; + if(k.signum() == 0) return this.curve.getInfinity(); + + var e = k; + var h = e.multiply(new BigInteger("3")); + + var neg = this.negate(); + var R = this; + + var i; + for(i = h.bitLength() - 2; i > 0; --i) { + R = R.twice(); + + var hBit = h.testBit(i); + var eBit = e.testBit(i); + + if (hBit != eBit) { + R = R.add(hBit ? this : neg); + } + } + + return R; +} + +// Compute this*j + x*k (simultaneous multiplication) +function pointFpMultiplyTwo(j,x,k) { + var i; + if(j.bitLength() > k.bitLength()) + i = j.bitLength() - 1; + else + i = k.bitLength() - 1; + + var R = this.curve.getInfinity(); + var both = this.add(x); + while(i >= 0) { + R = R.twice(); + if(j.testBit(i)) { + if(k.testBit(i)) { + R = R.add(both); + } + else { + R = R.add(this); + } + } + else { + if(k.testBit(i)) { + R = R.add(x); + } + } + --i; + } + + return R; +} + +ECPointFp.prototype.getX = pointFpGetX; +ECPointFp.prototype.getY = pointFpGetY; +ECPointFp.prototype.equals = pointFpEquals; +ECPointFp.prototype.isInfinity = pointFpIsInfinity; +ECPointFp.prototype.negate = pointFpNegate; +ECPointFp.prototype.add = pointFpAdd; +ECPointFp.prototype.twice = pointFpTwice; +ECPointFp.prototype.multiply = pointFpMultiply; +ECPointFp.prototype.multiplyTwo = pointFpMultiplyTwo; + +// ---------------- +// ECCurveFp + +// constructor +function ECCurveFp(q,a,b) { + this.q = q; + this.a = this.fromBigInteger(a); + this.b = this.fromBigInteger(b); + this.infinity = new ECPointFp(this, null, null); + this.reducer = new Barrett(this.q); +} + +function curveFpGetQ() { + return this.q; +} + +function curveFpGetA() { + return this.a; +} + +function curveFpGetB() { + return this.b; +} + +function curveFpEquals(other) { + if(other == this) return true; + return(this.q.equals(other.q) && this.a.equals(other.a) && this.b.equals(other.b)); +} + +function curveFpGetInfinity() { + return this.infinity; +} + +function curveFpFromBigInteger(x) { + return new ECFieldElementFp(this.q, x); +} + +function curveReduce(x) { + this.reducer.reduce(x); +} + +// for now, work with hex strings because they're easier in JS +function curveFpDecodePointHex(s) { + switch(parseInt(s.substr(0,2), 16)) { // first byte + case 0: + return this.infinity; + case 2: + case 3: + // point compression not supported yet + return null; + case 4: + case 6: + case 7: + var len = (s.length - 2) / 2; + var xHex = s.substr(2, len); + var yHex = s.substr(len+2, len); + + return new ECPointFp(this, + this.fromBigInteger(new BigInteger(xHex, 16)), + this.fromBigInteger(new BigInteger(yHex, 16))); + + default: // unsupported + return null; + } +} + +function curveFpEncodePointHex(p) { + if (p.isInfinity()) return "00"; + var xHex = p.getX().toBigInteger().toString(16); + var yHex = p.getY().toBigInteger().toString(16); + var oLen = this.getQ().toString(16).length; + if ((oLen % 2) != 0) oLen++; + while (xHex.length < oLen) { + xHex = "0" + xHex; + } + while (yHex.length < oLen) { + yHex = "0" + yHex; + } + return "04" + xHex + yHex; +} + +ECCurveFp.prototype.getQ = curveFpGetQ; +ECCurveFp.prototype.getA = curveFpGetA; +ECCurveFp.prototype.getB = curveFpGetB; +ECCurveFp.prototype.equals = curveFpEquals; +ECCurveFp.prototype.getInfinity = curveFpGetInfinity; +ECCurveFp.prototype.fromBigInteger = curveFpFromBigInteger; +ECCurveFp.prototype.reduce = curveReduce; +//ECCurveFp.prototype.decodePointHex = curveFpDecodePointHex; +ECCurveFp.prototype.encodePointHex = curveFpEncodePointHex; + +// from: https://github.com/kaielvin/jsbn-ec-point-compression +ECCurveFp.prototype.decodePointHex = function(s) +{ + var yIsEven; + switch(parseInt(s.substr(0,2), 16)) { // first byte + case 0: + return this.infinity; + case 2: + yIsEven = false; + case 3: + if(yIsEven == undefined) yIsEven = true; + var len = s.length - 2; + var xHex = s.substr(2, len); + var x = this.fromBigInteger(new BigInteger(xHex,16)); + var alpha = x.multiply(x.square().add(this.getA())).add(this.getB()); + var beta = alpha.sqrt(); + + if (beta == null) throw "Invalid point compression"; + + var betaValue = beta.toBigInteger(); + if (betaValue.testBit(0) != yIsEven) + { + // Use the other root + beta = this.fromBigInteger(this.getQ().subtract(betaValue)); + } + return new ECPointFp(this,x,beta); + case 4: + case 6: + case 7: + var len = (s.length - 2) / 2; + var xHex = s.substr(2, len); + var yHex = s.substr(len+2, len); + + return new ECPointFp(this, + this.fromBigInteger(new BigInteger(xHex, 16)), + this.fromBigInteger(new BigInteger(yHex, 16))); + + default: // unsupported + return null; + } +} +ECCurveFp.prototype.encodeCompressedPointHex = function(p) +{ + if (p.isInfinity()) return "00"; + var xHex = p.getX().toBigInteger().toString(16); + var oLen = this.getQ().toString(16).length; + if ((oLen % 2) != 0) oLen++; + while (xHex.length < oLen) + xHex = "0" + xHex; + var yPrefix; + if(p.getY().toBigInteger().isEven()) yPrefix = "02"; + else yPrefix = "03"; + + return yPrefix + xHex; +} + + +ECFieldElementFp.prototype.getR = function() +{ + if(this.r != undefined) return this.r; + + this.r = null; + var bitLength = this.q.bitLength(); + if (bitLength > 128) + { + var firstWord = this.q.shiftRight(bitLength - 64); + if (firstWord.intValue() == -1) + { + this.r = BigInteger.ONE.shiftLeft(bitLength).subtract(this.q); + } + } + return this.r; +} +ECFieldElementFp.prototype.modMult = function(x1,x2) +{ + return this.modReduce(x1.multiply(x2)); +} +ECFieldElementFp.prototype.modReduce = function(x) +{ + if (this.getR() != null) + { + var qLen = q.bitLength(); + while (x.bitLength() > (qLen + 1)) + { + var u = x.shiftRight(qLen); + var v = x.subtract(u.shiftLeft(qLen)); + if (!this.getR().equals(BigInteger.ONE)) + { + u = u.multiply(this.getR()); + } + x = u.add(v); + } + while (x.compareTo(q) >= 0) + { + x = x.subtract(q); + } + } + else + { + x = x.mod(q); + } + return x; +} +ECFieldElementFp.prototype.sqrt = function() +{ + if (!this.q.testBit(0)) throw "unsupported"; + + // p mod 4 == 3 + if (this.q.testBit(1)) + { + var z = new ECFieldElementFp(this.q,this.x.modPow(this.q.shiftRight(2).add(BigInteger.ONE),this.q)); + return z.square().equals(this) ? z : null; + } + + // p mod 4 == 1 + var qMinusOne = this.q.subtract(BigInteger.ONE); + + var legendreExponent = qMinusOne.shiftRight(1); + if (!(this.x.modPow(legendreExponent, this.q).equals(BigInteger.ONE))) + { + return null; + } + + var u = qMinusOne.shiftRight(2); + var k = u.shiftLeft(1).add(BigInteger.ONE); + + var Q = this.x; + var fourQ = modDouble(modDouble(Q)); + + var U, V; + do + { + var P; + do + { + P = new BigInteger(this.q.bitLength(), new SecureRandom()); + } + while (P.compareTo(this.q) >= 0 + || !(P.multiply(P).subtract(fourQ).modPow(legendreExponent, this.q).equals(qMinusOne))); + + var result = this.lucasSequence(P, Q, k); + U = result[0]; + V = result[1]; + + if (this.modMult(V, V).equals(fourQ)) + { + // Integer division by 2, mod q + if (V.testBit(0)) + { + V = V.add(q); + } + + V = V.shiftRight(1); + + return new ECFieldElementFp(q,V); + } + } + while (U.equals(BigInteger.ONE) || U.equals(qMinusOne)); + + return null; +} +ECFieldElementFp.prototype.lucasSequence = function(P,Q,k) +{ + var n = k.bitLength(); + var s = k.getLowestSetBit(); + + var Uh = BigInteger.ONE; + var Vl = BigInteger.TWO; + var Vh = P; + var Ql = BigInteger.ONE; + var Qh = BigInteger.ONE; + + for (var j = n - 1; j >= s + 1; --j) + { + Ql = this.modMult(Ql, Qh); + + if (k.testBit(j)) + { + Qh = this.modMult(Ql, Q); + Uh = this.modMult(Uh, Vh); + Vl = this.modReduce(Vh.multiply(Vl).subtract(P.multiply(Ql))); + Vh = this.modReduce(Vh.multiply(Vh).subtract(Qh.shiftLeft(1))); + } + else + { + Qh = Ql; + Uh = this.modReduce(Uh.multiply(Vl).subtract(Ql)); + Vh = this.modReduce(Vh.multiply(Vl).subtract(P.multiply(Ql))); + Vl = this.modReduce(Vl.multiply(Vl).subtract(Ql.shiftLeft(1))); + } + } + + Ql = this.modMult(Ql, Qh); + Qh = this.modMult(Ql, Q); + Uh = this.modReduce(Uh.multiply(Vl).subtract(Ql)); + Vl = this.modReduce(Vh.multiply(Vl).subtract(P.multiply(Ql))); + Ql = this.modMult(Ql, Qh); + + for (var j = 1; j <= s; ++j) + { + Uh = this.modMult(Uh, Vl); + Vl = this.modReduce(Vl.multiply(Vl).subtract(Ql.shiftLeft(1))); + Ql = this.modMult(Ql, Ql); + } + + return [ Uh, Vl ]; +} + +var exports = { + ECCurveFp: ECCurveFp, + ECPointFp: ECPointFp, + ECFieldElementFp: ECFieldElementFp +} + +module.exports = exports + + +/***/ }), + +/***/ 733: +/***/ (function(module, __unusedexports, __webpack_require__) { + +// Copyright 2011 Mark Cavage All rights reserved. + +var assert = __webpack_require__(357); +var Buffer = __webpack_require__(215).Buffer; + +var ASN1 = __webpack_require__(362); +var errors = __webpack_require__(584); + + +// --- Globals + +var newInvalidAsn1Error = errors.newInvalidAsn1Error; + + + +// --- API + +function Reader(data) { + if (!data || !Buffer.isBuffer(data)) + throw new TypeError('data must be a node Buffer'); + + this._buf = data; + this._size = data.length; + + // These hold the "current" state + this._len = 0; + this._offset = 0; +} + +Object.defineProperty(Reader.prototype, 'length', { + enumerable: true, + get: function () { return (this._len); } +}); + +Object.defineProperty(Reader.prototype, 'offset', { + enumerable: true, + get: function () { return (this._offset); } +}); + +Object.defineProperty(Reader.prototype, 'remain', { + get: function () { return (this._size - this._offset); } +}); + +Object.defineProperty(Reader.prototype, 'buffer', { + get: function () { return (this._buf.slice(this._offset)); } +}); + + +/** + * Reads a single byte and advances offset; you can pass in `true` to make this + * a "peek" operation (i.e., get the byte, but don't advance the offset). + * + * @param {Boolean} peek true means don't move offset. + * @return {Number} the next byte, null if not enough data. + */ +Reader.prototype.readByte = function (peek) { + if (this._size - this._offset < 1) + return null; + + var b = this._buf[this._offset] & 0xff; + + if (!peek) + this._offset += 1; + + return b; +}; + + +Reader.prototype.peek = function () { + return this.readByte(true); +}; + + +/** + * Reads a (potentially) variable length off the BER buffer. This call is + * not really meant to be called directly, as callers have to manipulate + * the internal buffer afterwards. + * + * As a result of this call, you can call `Reader.length`, until the + * next thing called that does a readLength. + * + * @return {Number} the amount of offset to advance the buffer. + * @throws {InvalidAsn1Error} on bad ASN.1 + */ +Reader.prototype.readLength = function (offset) { + if (offset === undefined) + offset = this._offset; + + if (offset >= this._size) + return null; + + var lenB = this._buf[offset++] & 0xff; + if (lenB === null) + return null; + + if ((lenB & 0x80) === 0x80) { + lenB &= 0x7f; + + if (lenB === 0) + throw newInvalidAsn1Error('Indefinite length not supported'); + + if (lenB > 4) + throw newInvalidAsn1Error('encoding too long'); + + if (this._size - offset < lenB) + return null; + + this._len = 0; + for (var i = 0; i < lenB; i++) + this._len = (this._len << 8) + (this._buf[offset++] & 0xff); + + } else { + // Wasn't a variable length + this._len = lenB; + } + + return offset; +}; + + +/** + * Parses the next sequence in this BER buffer. + * + * To get the length of the sequence, call `Reader.length`. + * + * @return {Number} the sequence's tag. + */ +Reader.prototype.readSequence = function (tag) { + var seq = this.peek(); + if (seq === null) + return null; + if (tag !== undefined && tag !== seq) + throw newInvalidAsn1Error('Expected 0x' + tag.toString(16) + + ': got 0x' + seq.toString(16)); + + var o = this.readLength(this._offset + 1); // stored in `length` + if (o === null) + return null; + + this._offset = o; + return seq; +}; + + +Reader.prototype.readInt = function () { + return this._readTag(ASN1.Integer); +}; + + +Reader.prototype.readBoolean = function () { + return (this._readTag(ASN1.Boolean) === 0 ? false : true); +}; + + +Reader.prototype.readEnumeration = function () { + return this._readTag(ASN1.Enumeration); +}; + + +Reader.prototype.readString = function (tag, retbuf) { + if (!tag) + tag = ASN1.OctetString; + + var b = this.peek(); + if (b === null) + return null; + + if (b !== tag) + throw newInvalidAsn1Error('Expected 0x' + tag.toString(16) + + ': got 0x' + b.toString(16)); + + var o = this.readLength(this._offset + 1); // stored in `length` + + if (o === null) + return null; + + if (this.length > this._size - o) + return null; + + this._offset = o; + + if (this.length === 0) + return retbuf ? Buffer.alloc(0) : ''; + + var str = this._buf.slice(this._offset, this._offset + this.length); + this._offset += this.length; + + return retbuf ? str : str.toString('utf8'); +}; + +Reader.prototype.readOID = function (tag) { + if (!tag) + tag = ASN1.OID; + + var b = this.readString(tag, true); + if (b === null) + return null; + + var values = []; + var value = 0; + + for (var i = 0; i < b.length; i++) { + var byte = b[i] & 0xff; + + value <<= 7; + value += byte & 0x7f; + if ((byte & 0x80) === 0) { + values.push(value); + value = 0; + } + } + + value = values.shift(); + values.unshift(value % 40); + values.unshift((value / 40) >> 0); + + return values.join('.'); +}; + + +Reader.prototype._readTag = function (tag) { + assert.ok(tag !== undefined); + + var b = this.peek(); + + if (b === null) + return null; + + if (b !== tag) + throw newInvalidAsn1Error('Expected 0x' + tag.toString(16) + + ': got 0x' + b.toString(16)); + + var o = this.readLength(this._offset + 1); // stored in `length` + if (o === null) + return null; + + if (this.length > 4) + throw newInvalidAsn1Error('Integer too long: ' + this.length); + + if (this.length > this._size - o) + return null; + this._offset = o; + + var fb = this._buf[this._offset]; + var value = 0; + + for (var i = 0; i < this.length; i++) { + value <<= 8; + value |= (this._buf[this._offset++] & 0xff); + } + + if ((fb & 0x80) === 0x80 && i !== 4) + value -= (1 << (i * 8)); + + return value >> 0; +}; + + + +// --- Exported API + +module.exports = Reader; + + +/***/ }), + +/***/ 740: +/***/ (function(module) { + +module.exports = {"$id":"postData.json#","$schema":"http://json-schema.org/draft-06/schema#","type":"object","optional":true,"required":["mimeType"],"properties":{"mimeType":{"type":"string"},"text":{"type":"string"},"params":{"type":"array","required":["name"],"properties":{"name":{"type":"string"},"value":{"type":"string"},"fileName":{"type":"string"},"contentType":{"type":"string"},"comment":{"type":"string"}}},"comment":{"type":"string"}}}; + +/***/ }), + +/***/ 741: +/***/ (function(module) { + +"use strict"; + + +module.exports = function (data, opts) { + if (!opts) opts = {}; + if (typeof opts === 'function') opts = { cmp: opts }; + var cycles = (typeof opts.cycles === 'boolean') ? opts.cycles : false; + + var cmp = opts.cmp && (function (f) { + return function (node) { + return function (a, b) { + var aobj = { key: a, value: node[a] }; + var bobj = { key: b, value: node[b] }; + return f(aobj, bobj); + }; + }; + })(opts.cmp); + + var seen = []; + return (function stringify (node) { + if (node && node.toJSON && typeof node.toJSON === 'function') { + node = node.toJSON(); + } + + if (node === undefined) return; + if (typeof node == 'number') return isFinite(node) ? '' + node : 'null'; + if (typeof node !== 'object') return JSON.stringify(node); + + var i, out; + if (Array.isArray(node)) { + out = '['; + for (i = 0; i < node.length; i++) { + if (i) out += ','; + out += stringify(node[i]) || 'null'; + } + return out + ']'; + } + + if (node === null) return 'null'; + + if (seen.indexOf(node) !== -1) { + if (cycles) return JSON.stringify('__cycle__'); + throw new TypeError('Converting circular structure to JSON'); + } + + var seenIndex = seen.push(node) - 1; + var keys = Object.keys(node).sort(cmp && cmp(node)); + out = ''; + for (i = 0; i < keys.length; i++) { + var key = keys[i]; + var value = stringify(node[key]); + + if (!value) continue; + if (out) out += ','; + out += JSON.stringify(key) + ':' + value; + } + seen.splice(seenIndex, 1); + return '{' + out + '}'; + })(data); +}; + + +/***/ }), + +/***/ 742: +/***/ (function(module) { + +// Generated by CoffeeScript 1.12.2 +(function() { + var getNanoSeconds, hrtime, loadTime, moduleLoadTime, nodeLoadTime, upTime; + + if ((typeof performance !== "undefined" && performance !== null) && performance.now) { + module.exports = function() { + return performance.now(); + }; + } else if ((typeof process !== "undefined" && process !== null) && process.hrtime) { + module.exports = function() { + return (getNanoSeconds() - nodeLoadTime) / 1e6; + }; + hrtime = process.hrtime; + getNanoSeconds = function() { + var hr; + hr = hrtime(); + return hr[0] * 1e9 + hr[1]; + }; + moduleLoadTime = getNanoSeconds(); + upTime = process.uptime() * 1e9; + nodeLoadTime = moduleLoadTime - upTime; + } else if (Date.now) { + module.exports = function() { + return Date.now() - loadTime; + }; + loadTime = Date.now(); + } else { + module.exports = function() { + return new Date().getTime() - loadTime; + }; + loadTime = new Date().getTime(); + } + +}).call(this); + +//# sourceMappingURL=performance-now.js.map + + +/***/ }), + +/***/ 744: +/***/ (function(module) { + +module.exports = {"$id":"page.json#","$schema":"http://json-schema.org/draft-06/schema#","type":"object","optional":true,"required":["startedDateTime","id","title","pageTimings"],"properties":{"startedDateTime":{"type":"string","format":"date-time","pattern":"^(\\d{4})(-)?(\\d\\d)(-)?(\\d\\d)(T)?(\\d\\d)(:)?(\\d\\d)(:)?(\\d\\d)(\\.\\d+)?(Z|([+-])(\\d\\d)(:)?(\\d\\d))"},"id":{"type":"string","unique":true},"title":{"type":"string"},"pageTimings":{"$ref":"pageTimings.json#"},"comment":{"type":"string"}}}; + +/***/ }), + +/***/ 747: +/***/ (function(module) { + +module.exports = require("fs"); + +/***/ }), + +/***/ 750: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; +/*eslint no-var:0, prefer-arrow-callback: 0, object-shorthand: 0 */ + + + +var Punycode = __webpack_require__(213); + + +var internals = {}; + + +// +// Read rules from file. +// +internals.rules = __webpack_require__(820).map(function (rule) { + + return { + rule: rule, + suffix: rule.replace(/^(\*\.|\!)/, ''), + punySuffix: -1, + wildcard: rule.charAt(0) === '*', + exception: rule.charAt(0) === '!' + }; +}); + + +// +// Check is given string ends with `suffix`. +// +internals.endsWith = function (str, suffix) { + + return str.indexOf(suffix, str.length - suffix.length) !== -1; +}; + + +// +// Find rule for a given domain. +// +internals.findRule = function (domain) { + + var punyDomain = Punycode.toASCII(domain); + return internals.rules.reduce(function (memo, rule) { + + if (rule.punySuffix === -1){ + rule.punySuffix = Punycode.toASCII(rule.suffix); + } + if (!internals.endsWith(punyDomain, '.' + rule.punySuffix) && punyDomain !== rule.punySuffix) { + return memo; + } + // This has been commented out as it never seems to run. This is because + // sub tlds always appear after their parents and we never find a shorter + // match. + //if (memo) { + // var memoSuffix = Punycode.toASCII(memo.suffix); + // if (memoSuffix.length >= punySuffix.length) { + // return memo; + // } + //} + return rule; + }, null); +}; + + +// +// Error codes and messages. +// +exports.errorCodes = { + DOMAIN_TOO_SHORT: 'Domain name too short.', + DOMAIN_TOO_LONG: 'Domain name too long. It should be no more than 255 chars.', + LABEL_STARTS_WITH_DASH: 'Domain name label can not start with a dash.', + LABEL_ENDS_WITH_DASH: 'Domain name label can not end with a dash.', + LABEL_TOO_LONG: 'Domain name label should be at most 63 chars long.', + LABEL_TOO_SHORT: 'Domain name label should be at least 1 character long.', + LABEL_INVALID_CHARS: 'Domain name label can only contain alphanumeric characters or dashes.' +}; + + +// +// Validate domain name and throw if not valid. +// +// From wikipedia: +// +// Hostnames are composed of series of labels concatenated with dots, as are all +// domain names. Each label must be between 1 and 63 characters long, and the +// entire hostname (including the delimiting dots) has a maximum of 255 chars. +// +// Allowed chars: +// +// * `a-z` +// * `0-9` +// * `-` but not as a starting or ending character +// * `.` as a separator for the textual portions of a domain name +// +// * http://en.wikipedia.org/wiki/Domain_name +// * http://en.wikipedia.org/wiki/Hostname +// +internals.validate = function (input) { + + // Before we can validate we need to take care of IDNs with unicode chars. + var ascii = Punycode.toASCII(input); + + if (ascii.length < 1) { + return 'DOMAIN_TOO_SHORT'; + } + if (ascii.length > 255) { + return 'DOMAIN_TOO_LONG'; + } + + // Check each part's length and allowed chars. + var labels = ascii.split('.'); + var label; + + for (var i = 0; i < labels.length; ++i) { + label = labels[i]; + if (!label.length) { + return 'LABEL_TOO_SHORT'; + } + if (label.length > 63) { + return 'LABEL_TOO_LONG'; + } + if (label.charAt(0) === '-') { + return 'LABEL_STARTS_WITH_DASH'; + } + if (label.charAt(label.length - 1) === '-') { + return 'LABEL_ENDS_WITH_DASH'; + } + if (!/^[a-z0-9\-]+$/.test(label)) { + return 'LABEL_INVALID_CHARS'; + } + } +}; + + +// +// Public API +// + + +// +// Parse domain. +// +exports.parse = function (input) { + + if (typeof input !== 'string') { + throw new TypeError('Domain name must be a string.'); + } + + // Force domain to lowercase. + var domain = input.slice(0).toLowerCase(); + + // Handle FQDN. + // TODO: Simply remove trailing dot? + if (domain.charAt(domain.length - 1) === '.') { + domain = domain.slice(0, domain.length - 1); + } + + // Validate and sanitise input. + var error = internals.validate(domain); + if (error) { + return { + input: input, + error: { + message: exports.errorCodes[error], + code: error + } + }; + } + + var parsed = { + input: input, + tld: null, + sld: null, + domain: null, + subdomain: null, + listed: false + }; + + var domainParts = domain.split('.'); + + // Non-Internet TLD + if (domainParts[domainParts.length - 1] === 'local') { + return parsed; + } + + var handlePunycode = function () { + + if (!/xn--/.test(domain)) { + return parsed; + } + if (parsed.domain) { + parsed.domain = Punycode.toASCII(parsed.domain); + } + if (parsed.subdomain) { + parsed.subdomain = Punycode.toASCII(parsed.subdomain); + } + return parsed; + }; + + var rule = internals.findRule(domain); + + // Unlisted tld. + if (!rule) { + if (domainParts.length < 2) { + return parsed; + } + parsed.tld = domainParts.pop(); + parsed.sld = domainParts.pop(); + parsed.domain = [parsed.sld, parsed.tld].join('.'); + if (domainParts.length) { + parsed.subdomain = domainParts.pop(); + } + return handlePunycode(); + } + + // At this point we know the public suffix is listed. + parsed.listed = true; + + var tldParts = rule.suffix.split('.'); + var privateParts = domainParts.slice(0, domainParts.length - tldParts.length); + + if (rule.exception) { + privateParts.push(tldParts.shift()); + } + + parsed.tld = tldParts.join('.'); + + if (!privateParts.length) { + return handlePunycode(); + } + + if (rule.wildcard) { + tldParts.unshift(privateParts.pop()); + parsed.tld = tldParts.join('.'); + } + + if (!privateParts.length) { + return handlePunycode(); + } + + parsed.sld = privateParts.pop(); + parsed.domain = [parsed.sld, parsed.tld].join('.'); + + if (privateParts.length) { + parsed.subdomain = privateParts.join('.'); + } + + return handlePunycode(); +}; + + +// +// Get domain. +// +exports.get = function (domain) { + + if (!domain) { + return null; + } + return exports.parse(domain).domain || null; +}; + + +// +// Check whether domain belongs to a known public suffix. +// +exports.isValid = function (domain) { + + var parsed = exports.parse(domain); + return Boolean(parsed.domain && parsed.listed); +}; + + +/***/ }), + +/***/ 751: +/***/ (function(module, __unusedexports, __webpack_require__) { + +var defer = __webpack_require__(500); + +// 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); + }); + } + }; +} + + +/***/ }), + +/***/ 752: +/***/ (function(module, __unusedexports, __webpack_require__) { + +// Copyright 2016 Joyent, Inc. + +module.exports = Certificate; + +var assert = __webpack_require__(477); +var Buffer = __webpack_require__(215).Buffer; +var algs = __webpack_require__(98); +var crypto = __webpack_require__(417); +var Fingerprint = __webpack_require__(400); +var Signature = __webpack_require__(575); +var errs = __webpack_require__(753); +var util = __webpack_require__(669); +var utils = __webpack_require__(270); +var Key = __webpack_require__(852); +var PrivateKey = __webpack_require__(502); +var Identity = __webpack_require__(378); + +var formats = {}; +formats['openssh'] = __webpack_require__(893); +formats['x509'] = __webpack_require__(866); +formats['pem'] = __webpack_require__(680); + +var CertificateParseError = errs.CertificateParseError; +var InvalidAlgorithmError = errs.InvalidAlgorithmError; + +function Certificate(opts) { + assert.object(opts, 'options'); + assert.arrayOfObject(opts.subjects, 'options.subjects'); + utils.assertCompatible(opts.subjects[0], Identity, [1, 0], + 'options.subjects'); + utils.assertCompatible(opts.subjectKey, Key, [1, 0], + 'options.subjectKey'); + utils.assertCompatible(opts.issuer, Identity, [1, 0], 'options.issuer'); + if (opts.issuerKey !== undefined) { + utils.assertCompatible(opts.issuerKey, Key, [1, 0], + 'options.issuerKey'); + } + assert.object(opts.signatures, 'options.signatures'); + assert.buffer(opts.serial, 'options.serial'); + assert.date(opts.validFrom, 'options.validFrom'); + assert.date(opts.validUntil, 'optons.validUntil'); + + assert.optionalArrayOfString(opts.purposes, 'options.purposes'); + + this._hashCache = {}; + + this.subjects = opts.subjects; + this.issuer = opts.issuer; + this.subjectKey = opts.subjectKey; + this.issuerKey = opts.issuerKey; + this.signatures = opts.signatures; + this.serial = opts.serial; + this.validFrom = opts.validFrom; + this.validUntil = opts.validUntil; + this.purposes = opts.purposes; +} + +Certificate.formats = formats; + +Certificate.prototype.toBuffer = function (format, options) { + if (format === undefined) + format = 'x509'; + assert.string(format, 'format'); + assert.object(formats[format], 'formats[format]'); + assert.optionalObject(options, 'options'); + + return (formats[format].write(this, options)); +}; + +Certificate.prototype.toString = function (format, options) { + if (format === undefined) + format = 'pem'; + return (this.toBuffer(format, options).toString()); +}; + +Certificate.prototype.fingerprint = function (algo) { + if (algo === undefined) + algo = 'sha256'; + assert.string(algo, 'algorithm'); + var opts = { + type: 'certificate', + hash: this.hash(algo), + algorithm: algo + }; + return (new Fingerprint(opts)); +}; + +Certificate.prototype.hash = function (algo) { + assert.string(algo, 'algorithm'); + algo = algo.toLowerCase(); + if (algs.hashAlgs[algo] === undefined) + throw (new InvalidAlgorithmError(algo)); + + if (this._hashCache[algo]) + return (this._hashCache[algo]); + + var hash = crypto.createHash(algo). + update(this.toBuffer('x509')).digest(); + this._hashCache[algo] = hash; + return (hash); +}; + +Certificate.prototype.isExpired = function (when) { + if (when === undefined) + when = new Date(); + return (!((when.getTime() >= this.validFrom.getTime()) && + (when.getTime() < this.validUntil.getTime()))); +}; + +Certificate.prototype.isSignedBy = function (issuerCert) { + utils.assertCompatible(issuerCert, Certificate, [1, 0], 'issuer'); + + if (!this.issuer.equals(issuerCert.subjects[0])) + return (false); + if (this.issuer.purposes && this.issuer.purposes.length > 0 && + this.issuer.purposes.indexOf('ca') === -1) { + return (false); + } + + return (this.isSignedByKey(issuerCert.subjectKey)); +}; + +Certificate.prototype.getExtension = function (keyOrOid) { + assert.string(keyOrOid, 'keyOrOid'); + var ext = this.getExtensions().filter(function (maybeExt) { + if (maybeExt.format === 'x509') + return (maybeExt.oid === keyOrOid); + if (maybeExt.format === 'openssh') + return (maybeExt.name === keyOrOid); + return (false); + })[0]; + return (ext); +}; + +Certificate.prototype.getExtensions = function () { + var exts = []; + var x509 = this.signatures.x509; + if (x509 && x509.extras && x509.extras.exts) { + x509.extras.exts.forEach(function (ext) { + ext.format = 'x509'; + exts.push(ext); + }); + } + var openssh = this.signatures.openssh; + if (openssh && openssh.exts) { + openssh.exts.forEach(function (ext) { + ext.format = 'openssh'; + exts.push(ext); + }); + } + return (exts); +}; + +Certificate.prototype.isSignedByKey = function (issuerKey) { + utils.assertCompatible(issuerKey, Key, [1, 2], 'issuerKey'); + + if (this.issuerKey !== undefined) { + return (this.issuerKey. + fingerprint('sha512').matches(issuerKey)); + } + + var fmt = Object.keys(this.signatures)[0]; + var valid = formats[fmt].verify(this, issuerKey); + if (valid) + this.issuerKey = issuerKey; + return (valid); +}; + +Certificate.prototype.signWith = function (key) { + utils.assertCompatible(key, PrivateKey, [1, 2], 'key'); + var fmts = Object.keys(formats); + var didOne = false; + for (var i = 0; i < fmts.length; ++i) { + if (fmts[i] !== 'pem') { + var ret = formats[fmts[i]].sign(this, key); + if (ret === true) + didOne = true; + } + } + if (!didOne) { + throw (new Error('Failed to sign the certificate for any ' + + 'available certificate formats')); + } +}; + +Certificate.createSelfSigned = function (subjectOrSubjects, key, options) { + var subjects; + if (Array.isArray(subjectOrSubjects)) + subjects = subjectOrSubjects; + else + subjects = [subjectOrSubjects]; + + assert.arrayOfObject(subjects); + subjects.forEach(function (subject) { + utils.assertCompatible(subject, Identity, [1, 0], 'subject'); + }); + + utils.assertCompatible(key, PrivateKey, [1, 2], 'private key'); + + assert.optionalObject(options, 'options'); + if (options === undefined) + options = {}; + assert.optionalObject(options.validFrom, 'options.validFrom'); + assert.optionalObject(options.validUntil, 'options.validUntil'); + var validFrom = options.validFrom; + var validUntil = options.validUntil; + if (validFrom === undefined) + validFrom = new Date(); + if (validUntil === undefined) { + assert.optionalNumber(options.lifetime, 'options.lifetime'); + var lifetime = options.lifetime; + if (lifetime === undefined) + lifetime = 10*365*24*3600; + validUntil = new Date(); + validUntil.setTime(validUntil.getTime() + lifetime*1000); + } + assert.optionalBuffer(options.serial, 'options.serial'); + var serial = options.serial; + if (serial === undefined) + serial = Buffer.from('0000000000000001', 'hex'); + + var purposes = options.purposes; + if (purposes === undefined) + purposes = []; + + if (purposes.indexOf('signature') === -1) + purposes.push('signature'); + + /* Self-signed certs are always CAs. */ + if (purposes.indexOf('ca') === -1) + purposes.push('ca'); + if (purposes.indexOf('crl') === -1) + purposes.push('crl'); + + /* + * If we weren't explicitly given any other purposes, do the sensible + * thing and add some basic ones depending on the subject type. + */ + if (purposes.length <= 3) { + var hostSubjects = subjects.filter(function (subject) { + return (subject.type === 'host'); + }); + var userSubjects = subjects.filter(function (subject) { + return (subject.type === 'user'); + }); + if (hostSubjects.length > 0) { + if (purposes.indexOf('serverAuth') === -1) + purposes.push('serverAuth'); + } + if (userSubjects.length > 0) { + if (purposes.indexOf('clientAuth') === -1) + purposes.push('clientAuth'); + } + if (userSubjects.length > 0 || hostSubjects.length > 0) { + if (purposes.indexOf('keyAgreement') === -1) + purposes.push('keyAgreement'); + if (key.type === 'rsa' && + purposes.indexOf('encryption') === -1) + purposes.push('encryption'); + } + } + + var cert = new Certificate({ + subjects: subjects, + issuer: subjects[0], + subjectKey: key.toPublic(), + issuerKey: key.toPublic(), + signatures: {}, + serial: serial, + validFrom: validFrom, + validUntil: validUntil, + purposes: purposes + }); + cert.signWith(key); + + return (cert); +}; + +Certificate.create = + function (subjectOrSubjects, key, issuer, issuerKey, options) { + var subjects; + if (Array.isArray(subjectOrSubjects)) + subjects = subjectOrSubjects; + else + subjects = [subjectOrSubjects]; + + assert.arrayOfObject(subjects); + subjects.forEach(function (subject) { + utils.assertCompatible(subject, Identity, [1, 0], 'subject'); + }); + + utils.assertCompatible(key, Key, [1, 0], 'key'); + if (PrivateKey.isPrivateKey(key)) + key = key.toPublic(); + utils.assertCompatible(issuer, Identity, [1, 0], 'issuer'); + utils.assertCompatible(issuerKey, PrivateKey, [1, 2], 'issuer key'); + + assert.optionalObject(options, 'options'); + if (options === undefined) + options = {}; + assert.optionalObject(options.validFrom, 'options.validFrom'); + assert.optionalObject(options.validUntil, 'options.validUntil'); + var validFrom = options.validFrom; + var validUntil = options.validUntil; + if (validFrom === undefined) + validFrom = new Date(); + if (validUntil === undefined) { + assert.optionalNumber(options.lifetime, 'options.lifetime'); + var lifetime = options.lifetime; + if (lifetime === undefined) + lifetime = 10*365*24*3600; + validUntil = new Date(); + validUntil.setTime(validUntil.getTime() + lifetime*1000); + } + assert.optionalBuffer(options.serial, 'options.serial'); + var serial = options.serial; + if (serial === undefined) + serial = Buffer.from('0000000000000001', 'hex'); + + var purposes = options.purposes; + if (purposes === undefined) + purposes = []; + + if (purposes.indexOf('signature') === -1) + purposes.push('signature'); + + if (options.ca === true) { + if (purposes.indexOf('ca') === -1) + purposes.push('ca'); + if (purposes.indexOf('crl') === -1) + purposes.push('crl'); + } + + var hostSubjects = subjects.filter(function (subject) { + return (subject.type === 'host'); + }); + var userSubjects = subjects.filter(function (subject) { + return (subject.type === 'user'); + }); + if (hostSubjects.length > 0) { + if (purposes.indexOf('serverAuth') === -1) + purposes.push('serverAuth'); + } + if (userSubjects.length > 0) { + if (purposes.indexOf('clientAuth') === -1) + purposes.push('clientAuth'); + } + if (userSubjects.length > 0 || hostSubjects.length > 0) { + if (purposes.indexOf('keyAgreement') === -1) + purposes.push('keyAgreement'); + if (key.type === 'rsa' && + purposes.indexOf('encryption') === -1) + purposes.push('encryption'); + } + + var cert = new Certificate({ + subjects: subjects, + issuer: issuer, + subjectKey: key, + issuerKey: issuerKey.toPublic(), + signatures: {}, + serial: serial, + validFrom: validFrom, + validUntil: validUntil, + purposes: purposes + }); + cert.signWith(issuerKey); + + return (cert); +}; + +Certificate.parse = function (data, format, options) { + if (typeof (data) !== 'string') + assert.buffer(data, 'data'); + if (format === undefined) + format = 'auto'; + assert.string(format, 'format'); + if (typeof (options) === 'string') + options = { filename: options }; + assert.optionalObject(options, 'options'); + if (options === undefined) + options = {}; + assert.optionalString(options.filename, 'options.filename'); + if (options.filename === undefined) + options.filename = '(unnamed)'; + + assert.object(formats[format], 'formats[format]'); + + try { + var k = formats[format].read(data, options); + return (k); + } catch (e) { + throw (new CertificateParseError(options.filename, format, e)); + } +}; + +Certificate.isCertificate = function (obj, ver) { + return (utils.isCompatible(obj, Certificate, ver)); +}; + +/* + * API versions for Certificate: + * [1,0] -- initial ver + * [1,1] -- openssh format now unpacks extensions + */ +Certificate.prototype._sshpkApiVersion = [1, 1]; + +Certificate._oldVersionDetect = function (obj) { + return ([1, 0]); +}; + + +/***/ }), + +/***/ 753: +/***/ (function(module, __unusedexports, __webpack_require__) { + +// Copyright 2015 Joyent, Inc. + +var assert = __webpack_require__(477); +var util = __webpack_require__(669); + +function FingerprintFormatError(fp, format) { + if (Error.captureStackTrace) + Error.captureStackTrace(this, FingerprintFormatError); + this.name = 'FingerprintFormatError'; + this.fingerprint = fp; + this.format = format; + this.message = 'Fingerprint format is not supported, or is invalid: '; + if (fp !== undefined) + this.message += ' fingerprint = ' + fp; + if (format !== undefined) + this.message += ' format = ' + format; +} +util.inherits(FingerprintFormatError, Error); + +function InvalidAlgorithmError(alg) { + if (Error.captureStackTrace) + Error.captureStackTrace(this, InvalidAlgorithmError); + this.name = 'InvalidAlgorithmError'; + this.algorithm = alg; + this.message = 'Algorithm "' + alg + '" is not supported'; +} +util.inherits(InvalidAlgorithmError, Error); + +function KeyParseError(name, format, innerErr) { + if (Error.captureStackTrace) + Error.captureStackTrace(this, KeyParseError); + this.name = 'KeyParseError'; + this.format = format; + this.keyName = name; + this.innerErr = innerErr; + this.message = 'Failed to parse ' + name + ' as a valid ' + format + + ' format key: ' + innerErr.message; +} +util.inherits(KeyParseError, Error); + +function SignatureParseError(type, format, innerErr) { + if (Error.captureStackTrace) + Error.captureStackTrace(this, SignatureParseError); + this.name = 'SignatureParseError'; + this.type = type; + this.format = format; + this.innerErr = innerErr; + this.message = 'Failed to parse the given data as a ' + type + + ' signature in ' + format + ' format: ' + innerErr.message; +} +util.inherits(SignatureParseError, Error); + +function CertificateParseError(name, format, innerErr) { + if (Error.captureStackTrace) + Error.captureStackTrace(this, CertificateParseError); + this.name = 'CertificateParseError'; + this.format = format; + this.certName = name; + this.innerErr = innerErr; + this.message = 'Failed to parse ' + name + ' as a valid ' + format + + ' format certificate: ' + innerErr.message; +} +util.inherits(CertificateParseError, Error); + +function KeyEncryptedError(name, format) { + if (Error.captureStackTrace) + Error.captureStackTrace(this, KeyEncryptedError); + this.name = 'KeyEncryptedError'; + this.format = format; + this.keyName = name; + this.message = 'The ' + format + ' format key ' + name + ' is ' + + 'encrypted (password-protected), and no passphrase was ' + + 'provided in `options`'; +} +util.inherits(KeyEncryptedError, Error); + +module.exports = { + FingerprintFormatError: FingerprintFormatError, + InvalidAlgorithmError: InvalidAlgorithmError, + KeyParseError: KeyParseError, + SignatureParseError: SignatureParseError, + KeyEncryptedError: KeyEncryptedError, + CertificateParseError: CertificateParseError +}; + + +/***/ }), + +/***/ 755: +/***/ (function(module, __unusedexports, __webpack_require__) { + +"use strict"; + + +var utils = __webpack_require__(581); + +var has = Object.prototype.hasOwnProperty; + +var defaults = { + allowDots: false, + allowPrototypes: false, + arrayLimit: 20, + decoder: utils.decode, + delimiter: '&', + depth: 5, + parameterLimit: 1000, + plainObjects: false, + strictNullHandling: false +}; + +var parseValues = function parseQueryStringValues(str, options) { + var obj = {}; + var cleanStr = options.ignoreQueryPrefix ? str.replace(/^\?/, '') : str; + var limit = options.parameterLimit === Infinity ? undefined : options.parameterLimit; + var parts = cleanStr.split(options.delimiter, limit); + + for (var i = 0; i < parts.length; ++i) { + var part = parts[i]; + + var bracketEqualsPos = part.indexOf(']='); + var pos = bracketEqualsPos === -1 ? part.indexOf('=') : bracketEqualsPos + 1; + + var key, val; + if (pos === -1) { + key = options.decoder(part, defaults.decoder); + val = options.strictNullHandling ? null : ''; + } else { + key = options.decoder(part.slice(0, pos), defaults.decoder); + val = options.decoder(part.slice(pos + 1), defaults.decoder); + } + if (has.call(obj, key)) { + obj[key] = [].concat(obj[key]).concat(val); + } else { + obj[key] = val; + } + } + + return obj; +}; + +var parseObject = function (chain, val, options) { + var leaf = val; + + for (var i = chain.length - 1; i >= 0; --i) { + var obj; + var root = chain[i]; + + if (root === '[]') { + obj = []; + obj = obj.concat(leaf); + } else { + obj = options.plainObjects ? Object.create(null) : {}; + var cleanRoot = root.charAt(0) === '[' && root.charAt(root.length - 1) === ']' ? root.slice(1, -1) : root; + var index = parseInt(cleanRoot, 10); + if ( + !isNaN(index) + && root !== cleanRoot + && String(index) === cleanRoot + && index >= 0 + && (options.parseArrays && index <= options.arrayLimit) + ) { + obj = []; + obj[index] = leaf; + } else { + obj[cleanRoot] = leaf; + } + } + + leaf = obj; + } + + return leaf; +}; + +var parseKeys = function parseQueryStringKeys(givenKey, val, options) { + if (!givenKey) { + return; + } + + // Transform dot notation to bracket notation + var key = options.allowDots ? givenKey.replace(/\.([^.[]+)/g, '[$1]') : givenKey; + + // The regex chunks + + var brackets = /(\[[^[\]]*])/; + var child = /(\[[^[\]]*])/g; + + // Get the parent + + var segment = brackets.exec(key); + var parent = segment ? key.slice(0, segment.index) : key; + + // Stash the parent if it exists + + var keys = []; + if (parent) { + // If we aren't using plain objects, optionally prefix keys + // that would overwrite object prototype properties + if (!options.plainObjects && has.call(Object.prototype, parent)) { + if (!options.allowPrototypes) { + return; + } + } + + keys.push(parent); + } + + // Loop through children appending to the array until we hit depth + + var i = 0; + while ((segment = child.exec(key)) !== null && i < options.depth) { + i += 1; + if (!options.plainObjects && has.call(Object.prototype, segment[1].slice(1, -1))) { + if (!options.allowPrototypes) { + return; + } + } + keys.push(segment[1]); + } + + // If there's a remainder, just add whatever is left + + if (segment) { + keys.push('[' + key.slice(segment.index) + ']'); + } + + return parseObject(keys, val, options); +}; + +module.exports = function (str, opts) { + var options = opts ? utils.assign({}, opts) : {}; + + if (options.decoder !== null && options.decoder !== undefined && typeof options.decoder !== 'function') { + throw new TypeError('Decoder has to be a function.'); + } + + options.ignoreQueryPrefix = options.ignoreQueryPrefix === true; + options.delimiter = typeof options.delimiter === 'string' || utils.isRegExp(options.delimiter) ? options.delimiter : defaults.delimiter; + options.depth = typeof options.depth === 'number' ? options.depth : defaults.depth; + options.arrayLimit = typeof options.arrayLimit === 'number' ? options.arrayLimit : defaults.arrayLimit; + options.parseArrays = options.parseArrays !== false; + options.decoder = typeof options.decoder === 'function' ? options.decoder : defaults.decoder; + options.allowDots = typeof options.allowDots === 'boolean' ? options.allowDots : defaults.allowDots; + options.plainObjects = typeof options.plainObjects === 'boolean' ? options.plainObjects : defaults.plainObjects; + options.allowPrototypes = typeof options.allowPrototypes === 'boolean' ? options.allowPrototypes : defaults.allowPrototypes; + options.parameterLimit = typeof options.parameterLimit === 'number' ? options.parameterLimit : defaults.parameterLimit; + options.strictNullHandling = typeof options.strictNullHandling === 'boolean' ? options.strictNullHandling : defaults.strictNullHandling; + + if (str === '' || str === null || typeof str === 'undefined') { + return options.plainObjects ? Object.create(null) : {}; + } + + var tempObj = typeof str === 'string' ? parseValues(str, options) : str; + var obj = options.plainObjects ? Object.create(null) : {}; + + // Iterate over the keys and setup the new object + + var keys = Object.keys(tempObj); + for (var i = 0; i < keys.length; ++i) { + var key = keys[i]; + var newObj = parseKeys(key, tempObj[key], options); + obj = utils.merge(obj, newObj, options); + } + + return utils.compact(obj); +}; + + +/***/ }), + +/***/ 758: +/***/ (function(module) { + +module.exports = {"$id":"timings.json#","$schema":"http://json-schema.org/draft-06/schema#","required":["send","wait","receive"],"properties":{"dns":{"type":"number","min":-1},"connect":{"type":"number","min":-1},"blocked":{"type":"number","min":-1},"send":{"type":"number","min":-1},"wait":{"type":"number","min":-1},"receive":{"type":"number","min":-1},"ssl":{"type":"number","min":-1},"comment":{"type":"string"}}}; + +/***/ }), + +/***/ 761: +/***/ (function(module) { + +module.exports = require("zlib"); + +/***/ }), + +/***/ 772: +/***/ (function(module) { + +"use strict"; + +module.exports = function generate__limitLength(it, $keyword, $ruleType) { + var out = ' '; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $errorKeyword; + var $data = 'data' + ($dataLvl || ''); + var $isData = it.opts.$data && $schema && $schema.$data, + $schemaValue; + if ($isData) { + out += ' var schema' + ($lvl) + ' = ' + (it.util.getData($schema.$data, $dataLvl, it.dataPathArr)) + '; '; + $schemaValue = 'schema' + $lvl; + } else { + $schemaValue = $schema; + } + var $op = $keyword == 'maxLength' ? '>' : '<'; + out += 'if ( '; + if ($isData) { + out += ' (' + ($schemaValue) + ' !== undefined && typeof ' + ($schemaValue) + ' != \'number\') || '; + } + if (it.opts.unicode === false) { + out += ' ' + ($data) + '.length '; + } else { + out += ' ucs2length(' + ($data) + ') '; + } + out += ' ' + ($op) + ' ' + ($schemaValue) + ') { '; + var $errorKeyword = $keyword; + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ($errorKeyword || '_limitLength') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { limit: ' + ($schemaValue) + ' } '; + if (it.opts.messages !== false) { + out += ' , message: \'should NOT be '; + if ($keyword == 'maxLength') { + out += 'longer'; + } else { + out += 'shorter'; + } + out += ' than '; + if ($isData) { + out += '\' + ' + ($schemaValue) + ' + \''; + } else { + out += '' + ($schema); + } + out += ' characters\' '; + } + if (it.opts.verbose) { + out += ' , schema: '; + if ($isData) { + out += 'validate.schema' + ($schemaPath); + } else { + out += '' + ($schema); + } + out += ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + out += '} '; + if ($breakOnError) { + out += ' else { '; + } + return out; +} + + +/***/ }), + +/***/ 776: +/***/ (function(module) { + +module.exports = {"$id":"creator.json#","$schema":"http://json-schema.org/draft-06/schema#","type":"object","required":["name","version"],"properties":{"name":{"type":"string"},"version":{"type":"string"},"comment":{"type":"string"}}}; + +/***/ }), + +/***/ 779: +/***/ (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__(972) +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 + } + }) +} + + +/***/ }), + +/***/ 789: +/***/ (function(module, __unusedexports, __webpack_require__) { + +// Copyright 2015 Joyent, Inc. + +var parser = __webpack_require__(342); +var signer = __webpack_require__(64); +var verify = __webpack_require__(428); +var utils = __webpack_require__(909); + + + +///--- API + +module.exports = { + + parse: parser.parseRequest, + parseRequest: parser.parseRequest, + + sign: signer.signRequest, + signRequest: signer.signRequest, + createSigner: signer.createSigner, + isSigner: signer.isSigner, + + sshKeyToPEM: utils.sshKeyToPEM, + sshKeyFingerprint: utils.fingerprint, + pemToRsaSSHKey: utils.pemToRsaSSHKey, + + verify: verify.verifySignature, + verifySignature: verify.verifySignature, + verifyHMAC: verify.verifyHMAC +}; + + +/***/ }), + +/***/ 792: +/***/ (function(module, __unusedexports, __webpack_require__) { + +module.exports = ForeverAgent +ForeverAgent.SSL = ForeverAgentSSL + +var util = __webpack_require__(669) + , Agent = __webpack_require__(605).Agent + , net = __webpack_require__(631) + , tls = __webpack_require__(16) + , AgentSSL = __webpack_require__(211).Agent + +function getConnectionName(host, port) { + var name = '' + if (typeof host === 'string') { + name = host + ':' + port + } else { + // For node.js v012.0 and iojs-v1.5.1, host is an object. And any existing localAddress is part of the connection name. + name = host.host + ':' + host.port + ':' + (host.localAddress ? (host.localAddress + ':') : ':') + } + return name +} + +function ForeverAgent(options) { + var self = this + self.options = options || {} + self.requests = {} + self.sockets = {} + self.freeSockets = {} + self.maxSockets = self.options.maxSockets || Agent.defaultMaxSockets + self.minSockets = self.options.minSockets || ForeverAgent.defaultMinSockets + self.on('free', function(socket, host, port) { + var name = getConnectionName(host, port) + + if (self.requests[name] && self.requests[name].length) { + self.requests[name].shift().onSocket(socket) + } else if (self.sockets[name].length < self.minSockets) { + if (!self.freeSockets[name]) self.freeSockets[name] = [] + self.freeSockets[name].push(socket) + + // if an error happens while we don't use the socket anyway, meh, throw the socket away + var onIdleError = function() { + socket.destroy() + } + socket._onIdleError = onIdleError + socket.on('error', onIdleError) + } else { + // If there are no pending requests just destroy the + // socket and it will get removed from the pool. This + // gets us out of timeout issues and allows us to + // default to Connection:keep-alive. + socket.destroy() + } + }) + +} +util.inherits(ForeverAgent, Agent) + +ForeverAgent.defaultMinSockets = 5 + + +ForeverAgent.prototype.createConnection = net.createConnection +ForeverAgent.prototype.addRequestNoreuse = Agent.prototype.addRequest +ForeverAgent.prototype.addRequest = function(req, host, port) { + var name = getConnectionName(host, port) + + if (typeof host !== 'string') { + var options = host + port = options.port + host = options.host + } + + if (this.freeSockets[name] && this.freeSockets[name].length > 0 && !req.useChunkedEncodingByDefault) { + var idleSocket = this.freeSockets[name].pop() + idleSocket.removeListener('error', idleSocket._onIdleError) + delete idleSocket._onIdleError + req._reusedSocket = true + req.onSocket(idleSocket) + } else { + this.addRequestNoreuse(req, host, port) + } +} + +ForeverAgent.prototype.removeSocket = function(s, name, host, port) { + if (this.sockets[name]) { + var index = this.sockets[name].indexOf(s) + if (index !== -1) { + this.sockets[name].splice(index, 1) + } + } else if (this.sockets[name] && this.sockets[name].length === 0) { + // don't leak + delete this.sockets[name] + delete this.requests[name] + } + + if (this.freeSockets[name]) { + var index = this.freeSockets[name].indexOf(s) + if (index !== -1) { + this.freeSockets[name].splice(index, 1) + if (this.freeSockets[name].length === 0) { + delete this.freeSockets[name] + } + } + } + + if (this.requests[name] && this.requests[name].length) { + // If we have pending requests and a socket gets closed a new one + // needs to be created to take over in the pool for the one that closed. + this.createSocket(name, host, port).emit('free') + } +} + +function ForeverAgentSSL (options) { + ForeverAgent.call(this, options) +} +util.inherits(ForeverAgentSSL, ForeverAgent) + +ForeverAgentSSL.prototype.createConnection = createConnectionSSL +ForeverAgentSSL.prototype.addRequestNoreuse = AgentSSL.prototype.addRequest + +function createConnectionSSL (port, host, options) { + if (typeof port === 'object') { + options = port; + } else if (typeof host === 'object') { + options = host; + } else if (typeof options === 'object') { + options = options; + } else { + options = {}; + } + + if (typeof port === 'number') { + options.port = port; + } + + if (typeof host === 'string') { + options.host = host; + } + + return tls.connect(options); +} + + +/***/ }), + +/***/ 805: +/***/ (function(module, __unusedexports, __webpack_require__) { + +"use strict"; + + +var resolve = __webpack_require__(867) + , util = __webpack_require__(855) + , errorClasses = __webpack_require__(844) + , stableStringify = __webpack_require__(741); + +var validateGenerator = __webpack_require__(967); + +/** + * Functions below are used inside compiled validations function + */ + +var ucs2length = util.ucs2length; +var equal = __webpack_require__(832); + +// this error is thrown by async schemas to return validation errors via exception +var ValidationError = errorClasses.Validation; + +module.exports = compile; + + +/** + * Compiles schema to validation function + * @this Ajv + * @param {Object} schema schema object + * @param {Object} root object with information about the root schema for this schema + * @param {Object} localRefs the hash of local references inside the schema (created by resolve.id), used for inline resolution + * @param {String} baseId base ID for IDs in the schema + * @return {Function} validation function + */ +function compile(schema, root, localRefs, baseId) { + /* jshint validthis: true, evil: true */ + /* eslint no-shadow: 0 */ + var self = this + , opts = this._opts + , refVal = [ undefined ] + , refs = {} + , patterns = [] + , patternsHash = {} + , defaults = [] + , defaultsHash = {} + , customRules = []; + + root = root || { schema: schema, refVal: refVal, refs: refs }; + + var c = checkCompiling.call(this, schema, root, baseId); + var compilation = this._compilations[c.index]; + if (c.compiling) return (compilation.callValidate = callValidate); + + var formats = this._formats; + var RULES = this.RULES; + + try { + var v = localCompile(schema, root, localRefs, baseId); + compilation.validate = v; + var cv = compilation.callValidate; + if (cv) { + cv.schema = v.schema; + cv.errors = null; + cv.refs = v.refs; + cv.refVal = v.refVal; + cv.root = v.root; + cv.$async = v.$async; + if (opts.sourceCode) cv.source = v.source; + } + return v; + } finally { + endCompiling.call(this, schema, root, baseId); + } + + /* @this {*} - custom context, see passContext option */ + function callValidate() { + /* jshint validthis: true */ + var validate = compilation.validate; + var result = validate.apply(this, arguments); + callValidate.errors = validate.errors; + return result; + } + + function localCompile(_schema, _root, localRefs, baseId) { + var isRoot = !_root || (_root && _root.schema == _schema); + if (_root.schema != root.schema) + return compile.call(self, _schema, _root, localRefs, baseId); + + var $async = _schema.$async === true; + + var sourceCode = validateGenerator({ + isTop: true, + schema: _schema, + isRoot: isRoot, + baseId: baseId, + root: _root, + schemaPath: '', + errSchemaPath: '#', + errorPath: '""', + MissingRefError: errorClasses.MissingRef, + RULES: RULES, + validate: validateGenerator, + util: util, + resolve: resolve, + resolveRef: resolveRef, + usePattern: usePattern, + useDefault: useDefault, + useCustomRule: useCustomRule, + opts: opts, + formats: formats, + logger: self.logger, + self: self + }); + + sourceCode = vars(refVal, refValCode) + vars(patterns, patternCode) + + vars(defaults, defaultCode) + vars(customRules, customRuleCode) + + sourceCode; + + if (opts.processCode) sourceCode = opts.processCode(sourceCode); + // console.log('\n\n\n *** \n', JSON.stringify(sourceCode)); + var validate; + try { + var makeValidate = new Function( + 'self', + 'RULES', + 'formats', + 'root', + 'refVal', + 'defaults', + 'customRules', + 'equal', + 'ucs2length', + 'ValidationError', + sourceCode + ); + + validate = makeValidate( + self, + RULES, + formats, + root, + refVal, + defaults, + customRules, + equal, + ucs2length, + ValidationError + ); + + refVal[0] = validate; + } catch(e) { + self.logger.error('Error compiling schema, function code:', sourceCode); + throw e; + } + + validate.schema = _schema; + validate.errors = null; + validate.refs = refs; + validate.refVal = refVal; + validate.root = isRoot ? validate : _root; + if ($async) validate.$async = true; + if (opts.sourceCode === true) { + validate.source = { + code: sourceCode, + patterns: patterns, + defaults: defaults + }; + } + + return validate; + } + + function resolveRef(baseId, ref, isRoot) { + ref = resolve.url(baseId, ref); + var refIndex = refs[ref]; + var _refVal, refCode; + if (refIndex !== undefined) { + _refVal = refVal[refIndex]; + refCode = 'refVal[' + refIndex + ']'; + return resolvedRef(_refVal, refCode); + } + if (!isRoot && root.refs) { + var rootRefId = root.refs[ref]; + if (rootRefId !== undefined) { + _refVal = root.refVal[rootRefId]; + refCode = addLocalRef(ref, _refVal); + return resolvedRef(_refVal, refCode); + } + } + + refCode = addLocalRef(ref); + var v = resolve.call(self, localCompile, root, ref); + if (v === undefined) { + var localSchema = localRefs && localRefs[ref]; + if (localSchema) { + v = resolve.inlineRef(localSchema, opts.inlineRefs) + ? localSchema + : compile.call(self, localSchema, root, localRefs, baseId); + } + } + + if (v === undefined) { + removeLocalRef(ref); + } else { + replaceLocalRef(ref, v); + return resolvedRef(v, refCode); + } + } + + function addLocalRef(ref, v) { + var refId = refVal.length; + refVal[refId] = v; + refs[ref] = refId; + return 'refVal' + refId; + } + + function removeLocalRef(ref) { + delete refs[ref]; + } + + function replaceLocalRef(ref, v) { + var refId = refs[ref]; + refVal[refId] = v; + } + + function resolvedRef(refVal, code) { + return typeof refVal == 'object' || typeof refVal == 'boolean' + ? { code: code, schema: refVal, inline: true } + : { code: code, $async: refVal && !!refVal.$async }; + } + + function usePattern(regexStr) { + var index = patternsHash[regexStr]; + if (index === undefined) { + index = patternsHash[regexStr] = patterns.length; + patterns[index] = regexStr; + } + return 'pattern' + index; + } + + function useDefault(value) { + switch (typeof value) { + case 'boolean': + case 'number': + return '' + value; + case 'string': + return util.toQuotedString(value); + case 'object': + if (value === null) return 'null'; + var valueStr = stableStringify(value); + var index = defaultsHash[valueStr]; + if (index === undefined) { + index = defaultsHash[valueStr] = defaults.length; + defaults[index] = value; + } + return 'default' + index; + } + } + + function useCustomRule(rule, schema, parentSchema, it) { + if (self._opts.validateSchema !== false) { + var deps = rule.definition.dependencies; + if (deps && !deps.every(function(keyword) { + return Object.prototype.hasOwnProperty.call(parentSchema, keyword); + })) + throw new Error('parent schema must have all required keywords: ' + deps.join(',')); + + var validateSchema = rule.definition.validateSchema; + if (validateSchema) { + var valid = validateSchema(schema); + if (!valid) { + var message = 'keyword schema is invalid: ' + self.errorsText(validateSchema.errors); + if (self._opts.validateSchema == 'log') self.logger.error(message); + else throw new Error(message); + } + } + } + + var compile = rule.definition.compile + , inline = rule.definition.inline + , macro = rule.definition.macro; + + var validate; + if (compile) { + validate = compile.call(self, schema, parentSchema, it); + } else if (macro) { + validate = macro.call(self, schema, parentSchema, it); + if (opts.validateSchema !== false) self.validateSchema(validate, true); + } else if (inline) { + validate = inline.call(self, it, rule.keyword, schema, parentSchema); + } else { + validate = rule.definition.validate; + if (!validate) return; + } + + if (validate === undefined) + throw new Error('custom keyword "' + rule.keyword + '"failed to compile'); + + var index = customRules.length; + customRules[index] = validate; + + return { + code: 'customRule' + index, + validate: validate + }; + } +} + + +/** + * Checks if the schema is currently compiled + * @this Ajv + * @param {Object} schema schema to compile + * @param {Object} root root object + * @param {String} baseId base schema ID + * @return {Object} object with properties "index" (compilation index) and "compiling" (boolean) + */ +function checkCompiling(schema, root, baseId) { + /* jshint validthis: true */ + var index = compIndex.call(this, schema, root, baseId); + if (index >= 0) return { index: index, compiling: true }; + index = this._compilations.length; + this._compilations[index] = { + schema: schema, + root: root, + baseId: baseId + }; + return { index: index, compiling: false }; +} + + +/** + * Removes the schema from the currently compiled list + * @this Ajv + * @param {Object} schema schema to compile + * @param {Object} root root object + * @param {String} baseId base schema ID + */ +function endCompiling(schema, root, baseId) { + /* jshint validthis: true */ + var i = compIndex.call(this, schema, root, baseId); + if (i >= 0) this._compilations.splice(i, 1); +} + + +/** + * Index of schema compilation in the currently compiled list + * @this Ajv + * @param {Object} schema schema to compile + * @param {Object} root root object + * @param {String} baseId base schema ID + * @return {Integer} compilation index + */ +function compIndex(schema, root, baseId) { + /* jshint validthis: true */ + for (var i=0; i 1024) + hashAlgo = 'sha256'; + if (this.type === 'ed25519') + hashAlgo = 'sha512'; + if (this.type === 'ecdsa') { + if (this.size <= 256) + hashAlgo = 'sha256'; + else if (this.size <= 384) + hashAlgo = 'sha384'; + else + hashAlgo = 'sha512'; + } + return (hashAlgo); +}; + +Key.prototype.createVerify = function (hashAlgo) { + if (hashAlgo === undefined) + hashAlgo = this.defaultHashAlgorithm(); + assert.string(hashAlgo, 'hash algorithm'); + + /* ED25519 is not supported by OpenSSL, use a javascript impl. */ + if (this.type === 'ed25519' && edCompat !== undefined) + return (new edCompat.Verifier(this, hashAlgo)); + if (this.type === 'curve25519') + throw (new Error('Curve25519 keys are not suitable for ' + + 'signing or verification')); + + var v, nm, err; + try { + nm = hashAlgo.toUpperCase(); + v = crypto.createVerify(nm); + } catch (e) { + err = e; + } + if (v === undefined || (err instanceof Error && + err.message.match(/Unknown message digest/))) { + nm = 'RSA-'; + nm += hashAlgo.toUpperCase(); + v = crypto.createVerify(nm); + } + assert.ok(v, 'failed to create verifier'); + var oldVerify = v.verify.bind(v); + var key = this.toBuffer('pkcs8'); + var curve = this.curve; + var self = this; + v.verify = function (signature, fmt) { + if (Signature.isSignature(signature, [2, 0])) { + if (signature.type !== self.type) + return (false); + if (signature.hashAlgorithm && + signature.hashAlgorithm !== hashAlgo) + return (false); + if (signature.curve && self.type === 'ecdsa' && + signature.curve !== curve) + return (false); + return (oldVerify(key, signature.toBuffer('asn1'))); + + } else if (typeof (signature) === 'string' || + Buffer.isBuffer(signature)) { + return (oldVerify(key, signature, fmt)); + + /* + * Avoid doing this on valid arguments, walking the prototype + * chain can be quite slow. + */ + } else if (Signature.isSignature(signature, [1, 0])) { + throw (new Error('signature was created by too old ' + + 'a version of sshpk and cannot be verified')); + + } else { + throw (new TypeError('signature must be a string, ' + + 'Buffer, or Signature object')); + } + }; + return (v); +}; + +Key.prototype.createDiffieHellman = function () { + if (this.type === 'rsa') + throw (new Error('RSA keys do not support Diffie-Hellman')); + + return (new DiffieHellman(this)); +}; +Key.prototype.createDH = Key.prototype.createDiffieHellman; + +Key.parse = function (data, format, options) { + if (typeof (data) !== 'string') + assert.buffer(data, 'data'); + if (format === undefined) + format = 'auto'; + assert.string(format, 'format'); + if (typeof (options) === 'string') + options = { filename: options }; + assert.optionalObject(options, 'options'); + if (options === undefined) + options = {}; + assert.optionalString(options.filename, 'options.filename'); + if (options.filename === undefined) + options.filename = '(unnamed)'; + + assert.object(formats[format], 'formats[format]'); + + try { + var k = formats[format].read(data, options); + if (k instanceof PrivateKey) + k = k.toPublic(); + if (!k.comment) + k.comment = options.filename; + return (k); + } catch (e) { + if (e.name === 'KeyEncryptedError') + throw (e); + throw (new KeyParseError(options.filename, format, e)); + } +}; + +Key.isKey = function (obj, ver) { + return (utils.isCompatible(obj, Key, ver)); +}; + +/* + * API versions for Key: + * [1,0] -- initial ver, may take Signature for createVerify or may not + * [1,1] -- added pkcs1, pkcs8 formats + * [1,2] -- added auto, ssh-private, openssh formats + * [1,3] -- added defaultHashAlgorithm + * [1,4] -- added ed support, createDH + * [1,5] -- first explicitly tagged version + * [1,6] -- changed ed25519 part names + * [1,7] -- spki hash types + */ +Key.prototype._sshpkApiVersion = [1, 7]; + +Key._oldVersionDetect = function (obj) { + assert.func(obj.toBuffer); + assert.func(obj.fingerprint); + if (obj.createDH) + return ([1, 4]); + if (obj.defaultHashAlgorithm) + return ([1, 3]); + if (obj.formats['auto']) + return ([1, 2]); + if (obj.formats['pkcs1']) + return ([1, 1]); + return ([1, 0]); +}; + + +/***/ }), + +/***/ 853: +/***/ (function(__unusedmodule, exports) { + +/** @license URI.js v4.2.1 (c) 2011 Gary Court. License: http://github.com/garycourt/uri-js */ +(function (global, factory) { + true ? factory(exports) : + undefined; +}(this, (function (exports) { 'use strict'; + +function merge() { + for (var _len = arguments.length, sets = Array(_len), _key = 0; _key < _len; _key++) { + sets[_key] = arguments[_key]; + } + + if (sets.length > 1) { + sets[0] = sets[0].slice(0, -1); + var xl = sets.length - 1; + for (var x = 1; x < xl; ++x) { + sets[x] = sets[x].slice(1, -1); + } + sets[xl] = sets[xl].slice(1); + return sets.join(''); + } else { + return sets[0]; + } +} +function subexp(str) { + return "(?:" + str + ")"; +} +function typeOf(o) { + return o === undefined ? "undefined" : o === null ? "null" : Object.prototype.toString.call(o).split(" ").pop().split("]").shift().toLowerCase(); +} +function toUpperCase(str) { + return str.toUpperCase(); +} +function toArray(obj) { + return obj !== undefined && obj !== null ? obj instanceof Array ? obj : typeof obj.length !== "number" || obj.split || obj.setInterval || obj.call ? [obj] : Array.prototype.slice.call(obj) : []; +} +function assign(target, source) { + var obj = target; + if (source) { + for (var key in source) { + obj[key] = source[key]; + } + } + return obj; +} + +function buildExps(isIRI) { + var ALPHA$$ = "[A-Za-z]", + CR$ = "[\\x0D]", + DIGIT$$ = "[0-9]", + DQUOTE$$ = "[\\x22]", + HEXDIG$$ = merge(DIGIT$$, "[A-Fa-f]"), + //case-insensitive + LF$$ = "[\\x0A]", + SP$$ = "[\\x20]", + PCT_ENCODED$ = subexp(subexp("%[EFef]" + HEXDIG$$ + "%" + HEXDIG$$ + HEXDIG$$ + "%" + HEXDIG$$ + HEXDIG$$) + "|" + subexp("%[89A-Fa-f]" + HEXDIG$$ + "%" + HEXDIG$$ + HEXDIG$$) + "|" + subexp("%" + HEXDIG$$ + HEXDIG$$)), + //expanded + GEN_DELIMS$$ = "[\\:\\/\\?\\#\\[\\]\\@]", + SUB_DELIMS$$ = "[\\!\\$\\&\\'\\(\\)\\*\\+\\,\\;\\=]", + RESERVED$$ = merge(GEN_DELIMS$$, SUB_DELIMS$$), + UCSCHAR$$ = isIRI ? "[\\xA0-\\u200D\\u2010-\\u2029\\u202F-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF]" : "[]", + //subset, excludes bidi control characters + IPRIVATE$$ = isIRI ? "[\\uE000-\\uF8FF]" : "[]", + //subset + UNRESERVED$$ = merge(ALPHA$$, DIGIT$$, "[\\-\\.\\_\\~]", UCSCHAR$$), + SCHEME$ = subexp(ALPHA$$ + merge(ALPHA$$, DIGIT$$, "[\\+\\-\\.]") + "*"), + USERINFO$ = subexp(subexp(PCT_ENCODED$ + "|" + merge(UNRESERVED$$, SUB_DELIMS$$, "[\\:]")) + "*"), + DEC_OCTET$ = subexp(subexp("25[0-5]") + "|" + subexp("2[0-4]" + DIGIT$$) + "|" + subexp("1" + DIGIT$$ + DIGIT$$) + "|" + subexp("[1-9]" + DIGIT$$) + "|" + DIGIT$$), + DEC_OCTET_RELAXED$ = subexp(subexp("25[0-5]") + "|" + subexp("2[0-4]" + DIGIT$$) + "|" + subexp("1" + DIGIT$$ + DIGIT$$) + "|" + subexp("0?[1-9]" + DIGIT$$) + "|0?0?" + DIGIT$$), + //relaxed parsing rules + IPV4ADDRESS$ = subexp(DEC_OCTET_RELAXED$ + "\\." + DEC_OCTET_RELAXED$ + "\\." + DEC_OCTET_RELAXED$ + "\\." + DEC_OCTET_RELAXED$), + H16$ = subexp(HEXDIG$$ + "{1,4}"), + LS32$ = subexp(subexp(H16$ + "\\:" + H16$) + "|" + IPV4ADDRESS$), + IPV6ADDRESS1$ = subexp(subexp(H16$ + "\\:") + "{6}" + LS32$), + // 6( h16 ":" ) ls32 + IPV6ADDRESS2$ = subexp("\\:\\:" + subexp(H16$ + "\\:") + "{5}" + LS32$), + // "::" 5( h16 ":" ) ls32 + IPV6ADDRESS3$ = subexp(subexp(H16$) + "?\\:\\:" + subexp(H16$ + "\\:") + "{4}" + LS32$), + //[ h16 ] "::" 4( h16 ":" ) ls32 + IPV6ADDRESS4$ = subexp(subexp(subexp(H16$ + "\\:") + "{0,1}" + H16$) + "?\\:\\:" + subexp(H16$ + "\\:") + "{3}" + LS32$), + //[ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32 + IPV6ADDRESS5$ = subexp(subexp(subexp(H16$ + "\\:") + "{0,2}" + H16$) + "?\\:\\:" + subexp(H16$ + "\\:") + "{2}" + LS32$), + //[ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32 + IPV6ADDRESS6$ = subexp(subexp(subexp(H16$ + "\\:") + "{0,3}" + H16$) + "?\\:\\:" + H16$ + "\\:" + LS32$), + //[ *3( h16 ":" ) h16 ] "::" h16 ":" ls32 + IPV6ADDRESS7$ = subexp(subexp(subexp(H16$ + "\\:") + "{0,4}" + H16$) + "?\\:\\:" + LS32$), + //[ *4( h16 ":" ) h16 ] "::" ls32 + IPV6ADDRESS8$ = subexp(subexp(subexp(H16$ + "\\:") + "{0,5}" + H16$) + "?\\:\\:" + H16$), + //[ *5( h16 ":" ) h16 ] "::" h16 + IPV6ADDRESS9$ = subexp(subexp(subexp(H16$ + "\\:") + "{0,6}" + H16$) + "?\\:\\:"), + //[ *6( h16 ":" ) h16 ] "::" + IPV6ADDRESS$ = subexp([IPV6ADDRESS1$, IPV6ADDRESS2$, IPV6ADDRESS3$, IPV6ADDRESS4$, IPV6ADDRESS5$, IPV6ADDRESS6$, IPV6ADDRESS7$, IPV6ADDRESS8$, IPV6ADDRESS9$].join("|")), + ZONEID$ = subexp(subexp(UNRESERVED$$ + "|" + PCT_ENCODED$) + "+"), + //RFC 6874 + IPV6ADDRZ$ = subexp(IPV6ADDRESS$ + "\\%25" + ZONEID$), + //RFC 6874 + IPV6ADDRZ_RELAXED$ = subexp(IPV6ADDRESS$ + subexp("\\%25|\\%(?!" + HEXDIG$$ + "{2})") + ZONEID$), + //RFC 6874, with relaxed parsing rules + IPVFUTURE$ = subexp("[vV]" + HEXDIG$$ + "+\\." + merge(UNRESERVED$$, SUB_DELIMS$$, "[\\:]") + "+"), + IP_LITERAL$ = subexp("\\[" + subexp(IPV6ADDRZ_RELAXED$ + "|" + IPV6ADDRESS$ + "|" + IPVFUTURE$) + "\\]"), + //RFC 6874 + REG_NAME$ = subexp(subexp(PCT_ENCODED$ + "|" + merge(UNRESERVED$$, SUB_DELIMS$$)) + "*"), + HOST$ = subexp(IP_LITERAL$ + "|" + IPV4ADDRESS$ + "(?!" + REG_NAME$ + ")" + "|" + REG_NAME$), + PORT$ = subexp(DIGIT$$ + "*"), + AUTHORITY$ = subexp(subexp(USERINFO$ + "@") + "?" + HOST$ + subexp("\\:" + PORT$) + "?"), + PCHAR$ = subexp(PCT_ENCODED$ + "|" + merge(UNRESERVED$$, SUB_DELIMS$$, "[\\:\\@]")), + SEGMENT$ = subexp(PCHAR$ + "*"), + SEGMENT_NZ$ = subexp(PCHAR$ + "+"), + SEGMENT_NZ_NC$ = subexp(subexp(PCT_ENCODED$ + "|" + merge(UNRESERVED$$, SUB_DELIMS$$, "[\\@]")) + "+"), + PATH_ABEMPTY$ = subexp(subexp("\\/" + SEGMENT$) + "*"), + PATH_ABSOLUTE$ = subexp("\\/" + subexp(SEGMENT_NZ$ + PATH_ABEMPTY$) + "?"), + //simplified + PATH_NOSCHEME$ = subexp(SEGMENT_NZ_NC$ + PATH_ABEMPTY$), + //simplified + PATH_ROOTLESS$ = subexp(SEGMENT_NZ$ + PATH_ABEMPTY$), + //simplified + PATH_EMPTY$ = "(?!" + PCHAR$ + ")", + PATH$ = subexp(PATH_ABEMPTY$ + "|" + PATH_ABSOLUTE$ + "|" + PATH_NOSCHEME$ + "|" + PATH_ROOTLESS$ + "|" + PATH_EMPTY$), + QUERY$ = subexp(subexp(PCHAR$ + "|" + merge("[\\/\\?]", IPRIVATE$$)) + "*"), + FRAGMENT$ = subexp(subexp(PCHAR$ + "|[\\/\\?]") + "*"), + HIER_PART$ = subexp(subexp("\\/\\/" + AUTHORITY$ + PATH_ABEMPTY$) + "|" + PATH_ABSOLUTE$ + "|" + PATH_ROOTLESS$ + "|" + PATH_EMPTY$), + URI$ = subexp(SCHEME$ + "\\:" + HIER_PART$ + subexp("\\?" + QUERY$) + "?" + subexp("\\#" + FRAGMENT$) + "?"), + RELATIVE_PART$ = subexp(subexp("\\/\\/" + AUTHORITY$ + PATH_ABEMPTY$) + "|" + PATH_ABSOLUTE$ + "|" + PATH_NOSCHEME$ + "|" + PATH_EMPTY$), + RELATIVE$ = subexp(RELATIVE_PART$ + subexp("\\?" + QUERY$) + "?" + subexp("\\#" + FRAGMENT$) + "?"), + URI_REFERENCE$ = subexp(URI$ + "|" + RELATIVE$), + ABSOLUTE_URI$ = subexp(SCHEME$ + "\\:" + HIER_PART$ + subexp("\\?" + QUERY$) + "?"), + GENERIC_REF$ = "^(" + SCHEME$ + ")\\:" + subexp(subexp("\\/\\/(" + subexp("(" + USERINFO$ + ")@") + "?(" + HOST$ + ")" + subexp("\\:(" + PORT$ + ")") + "?)") + "?(" + PATH_ABEMPTY$ + "|" + PATH_ABSOLUTE$ + "|" + PATH_ROOTLESS$ + "|" + PATH_EMPTY$ + ")") + subexp("\\?(" + QUERY$ + ")") + "?" + subexp("\\#(" + FRAGMENT$ + ")") + "?$", + RELATIVE_REF$ = "^(){0}" + subexp(subexp("\\/\\/(" + subexp("(" + USERINFO$ + ")@") + "?(" + HOST$ + ")" + subexp("\\:(" + PORT$ + ")") + "?)") + "?(" + PATH_ABEMPTY$ + "|" + PATH_ABSOLUTE$ + "|" + PATH_NOSCHEME$ + "|" + PATH_EMPTY$ + ")") + subexp("\\?(" + QUERY$ + ")") + "?" + subexp("\\#(" + FRAGMENT$ + ")") + "?$", + ABSOLUTE_REF$ = "^(" + SCHEME$ + ")\\:" + subexp(subexp("\\/\\/(" + subexp("(" + USERINFO$ + ")@") + "?(" + HOST$ + ")" + subexp("\\:(" + PORT$ + ")") + "?)") + "?(" + PATH_ABEMPTY$ + "|" + PATH_ABSOLUTE$ + "|" + PATH_ROOTLESS$ + "|" + PATH_EMPTY$ + ")") + subexp("\\?(" + QUERY$ + ")") + "?$", + SAMEDOC_REF$ = "^" + subexp("\\#(" + FRAGMENT$ + ")") + "?$", + AUTHORITY_REF$ = "^" + subexp("(" + USERINFO$ + ")@") + "?(" + HOST$ + ")" + subexp("\\:(" + PORT$ + ")") + "?$"; + return { + NOT_SCHEME: new RegExp(merge("[^]", ALPHA$$, DIGIT$$, "[\\+\\-\\.]"), "g"), + NOT_USERINFO: new RegExp(merge("[^\\%\\:]", UNRESERVED$$, SUB_DELIMS$$), "g"), + NOT_HOST: new RegExp(merge("[^\\%\\[\\]\\:]", UNRESERVED$$, SUB_DELIMS$$), "g"), + NOT_PATH: new RegExp(merge("[^\\%\\/\\:\\@]", UNRESERVED$$, SUB_DELIMS$$), "g"), + NOT_PATH_NOSCHEME: new RegExp(merge("[^\\%\\/\\@]", UNRESERVED$$, SUB_DELIMS$$), "g"), + NOT_QUERY: new RegExp(merge("[^\\%]", UNRESERVED$$, SUB_DELIMS$$, "[\\:\\@\\/\\?]", IPRIVATE$$), "g"), + NOT_FRAGMENT: new RegExp(merge("[^\\%]", UNRESERVED$$, SUB_DELIMS$$, "[\\:\\@\\/\\?]"), "g"), + ESCAPE: new RegExp(merge("[^]", UNRESERVED$$, SUB_DELIMS$$), "g"), + UNRESERVED: new RegExp(UNRESERVED$$, "g"), + OTHER_CHARS: new RegExp(merge("[^\\%]", UNRESERVED$$, RESERVED$$), "g"), + PCT_ENCODED: new RegExp(PCT_ENCODED$, "g"), + IPV4ADDRESS: new RegExp("^(" + IPV4ADDRESS$ + ")$"), + IPV6ADDRESS: new RegExp("^\\[?(" + IPV6ADDRESS$ + ")" + subexp(subexp("\\%25|\\%(?!" + HEXDIG$$ + "{2})") + "(" + ZONEID$ + ")") + "?\\]?$") //RFC 6874, with relaxed parsing rules + }; +} +var URI_PROTOCOL = buildExps(false); + +var IRI_PROTOCOL = buildExps(true); + +var slicedToArray = function () { + function sliceIterator(arr, i) { + var _arr = []; + var _n = true; + var _d = false; + var _e = undefined; + + try { + for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { + _arr.push(_s.value); + + if (i && _arr.length === i) break; + } + } catch (err) { + _d = true; + _e = err; + } finally { + try { + if (!_n && _i["return"]) _i["return"](); + } finally { + if (_d) throw _e; + } + } + + return _arr; + } + + return function (arr, i) { + if (Array.isArray(arr)) { + return arr; + } else if (Symbol.iterator in Object(arr)) { + return sliceIterator(arr, i); + } else { + throw new TypeError("Invalid attempt to destructure non-iterable instance"); + } + }; +}(); + + + + + + + + + + + + + +var toConsumableArray = function (arr) { + if (Array.isArray(arr)) { + for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; + + return arr2; + } else { + return Array.from(arr); + } +}; + +/** Highest positive signed 32-bit float value */ + +var maxInt = 2147483647; // aka. 0x7FFFFFFF or 2^31-1 + +/** Bootstring parameters */ +var base = 36; +var tMin = 1; +var tMax = 26; +var skew = 38; +var damp = 700; +var initialBias = 72; +var initialN = 128; // 0x80 +var delimiter = '-'; // '\x2D' + +/** Regular expressions */ +var regexPunycode = /^xn--/; +var regexNonASCII = /[^\0-\x7E]/; // non-ASCII chars +var regexSeparators = /[\x2E\u3002\uFF0E\uFF61]/g; // RFC 3490 separators + +/** Error messages */ +var errors = { + 'overflow': 'Overflow: input needs wider integers to process', + 'not-basic': 'Illegal input >= 0x80 (not a basic code point)', + 'invalid-input': 'Invalid input' +}; + +/** Convenience shortcuts */ +var baseMinusTMin = base - tMin; +var floor = Math.floor; +var stringFromCharCode = String.fromCharCode; + +/*--------------------------------------------------------------------------*/ + +/** + * A generic error utility function. + * @private + * @param {String} type The error type. + * @returns {Error} Throws a `RangeError` with the applicable error message. + */ +function error$1(type) { + throw new RangeError(errors[type]); +} + +/** + * A generic `Array#map` utility function. + * @private + * @param {Array} array The array to iterate over. + * @param {Function} callback The function that gets called for every array + * item. + * @returns {Array} A new array of values returned by the callback function. + */ +function map(array, fn) { + var result = []; + var length = array.length; + while (length--) { + result[length] = fn(array[length]); + } + return result; +} + +/** + * A simple `Array#map`-like wrapper to work with domain name strings or email + * addresses. + * @private + * @param {String} domain The domain name or email address. + * @param {Function} callback The function that gets called for every + * character. + * @returns {Array} A new string of characters returned by the callback + * function. + */ +function mapDomain(string, fn) { + var parts = string.split('@'); + var result = ''; + if (parts.length > 1) { + // In email addresses, only the domain name should be punycoded. Leave + // the local part (i.e. everything up to `@`) intact. + result = parts[0] + '@'; + string = parts[1]; + } + // Avoid `split(regex)` for IE8 compatibility. See #17. + string = string.replace(regexSeparators, '\x2E'); + var labels = string.split('.'); + var encoded = map(labels, fn).join('.'); + return result + encoded; +} + +/** + * Creates an array containing the numeric code points of each Unicode + * character in the string. While JavaScript uses UCS-2 internally, + * this function will convert a pair of surrogate halves (each of which + * UCS-2 exposes as separate characters) into a single code point, + * matching UTF-16. + * @see `punycode.ucs2.encode` + * @see + * @memberOf punycode.ucs2 + * @name decode + * @param {String} string The Unicode input string (UCS-2). + * @returns {Array} The new array of code points. + */ +function ucs2decode(string) { + var output = []; + var counter = 0; + var length = string.length; + while (counter < length) { + var value = string.charCodeAt(counter++); + if (value >= 0xD800 && value <= 0xDBFF && counter < length) { + // It's a high surrogate, and there is a next character. + var extra = string.charCodeAt(counter++); + if ((extra & 0xFC00) == 0xDC00) { + // Low surrogate. + output.push(((value & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000); + } else { + // It's an unmatched surrogate; only append this code unit, in case the + // next code unit is the high surrogate of a surrogate pair. + output.push(value); + counter--; + } + } else { + output.push(value); + } + } + return output; +} + +/** + * Creates a string based on an array of numeric code points. + * @see `punycode.ucs2.decode` + * @memberOf punycode.ucs2 + * @name encode + * @param {Array} codePoints The array of numeric code points. + * @returns {String} The new Unicode string (UCS-2). + */ +var ucs2encode = function ucs2encode(array) { + return String.fromCodePoint.apply(String, toConsumableArray(array)); +}; + +/** + * Converts a basic code point into a digit/integer. + * @see `digitToBasic()` + * @private + * @param {Number} codePoint The basic numeric code point value. + * @returns {Number} The numeric value of a basic code point (for use in + * representing integers) in the range `0` to `base - 1`, or `base` if + * the code point does not represent a value. + */ +var basicToDigit = function basicToDigit(codePoint) { + if (codePoint - 0x30 < 0x0A) { + return codePoint - 0x16; + } + if (codePoint - 0x41 < 0x1A) { + return codePoint - 0x41; + } + if (codePoint - 0x61 < 0x1A) { + return codePoint - 0x61; + } + return base; +}; + +/** + * Converts a digit/integer into a basic code point. + * @see `basicToDigit()` + * @private + * @param {Number} digit The numeric value of a basic code point. + * @returns {Number} The basic code point whose value (when used for + * representing integers) is `digit`, which needs to be in the range + * `0` to `base - 1`. If `flag` is non-zero, the uppercase form is + * used; else, the lowercase form is used. The behavior is undefined + * if `flag` is non-zero and `digit` has no uppercase form. + */ +var digitToBasic = function digitToBasic(digit, flag) { + // 0..25 map to ASCII a..z or A..Z + // 26..35 map to ASCII 0..9 + return digit + 22 + 75 * (digit < 26) - ((flag != 0) << 5); +}; + +/** + * Bias adaptation function as per section 3.4 of RFC 3492. + * https://tools.ietf.org/html/rfc3492#section-3.4 + * @private + */ +var adapt = function adapt(delta, numPoints, firstTime) { + var k = 0; + delta = firstTime ? floor(delta / damp) : delta >> 1; + delta += floor(delta / numPoints); + for (; /* no initialization */delta > baseMinusTMin * tMax >> 1; k += base) { + delta = floor(delta / baseMinusTMin); + } + return floor(k + (baseMinusTMin + 1) * delta / (delta + skew)); +}; + +/** + * Converts a Punycode string of ASCII-only symbols to a string of Unicode + * symbols. + * @memberOf punycode + * @param {String} input The Punycode string of ASCII-only symbols. + * @returns {String} The resulting string of Unicode symbols. + */ +var decode = function decode(input) { + // Don't use UCS-2. + var output = []; + var inputLength = input.length; + var i = 0; + var n = initialN; + var bias = initialBias; + + // Handle the basic code points: let `basic` be the number of input code + // points before the last delimiter, or `0` if there is none, then copy + // the first basic code points to the output. + + var basic = input.lastIndexOf(delimiter); + if (basic < 0) { + basic = 0; + } + + for (var j = 0; j < basic; ++j) { + // if it's not a basic code point + if (input.charCodeAt(j) >= 0x80) { + error$1('not-basic'); + } + output.push(input.charCodeAt(j)); + } + + // Main decoding loop: start just after the last delimiter if any basic code + // points were copied; start at the beginning otherwise. + + for (var index = basic > 0 ? basic + 1 : 0; index < inputLength;) /* no final expression */{ + + // `index` is the index of the next character to be consumed. + // Decode a generalized variable-length integer into `delta`, + // which gets added to `i`. The overflow checking is easier + // if we increase `i` as we go, then subtract off its starting + // value at the end to obtain `delta`. + var oldi = i; + for (var w = 1, k = base;; /* no condition */k += base) { + + if (index >= inputLength) { + error$1('invalid-input'); + } + + var digit = basicToDigit(input.charCodeAt(index++)); + + if (digit >= base || digit > floor((maxInt - i) / w)) { + error$1('overflow'); + } + + i += digit * w; + var t = k <= bias ? tMin : k >= bias + tMax ? tMax : k - bias; + + if (digit < t) { + break; + } + + var baseMinusT = base - t; + if (w > floor(maxInt / baseMinusT)) { + error$1('overflow'); + } + + w *= baseMinusT; + } + + var out = output.length + 1; + bias = adapt(i - oldi, out, oldi == 0); + + // `i` was supposed to wrap around from `out` to `0`, + // incrementing `n` each time, so we'll fix that now: + if (floor(i / out) > maxInt - n) { + error$1('overflow'); + } + + n += floor(i / out); + i %= out; + + // Insert `n` at position `i` of the output. + output.splice(i++, 0, n); + } + + return String.fromCodePoint.apply(String, output); +}; + +/** + * Converts a string of Unicode symbols (e.g. a domain name label) to a + * Punycode string of ASCII-only symbols. + * @memberOf punycode + * @param {String} input The string of Unicode symbols. + * @returns {String} The resulting Punycode string of ASCII-only symbols. + */ +var encode = function encode(input) { + var output = []; + + // Convert the input in UCS-2 to an array of Unicode code points. + input = ucs2decode(input); + + // Cache the length. + var inputLength = input.length; + + // Initialize the state. + var n = initialN; + var delta = 0; + var bias = initialBias; + + // Handle the basic code points. + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = input[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var _currentValue2 = _step.value; + + if (_currentValue2 < 0x80) { + output.push(stringFromCharCode(_currentValue2)); + } + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + + var basicLength = output.length; + var handledCPCount = basicLength; + + // `handledCPCount` is the number of code points that have been handled; + // `basicLength` is the number of basic code points. + + // Finish the basic string with a delimiter unless it's empty. + if (basicLength) { + output.push(delimiter); + } + + // Main encoding loop: + while (handledCPCount < inputLength) { + + // All non-basic code points < n have been handled already. Find the next + // larger one: + var m = maxInt; + var _iteratorNormalCompletion2 = true; + var _didIteratorError2 = false; + var _iteratorError2 = undefined; + + try { + for (var _iterator2 = input[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { + var currentValue = _step2.value; + + if (currentValue >= n && currentValue < m) { + m = currentValue; + } + } + + // Increase `delta` enough to advance the decoder's state to , + // but guard against overflow. + } catch (err) { + _didIteratorError2 = true; + _iteratorError2 = err; + } finally { + try { + if (!_iteratorNormalCompletion2 && _iterator2.return) { + _iterator2.return(); + } + } finally { + if (_didIteratorError2) { + throw _iteratorError2; + } + } + } + + var handledCPCountPlusOne = handledCPCount + 1; + if (m - n > floor((maxInt - delta) / handledCPCountPlusOne)) { + error$1('overflow'); + } + + delta += (m - n) * handledCPCountPlusOne; + n = m; + + var _iteratorNormalCompletion3 = true; + var _didIteratorError3 = false; + var _iteratorError3 = undefined; + + try { + for (var _iterator3 = input[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { + var _currentValue = _step3.value; + + if (_currentValue < n && ++delta > maxInt) { + error$1('overflow'); + } + if (_currentValue == n) { + // Represent delta as a generalized variable-length integer. + var q = delta; + for (var k = base;; /* no condition */k += base) { + var t = k <= bias ? tMin : k >= bias + tMax ? tMax : k - bias; + if (q < t) { + break; + } + var qMinusT = q - t; + var baseMinusT = base - t; + output.push(stringFromCharCode(digitToBasic(t + qMinusT % baseMinusT, 0))); + q = floor(qMinusT / baseMinusT); + } + + output.push(stringFromCharCode(digitToBasic(q, 0))); + bias = adapt(delta, handledCPCountPlusOne, handledCPCount == basicLength); + delta = 0; + ++handledCPCount; + } + } + } catch (err) { + _didIteratorError3 = true; + _iteratorError3 = err; + } finally { + try { + if (!_iteratorNormalCompletion3 && _iterator3.return) { + _iterator3.return(); + } + } finally { + if (_didIteratorError3) { + throw _iteratorError3; + } + } + } + + ++delta; + ++n; + } + return output.join(''); +}; + +/** + * Converts a Punycode string representing a domain name or an email address + * to Unicode. Only the Punycoded parts of the input will be converted, i.e. + * it doesn't matter if you call it on a string that has already been + * converted to Unicode. + * @memberOf punycode + * @param {String} input The Punycoded domain name or email address to + * convert to Unicode. + * @returns {String} The Unicode representation of the given Punycode + * string. + */ +var toUnicode = function toUnicode(input) { + return mapDomain(input, function (string) { + return regexPunycode.test(string) ? decode(string.slice(4).toLowerCase()) : string; + }); +}; + +/** + * Converts a Unicode string representing a domain name or an email address to + * Punycode. Only the non-ASCII parts of the domain name will be converted, + * i.e. it doesn't matter if you call it with a domain that's already in + * ASCII. + * @memberOf punycode + * @param {String} input The domain name or email address to convert, as a + * Unicode string. + * @returns {String} The Punycode representation of the given domain name or + * email address. + */ +var toASCII = function toASCII(input) { + return mapDomain(input, function (string) { + return regexNonASCII.test(string) ? 'xn--' + encode(string) : string; + }); +}; + +/*--------------------------------------------------------------------------*/ + +/** Define the public API */ +var punycode = { + /** + * A string representing the current Punycode.js version number. + * @memberOf punycode + * @type String + */ + 'version': '2.1.0', + /** + * An object of methods to convert from JavaScript's internal character + * representation (UCS-2) to Unicode code points, and back. + * @see + * @memberOf punycode + * @type Object + */ + 'ucs2': { + 'decode': ucs2decode, + 'encode': ucs2encode + }, + 'decode': decode, + 'encode': encode, + 'toASCII': toASCII, + 'toUnicode': toUnicode +}; + +/** + * URI.js + * + * @fileoverview An RFC 3986 compliant, scheme extendable URI parsing/validating/resolving library for JavaScript. + * @author Gary Court + * @see http://github.com/garycourt/uri-js + */ +/** + * Copyright 2011 Gary Court. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY GARY COURT ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GARY COURT OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of Gary Court. + */ +var SCHEMES = {}; +function pctEncChar(chr) { + var c = chr.charCodeAt(0); + var e = void 0; + if (c < 16) e = "%0" + c.toString(16).toUpperCase();else if (c < 128) e = "%" + c.toString(16).toUpperCase();else if (c < 2048) e = "%" + (c >> 6 | 192).toString(16).toUpperCase() + "%" + (c & 63 | 128).toString(16).toUpperCase();else e = "%" + (c >> 12 | 224).toString(16).toUpperCase() + "%" + (c >> 6 & 63 | 128).toString(16).toUpperCase() + "%" + (c & 63 | 128).toString(16).toUpperCase(); + return e; +} +function pctDecChars(str) { + var newStr = ""; + var i = 0; + var il = str.length; + while (i < il) { + var c = parseInt(str.substr(i + 1, 2), 16); + if (c < 128) { + newStr += String.fromCharCode(c); + i += 3; + } else if (c >= 194 && c < 224) { + if (il - i >= 6) { + var c2 = parseInt(str.substr(i + 4, 2), 16); + newStr += String.fromCharCode((c & 31) << 6 | c2 & 63); + } else { + newStr += str.substr(i, 6); + } + i += 6; + } else if (c >= 224) { + if (il - i >= 9) { + var _c = parseInt(str.substr(i + 4, 2), 16); + var c3 = parseInt(str.substr(i + 7, 2), 16); + newStr += String.fromCharCode((c & 15) << 12 | (_c & 63) << 6 | c3 & 63); + } else { + newStr += str.substr(i, 9); + } + i += 9; + } else { + newStr += str.substr(i, 3); + i += 3; + } + } + return newStr; +} +function _normalizeComponentEncoding(components, protocol) { + function decodeUnreserved(str) { + var decStr = pctDecChars(str); + return !decStr.match(protocol.UNRESERVED) ? str : decStr; + } + if (components.scheme) components.scheme = String(components.scheme).replace(protocol.PCT_ENCODED, decodeUnreserved).toLowerCase().replace(protocol.NOT_SCHEME, ""); + if (components.userinfo !== undefined) components.userinfo = String(components.userinfo).replace(protocol.PCT_ENCODED, decodeUnreserved).replace(protocol.NOT_USERINFO, pctEncChar).replace(protocol.PCT_ENCODED, toUpperCase); + if (components.host !== undefined) components.host = String(components.host).replace(protocol.PCT_ENCODED, decodeUnreserved).toLowerCase().replace(protocol.NOT_HOST, pctEncChar).replace(protocol.PCT_ENCODED, toUpperCase); + if (components.path !== undefined) components.path = String(components.path).replace(protocol.PCT_ENCODED, decodeUnreserved).replace(components.scheme ? protocol.NOT_PATH : protocol.NOT_PATH_NOSCHEME, pctEncChar).replace(protocol.PCT_ENCODED, toUpperCase); + if (components.query !== undefined) components.query = String(components.query).replace(protocol.PCT_ENCODED, decodeUnreserved).replace(protocol.NOT_QUERY, pctEncChar).replace(protocol.PCT_ENCODED, toUpperCase); + if (components.fragment !== undefined) components.fragment = String(components.fragment).replace(protocol.PCT_ENCODED, decodeUnreserved).replace(protocol.NOT_FRAGMENT, pctEncChar).replace(protocol.PCT_ENCODED, toUpperCase); + return components; +} + +function _stripLeadingZeros(str) { + return str.replace(/^0*(.*)/, "$1") || "0"; +} +function _normalizeIPv4(host, protocol) { + var matches = host.match(protocol.IPV4ADDRESS) || []; + + var _matches = slicedToArray(matches, 2), + address = _matches[1]; + + if (address) { + return address.split(".").map(_stripLeadingZeros).join("."); + } else { + return host; + } +} +function _normalizeIPv6(host, protocol) { + var matches = host.match(protocol.IPV6ADDRESS) || []; + + var _matches2 = slicedToArray(matches, 3), + address = _matches2[1], + zone = _matches2[2]; + + if (address) { + var _address$toLowerCase$ = address.toLowerCase().split('::').reverse(), + _address$toLowerCase$2 = slicedToArray(_address$toLowerCase$, 2), + last = _address$toLowerCase$2[0], + first = _address$toLowerCase$2[1]; + + var firstFields = first ? first.split(":").map(_stripLeadingZeros) : []; + var lastFields = last.split(":").map(_stripLeadingZeros); + var isLastFieldIPv4Address = protocol.IPV4ADDRESS.test(lastFields[lastFields.length - 1]); + var fieldCount = isLastFieldIPv4Address ? 7 : 8; + var lastFieldsStart = lastFields.length - fieldCount; + var fields = Array(fieldCount); + for (var x = 0; x < fieldCount; ++x) { + fields[x] = firstFields[x] || lastFields[lastFieldsStart + x] || ''; + } + if (isLastFieldIPv4Address) { + fields[fieldCount - 1] = _normalizeIPv4(fields[fieldCount - 1], protocol); + } + var allZeroFields = fields.reduce(function (acc, field, index) { + if (!field || field === "0") { + var lastLongest = acc[acc.length - 1]; + if (lastLongest && lastLongest.index + lastLongest.length === index) { + lastLongest.length++; + } else { + acc.push({ index: index, length: 1 }); + } + } + return acc; + }, []); + var longestZeroFields = allZeroFields.sort(function (a, b) { + return b.length - a.length; + })[0]; + var newHost = void 0; + if (longestZeroFields && longestZeroFields.length > 1) { + var newFirst = fields.slice(0, longestZeroFields.index); + var newLast = fields.slice(longestZeroFields.index + longestZeroFields.length); + newHost = newFirst.join(":") + "::" + newLast.join(":"); + } else { + newHost = fields.join(":"); + } + if (zone) { + newHost += "%" + zone; + } + return newHost; + } else { + return host; + } +} +var URI_PARSE = /^(?:([^:\/?#]+):)?(?:\/\/((?:([^\/?#@]*)@)?(\[[^\/?#\]]+\]|[^\/?#:]*)(?:\:(\d*))?))?([^?#]*)(?:\?([^#]*))?(?:#((?:.|\n|\r)*))?/i; +var NO_MATCH_IS_UNDEFINED = "".match(/(){0}/)[1] === undefined; +function parse(uriString) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + + var components = {}; + var protocol = options.iri !== false ? IRI_PROTOCOL : URI_PROTOCOL; + if (options.reference === "suffix") uriString = (options.scheme ? options.scheme + ":" : "") + "//" + uriString; + var matches = uriString.match(URI_PARSE); + if (matches) { + if (NO_MATCH_IS_UNDEFINED) { + //store each component + components.scheme = matches[1]; + components.userinfo = matches[3]; + components.host = matches[4]; + components.port = parseInt(matches[5], 10); + components.path = matches[6] || ""; + components.query = matches[7]; + components.fragment = matches[8]; + //fix port number + if (isNaN(components.port)) { + components.port = matches[5]; + } + } else { + //IE FIX for improper RegExp matching + //store each component + components.scheme = matches[1] || undefined; + components.userinfo = uriString.indexOf("@") !== -1 ? matches[3] : undefined; + components.host = uriString.indexOf("//") !== -1 ? matches[4] : undefined; + components.port = parseInt(matches[5], 10); + components.path = matches[6] || ""; + components.query = uriString.indexOf("?") !== -1 ? matches[7] : undefined; + components.fragment = uriString.indexOf("#") !== -1 ? matches[8] : undefined; + //fix port number + if (isNaN(components.port)) { + components.port = uriString.match(/\/\/(?:.|\n)*\:(?:\/|\?|\#|$)/) ? matches[4] : undefined; + } + } + if (components.host) { + //normalize IP hosts + components.host = _normalizeIPv6(_normalizeIPv4(components.host, protocol), protocol); + } + //determine reference type + if (components.scheme === undefined && components.userinfo === undefined && components.host === undefined && components.port === undefined && !components.path && components.query === undefined) { + components.reference = "same-document"; + } else if (components.scheme === undefined) { + components.reference = "relative"; + } else if (components.fragment === undefined) { + components.reference = "absolute"; + } else { + components.reference = "uri"; + } + //check for reference errors + if (options.reference && options.reference !== "suffix" && options.reference !== components.reference) { + components.error = components.error || "URI is not a " + options.reference + " reference."; + } + //find scheme handler + var schemeHandler = SCHEMES[(options.scheme || components.scheme || "").toLowerCase()]; + //check if scheme can't handle IRIs + if (!options.unicodeSupport && (!schemeHandler || !schemeHandler.unicodeSupport)) { + //if host component is a domain name + if (components.host && (options.domainHost || schemeHandler && schemeHandler.domainHost)) { + //convert Unicode IDN -> ASCII IDN + try { + components.host = punycode.toASCII(components.host.replace(protocol.PCT_ENCODED, pctDecChars).toLowerCase()); + } catch (e) { + components.error = components.error || "Host's domain name can not be converted to ASCII via punycode: " + e; + } + } + //convert IRI -> URI + _normalizeComponentEncoding(components, URI_PROTOCOL); + } else { + //normalize encodings + _normalizeComponentEncoding(components, protocol); + } + //perform scheme specific parsing + if (schemeHandler && schemeHandler.parse) { + schemeHandler.parse(components, options); + } + } else { + components.error = components.error || "URI can not be parsed."; + } + return components; +} + +function _recomposeAuthority(components, options) { + var protocol = options.iri !== false ? IRI_PROTOCOL : URI_PROTOCOL; + var uriTokens = []; + if (components.userinfo !== undefined) { + uriTokens.push(components.userinfo); + uriTokens.push("@"); + } + if (components.host !== undefined) { + //normalize IP hosts, add brackets and escape zone separator for IPv6 + uriTokens.push(_normalizeIPv6(_normalizeIPv4(String(components.host), protocol), protocol).replace(protocol.IPV6ADDRESS, function (_, $1, $2) { + return "[" + $1 + ($2 ? "%25" + $2 : "") + "]"; + })); + } + if (typeof components.port === "number") { + uriTokens.push(":"); + uriTokens.push(components.port.toString(10)); + } + return uriTokens.length ? uriTokens.join("") : undefined; +} + +var RDS1 = /^\.\.?\//; +var RDS2 = /^\/\.(\/|$)/; +var RDS3 = /^\/\.\.(\/|$)/; +var RDS5 = /^\/?(?:.|\n)*?(?=\/|$)/; +function removeDotSegments(input) { + var output = []; + while (input.length) { + if (input.match(RDS1)) { + input = input.replace(RDS1, ""); + } else if (input.match(RDS2)) { + input = input.replace(RDS2, "/"); + } else if (input.match(RDS3)) { + input = input.replace(RDS3, "/"); + output.pop(); + } else if (input === "." || input === "..") { + input = ""; + } else { + var im = input.match(RDS5); + if (im) { + var s = im[0]; + input = input.slice(s.length); + output.push(s); + } else { + throw new Error("Unexpected dot segment condition"); + } + } + } + return output.join(""); +} + +function serialize(components) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + + var protocol = options.iri ? IRI_PROTOCOL : URI_PROTOCOL; + var uriTokens = []; + //find scheme handler + var schemeHandler = SCHEMES[(options.scheme || components.scheme || "").toLowerCase()]; + //perform scheme specific serialization + if (schemeHandler && schemeHandler.serialize) schemeHandler.serialize(components, options); + if (components.host) { + //if host component is an IPv6 address + if (protocol.IPV6ADDRESS.test(components.host)) {} + //TODO: normalize IPv6 address as per RFC 5952 + + //if host component is a domain name + else if (options.domainHost || schemeHandler && schemeHandler.domainHost) { + //convert IDN via punycode + try { + components.host = !options.iri ? punycode.toASCII(components.host.replace(protocol.PCT_ENCODED, pctDecChars).toLowerCase()) : punycode.toUnicode(components.host); + } catch (e) { + components.error = components.error || "Host's domain name can not be converted to " + (!options.iri ? "ASCII" : "Unicode") + " via punycode: " + e; + } + } + } + //normalize encoding + _normalizeComponentEncoding(components, protocol); + if (options.reference !== "suffix" && components.scheme) { + uriTokens.push(components.scheme); + uriTokens.push(":"); + } + var authority = _recomposeAuthority(components, options); + if (authority !== undefined) { + if (options.reference !== "suffix") { + uriTokens.push("//"); + } + uriTokens.push(authority); + if (components.path && components.path.charAt(0) !== "/") { + uriTokens.push("/"); + } + } + if (components.path !== undefined) { + var s = components.path; + if (!options.absolutePath && (!schemeHandler || !schemeHandler.absolutePath)) { + s = removeDotSegments(s); + } + if (authority === undefined) { + s = s.replace(/^\/\//, "/%2F"); //don't allow the path to start with "//" + } + uriTokens.push(s); + } + if (components.query !== undefined) { + uriTokens.push("?"); + uriTokens.push(components.query); + } + if (components.fragment !== undefined) { + uriTokens.push("#"); + uriTokens.push(components.fragment); + } + return uriTokens.join(""); //merge tokens into a string +} + +function resolveComponents(base, relative) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + var skipNormalization = arguments[3]; + + var target = {}; + if (!skipNormalization) { + base = parse(serialize(base, options), options); //normalize base components + relative = parse(serialize(relative, options), options); //normalize relative components + } + options = options || {}; + if (!options.tolerant && relative.scheme) { + target.scheme = relative.scheme; + //target.authority = relative.authority; + target.userinfo = relative.userinfo; + target.host = relative.host; + target.port = relative.port; + target.path = removeDotSegments(relative.path || ""); + target.query = relative.query; + } else { + if (relative.userinfo !== undefined || relative.host !== undefined || relative.port !== undefined) { + //target.authority = relative.authority; + target.userinfo = relative.userinfo; + target.host = relative.host; + target.port = relative.port; + target.path = removeDotSegments(relative.path || ""); + target.query = relative.query; + } else { + if (!relative.path) { + target.path = base.path; + if (relative.query !== undefined) { + target.query = relative.query; + } else { + target.query = base.query; + } + } else { + if (relative.path.charAt(0) === "/") { + target.path = removeDotSegments(relative.path); + } else { + if ((base.userinfo !== undefined || base.host !== undefined || base.port !== undefined) && !base.path) { + target.path = "/" + relative.path; + } else if (!base.path) { + target.path = relative.path; + } else { + target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative.path; + } + target.path = removeDotSegments(target.path); + } + target.query = relative.query; + } + //target.authority = base.authority; + target.userinfo = base.userinfo; + target.host = base.host; + target.port = base.port; + } + target.scheme = base.scheme; + } + target.fragment = relative.fragment; + return target; +} + +function resolve(baseURI, relativeURI, options) { + var schemelessOptions = assign({ scheme: 'null' }, options); + return serialize(resolveComponents(parse(baseURI, schemelessOptions), parse(relativeURI, schemelessOptions), schemelessOptions, true), schemelessOptions); +} + +function normalize(uri, options) { + if (typeof uri === "string") { + uri = serialize(parse(uri, options), options); + } else if (typeOf(uri) === "object") { + uri = parse(serialize(uri, options), options); + } + return uri; +} + +function equal(uriA, uriB, options) { + if (typeof uriA === "string") { + uriA = serialize(parse(uriA, options), options); + } else if (typeOf(uriA) === "object") { + uriA = serialize(uriA, options); + } + if (typeof uriB === "string") { + uriB = serialize(parse(uriB, options), options); + } else if (typeOf(uriB) === "object") { + uriB = serialize(uriB, options); + } + return uriA === uriB; +} + +function escapeComponent(str, options) { + return str && str.toString().replace(!options || !options.iri ? URI_PROTOCOL.ESCAPE : IRI_PROTOCOL.ESCAPE, pctEncChar); +} + +function unescapeComponent(str, options) { + return str && str.toString().replace(!options || !options.iri ? URI_PROTOCOL.PCT_ENCODED : IRI_PROTOCOL.PCT_ENCODED, pctDecChars); +} + +var handler = { + scheme: "http", + domainHost: true, + parse: function parse(components, options) { + //report missing host + if (!components.host) { + components.error = components.error || "HTTP URIs must have a host."; + } + return components; + }, + serialize: function serialize(components, options) { + //normalize the default port + if (components.port === (String(components.scheme).toLowerCase() !== "https" ? 80 : 443) || components.port === "") { + components.port = undefined; + } + //normalize the empty path + if (!components.path) { + components.path = "/"; + } + //NOTE: We do not parse query strings for HTTP URIs + //as WWW Form Url Encoded query strings are part of the HTML4+ spec, + //and not the HTTP spec. + return components; + } +}; + +var handler$1 = { + scheme: "https", + domainHost: handler.domainHost, + parse: handler.parse, + serialize: handler.serialize +}; + +var O = {}; +var isIRI = true; +//RFC 3986 +var UNRESERVED$$ = "[A-Za-z0-9\\-\\.\\_\\~" + (isIRI ? "\\xA0-\\u200D\\u2010-\\u2029\\u202F-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF" : "") + "]"; +var HEXDIG$$ = "[0-9A-Fa-f]"; //case-insensitive +var PCT_ENCODED$ = subexp(subexp("%[EFef]" + HEXDIG$$ + "%" + HEXDIG$$ + HEXDIG$$ + "%" + HEXDIG$$ + HEXDIG$$) + "|" + subexp("%[89A-Fa-f]" + HEXDIG$$ + "%" + HEXDIG$$ + HEXDIG$$) + "|" + subexp("%" + HEXDIG$$ + HEXDIG$$)); //expanded +//RFC 5322, except these symbols as per RFC 6068: @ : / ? # [ ] & ; = +//const ATEXT$$ = "[A-Za-z0-9\\!\\#\\$\\%\\&\\'\\*\\+\\-\\/\\=\\?\\^\\_\\`\\{\\|\\}\\~]"; +//const WSP$$ = "[\\x20\\x09]"; +//const OBS_QTEXT$$ = "[\\x01-\\x08\\x0B\\x0C\\x0E-\\x1F\\x7F]"; //(%d1-8 / %d11-12 / %d14-31 / %d127) +//const QTEXT$$ = merge("[\\x21\\x23-\\x5B\\x5D-\\x7E]", OBS_QTEXT$$); //%d33 / %d35-91 / %d93-126 / obs-qtext +//const VCHAR$$ = "[\\x21-\\x7E]"; +//const WSP$$ = "[\\x20\\x09]"; +//const OBS_QP$ = subexp("\\\\" + merge("[\\x00\\x0D\\x0A]", OBS_QTEXT$$)); //%d0 / CR / LF / obs-qtext +//const FWS$ = subexp(subexp(WSP$$ + "*" + "\\x0D\\x0A") + "?" + WSP$$ + "+"); +//const QUOTED_PAIR$ = subexp(subexp("\\\\" + subexp(VCHAR$$ + "|" + WSP$$)) + "|" + OBS_QP$); +//const QUOTED_STRING$ = subexp('\\"' + subexp(FWS$ + "?" + QCONTENT$) + "*" + FWS$ + "?" + '\\"'); +var ATEXT$$ = "[A-Za-z0-9\\!\\$\\%\\'\\*\\+\\-\\^\\_\\`\\{\\|\\}\\~]"; +var QTEXT$$ = "[\\!\\$\\%\\'\\(\\)\\*\\+\\,\\-\\.0-9\\<\\>A-Z\\x5E-\\x7E]"; +var VCHAR$$ = merge(QTEXT$$, "[\\\"\\\\]"); +var SOME_DELIMS$$ = "[\\!\\$\\'\\(\\)\\*\\+\\,\\;\\:\\@]"; +var UNRESERVED = new RegExp(UNRESERVED$$, "g"); +var PCT_ENCODED = new RegExp(PCT_ENCODED$, "g"); +var NOT_LOCAL_PART = new RegExp(merge("[^]", ATEXT$$, "[\\.]", '[\\"]', VCHAR$$), "g"); +var NOT_HFNAME = new RegExp(merge("[^]", UNRESERVED$$, SOME_DELIMS$$), "g"); +var NOT_HFVALUE = NOT_HFNAME; +function decodeUnreserved(str) { + var decStr = pctDecChars(str); + return !decStr.match(UNRESERVED) ? str : decStr; +} +var handler$2 = { + scheme: "mailto", + parse: function parse$$1(components, options) { + var mailtoComponents = components; + var to = mailtoComponents.to = mailtoComponents.path ? mailtoComponents.path.split(",") : []; + mailtoComponents.path = undefined; + if (mailtoComponents.query) { + var unknownHeaders = false; + var headers = {}; + var hfields = mailtoComponents.query.split("&"); + for (var x = 0, xl = hfields.length; x < xl; ++x) { + var hfield = hfields[x].split("="); + switch (hfield[0]) { + case "to": + var toAddrs = hfield[1].split(","); + for (var _x = 0, _xl = toAddrs.length; _x < _xl; ++_x) { + to.push(toAddrs[_x]); + } + break; + case "subject": + mailtoComponents.subject = unescapeComponent(hfield[1], options); + break; + case "body": + mailtoComponents.body = unescapeComponent(hfield[1], options); + break; + default: + unknownHeaders = true; + headers[unescapeComponent(hfield[0], options)] = unescapeComponent(hfield[1], options); + break; + } + } + if (unknownHeaders) mailtoComponents.headers = headers; + } + mailtoComponents.query = undefined; + for (var _x2 = 0, _xl2 = to.length; _x2 < _xl2; ++_x2) { + var addr = to[_x2].split("@"); + addr[0] = unescapeComponent(addr[0]); + if (!options.unicodeSupport) { + //convert Unicode IDN -> ASCII IDN + try { + addr[1] = punycode.toASCII(unescapeComponent(addr[1], options).toLowerCase()); + } catch (e) { + mailtoComponents.error = mailtoComponents.error || "Email address's domain name can not be converted to ASCII via punycode: " + e; + } + } else { + addr[1] = unescapeComponent(addr[1], options).toLowerCase(); + } + to[_x2] = addr.join("@"); + } + return mailtoComponents; + }, + serialize: function serialize$$1(mailtoComponents, options) { + var components = mailtoComponents; + var to = toArray(mailtoComponents.to); + if (to) { + for (var x = 0, xl = to.length; x < xl; ++x) { + var toAddr = String(to[x]); + var atIdx = toAddr.lastIndexOf("@"); + var localPart = toAddr.slice(0, atIdx).replace(PCT_ENCODED, decodeUnreserved).replace(PCT_ENCODED, toUpperCase).replace(NOT_LOCAL_PART, pctEncChar); + var domain = toAddr.slice(atIdx + 1); + //convert IDN via punycode + try { + domain = !options.iri ? punycode.toASCII(unescapeComponent(domain, options).toLowerCase()) : punycode.toUnicode(domain); + } catch (e) { + components.error = components.error || "Email address's domain name can not be converted to " + (!options.iri ? "ASCII" : "Unicode") + " via punycode: " + e; + } + to[x] = localPart + "@" + domain; + } + components.path = to.join(","); + } + var headers = mailtoComponents.headers = mailtoComponents.headers || {}; + if (mailtoComponents.subject) headers["subject"] = mailtoComponents.subject; + if (mailtoComponents.body) headers["body"] = mailtoComponents.body; + var fields = []; + for (var name in headers) { + if (headers[name] !== O[name]) { + fields.push(name.replace(PCT_ENCODED, decodeUnreserved).replace(PCT_ENCODED, toUpperCase).replace(NOT_HFNAME, pctEncChar) + "=" + headers[name].replace(PCT_ENCODED, decodeUnreserved).replace(PCT_ENCODED, toUpperCase).replace(NOT_HFVALUE, pctEncChar)); + } + } + if (fields.length) { + components.query = fields.join("&"); + } + return components; + } +}; + +var URN_PARSE = /^([^\:]+)\:(.*)/; +//RFC 2141 +var handler$3 = { + scheme: "urn", + parse: function parse$$1(components, options) { + var matches = components.path && components.path.match(URN_PARSE); + var urnComponents = components; + if (matches) { + var scheme = options.scheme || urnComponents.scheme || "urn"; + var nid = matches[1].toLowerCase(); + var nss = matches[2]; + var urnScheme = scheme + ":" + (options.nid || nid); + var schemeHandler = SCHEMES[urnScheme]; + urnComponents.nid = nid; + urnComponents.nss = nss; + urnComponents.path = undefined; + if (schemeHandler) { + urnComponents = schemeHandler.parse(urnComponents, options); + } + } else { + urnComponents.error = urnComponents.error || "URN can not be parsed."; + } + return urnComponents; + }, + serialize: function serialize$$1(urnComponents, options) { + var scheme = options.scheme || urnComponents.scheme || "urn"; + var nid = urnComponents.nid; + var urnScheme = scheme + ":" + (options.nid || nid); + var schemeHandler = SCHEMES[urnScheme]; + if (schemeHandler) { + urnComponents = schemeHandler.serialize(urnComponents, options); + } + var uriComponents = urnComponents; + var nss = urnComponents.nss; + uriComponents.path = (nid || options.nid) + ":" + nss; + return uriComponents; + } +}; + +var UUID = /^[0-9A-Fa-f]{8}(?:\-[0-9A-Fa-f]{4}){3}\-[0-9A-Fa-f]{12}$/; +//RFC 4122 +var handler$4 = { + scheme: "urn:uuid", + parse: function parse(urnComponents, options) { + var uuidComponents = urnComponents; + uuidComponents.uuid = uuidComponents.nss; + uuidComponents.nss = undefined; + if (!options.tolerant && (!uuidComponents.uuid || !uuidComponents.uuid.match(UUID))) { + uuidComponents.error = uuidComponents.error || "UUID is not valid."; + } + return uuidComponents; + }, + serialize: function serialize(uuidComponents, options) { + var urnComponents = uuidComponents; + //normalize UUID + urnComponents.nss = (uuidComponents.uuid || "").toLowerCase(); + return urnComponents; + } +}; + +SCHEMES[handler.scheme] = handler; +SCHEMES[handler$1.scheme] = handler$1; +SCHEMES[handler$2.scheme] = handler$2; +SCHEMES[handler$3.scheme] = handler$3; +SCHEMES[handler$4.scheme] = handler$4; + +exports.SCHEMES = SCHEMES; +exports.pctEncChar = pctEncChar; +exports.pctDecChars = pctDecChars; +exports.parse = parse; +exports.removeDotSegments = removeDotSegments; +exports.serialize = serialize; +exports.resolveComponents = resolveComponents; +exports.resolve = resolve; +exports.normalize = normalize; +exports.equal = equal; +exports.escapeComponent = escapeComponent; +exports.unescapeComponent = unescapeComponent; + +Object.defineProperty(exports, '__esModule', { value: true }); + +}))); +//# sourceMappingURL=uri.all.js.map + + +/***/ }), + +/***/ 855: +/***/ (function(module, __unusedexports, __webpack_require__) { + +"use strict"; + + + +module.exports = { + copy: copy, + checkDataType: checkDataType, + checkDataTypes: checkDataTypes, + coerceToTypes: coerceToTypes, + toHash: toHash, + getProperty: getProperty, + escapeQuotes: escapeQuotes, + equal: __webpack_require__(832), + ucs2length: __webpack_require__(691), + varOccurences: varOccurences, + varReplace: varReplace, + cleanUpCode: cleanUpCode, + finalCleanUpCode: finalCleanUpCode, + schemaHasRules: schemaHasRules, + schemaHasRulesExcept: schemaHasRulesExcept, + schemaUnknownRules: schemaUnknownRules, + toQuotedString: toQuotedString, + getPathExpr: getPathExpr, + getPath: getPath, + getData: getData, + unescapeFragment: unescapeFragment, + unescapeJsonPointer: unescapeJsonPointer, + escapeFragment: escapeFragment, + escapeJsonPointer: escapeJsonPointer +}; + + +function copy(o, to) { + to = to || {}; + for (var key in o) to[key] = o[key]; + return to; +} + + +function checkDataType(dataType, data, negate) { + var EQUAL = negate ? ' !== ' : ' === ' + , AND = negate ? ' || ' : ' && ' + , OK = negate ? '!' : '' + , NOT = negate ? '' : '!'; + switch (dataType) { + case 'null': return data + EQUAL + 'null'; + case 'array': return OK + 'Array.isArray(' + data + ')'; + case 'object': return '(' + OK + data + AND + + 'typeof ' + data + EQUAL + '"object"' + AND + + NOT + 'Array.isArray(' + data + '))'; + case 'integer': return '(typeof ' + data + EQUAL + '"number"' + AND + + NOT + '(' + data + ' % 1)' + + AND + data + EQUAL + data + ')'; + default: return 'typeof ' + data + EQUAL + '"' + dataType + '"'; + } +} + + +function checkDataTypes(dataTypes, data) { + switch (dataTypes.length) { + case 1: return checkDataType(dataTypes[0], data, true); + default: + var code = ''; + var types = toHash(dataTypes); + if (types.array && types.object) { + code = types.null ? '(': '(!' + data + ' || '; + code += 'typeof ' + data + ' !== "object")'; + delete types.null; + delete types.array; + delete types.object; + } + if (types.number) delete types.integer; + for (var t in types) + code += (code ? ' && ' : '' ) + checkDataType(t, data, true); + + return code; + } +} + + +var COERCE_TO_TYPES = toHash([ 'string', 'number', 'integer', 'boolean', 'null' ]); +function coerceToTypes(optionCoerceTypes, dataTypes) { + if (Array.isArray(dataTypes)) { + var types = []; + for (var i=0; i= lvl) throw new Error('Cannot access property/index ' + up + ' levels up, current level is ' + lvl); + return paths[lvl - up]; + } + + if (up > lvl) throw new Error('Cannot access data ' + up + ' levels up, current level is ' + lvl); + data = 'data' + ((lvl - up) || ''); + if (!jsonPointer) return data; + } + + var expr = data; + var segments = jsonPointer.split('/'); + for (var i=0; i 0 : it.util.schemaHasRules($propertySch, it.RULES.all)))) { + $required[$required.length] = $property; + } + } + } + } else { + var $required = $schema; + } + } + if ($isData || $required.length) { + var $currentErrorPath = it.errorPath, + $loopRequired = $isData || $required.length >= it.opts.loopRequired, + $ownProperties = it.opts.ownProperties; + if ($breakOnError) { + out += ' var missing' + ($lvl) + '; '; + if ($loopRequired) { + if (!$isData) { + out += ' var ' + ($vSchema) + ' = validate.schema' + ($schemaPath) + '; '; + } + var $i = 'i' + $lvl, + $propertyPath = 'schema' + $lvl + '[' + $i + ']', + $missingProperty = '\' + ' + $propertyPath + ' + \''; + if (it.opts._errorDataPathProperty) { + it.errorPath = it.util.getPathExpr($currentErrorPath, $propertyPath, it.opts.jsonPointers); + } + out += ' var ' + ($valid) + ' = true; '; + if ($isData) { + out += ' if (schema' + ($lvl) + ' === undefined) ' + ($valid) + ' = true; else if (!Array.isArray(schema' + ($lvl) + ')) ' + ($valid) + ' = false; else {'; + } + out += ' for (var ' + ($i) + ' = 0; ' + ($i) + ' < ' + ($vSchema) + '.length; ' + ($i) + '++) { ' + ($valid) + ' = ' + ($data) + '[' + ($vSchema) + '[' + ($i) + ']] !== undefined '; + if ($ownProperties) { + out += ' && Object.prototype.hasOwnProperty.call(' + ($data) + ', ' + ($vSchema) + '[' + ($i) + ']) '; + } + out += '; if (!' + ($valid) + ') break; } '; + if ($isData) { + out += ' } '; + } + out += ' if (!' + ($valid) + ') { '; + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('required') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { missingProperty: \'' + ($missingProperty) + '\' } '; + if (it.opts.messages !== false) { + out += ' , message: \''; + if (it.opts._errorDataPathProperty) { + out += 'is a required property'; + } else { + out += 'should have required property \\\'' + ($missingProperty) + '\\\''; + } + out += '\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + out += ' } else { '; + } else { + out += ' if ( '; + var arr2 = $required; + if (arr2) { + var $propertyKey, $i = -1, + l2 = arr2.length - 1; + while ($i < l2) { + $propertyKey = arr2[$i += 1]; + if ($i) { + out += ' || '; + } + var $prop = it.util.getProperty($propertyKey), + $useData = $data + $prop; + out += ' ( ( ' + ($useData) + ' === undefined '; + if ($ownProperties) { + out += ' || ! Object.prototype.hasOwnProperty.call(' + ($data) + ', \'' + (it.util.escapeQuotes($propertyKey)) + '\') '; + } + out += ') && (missing' + ($lvl) + ' = ' + (it.util.toQuotedString(it.opts.jsonPointers ? $propertyKey : $prop)) + ') ) '; + } + } + out += ') { '; + var $propertyPath = 'missing' + $lvl, + $missingProperty = '\' + ' + $propertyPath + ' + \''; + if (it.opts._errorDataPathProperty) { + it.errorPath = it.opts.jsonPointers ? it.util.getPathExpr($currentErrorPath, $propertyPath, true) : $currentErrorPath + ' + ' + $propertyPath; + } + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('required') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { missingProperty: \'' + ($missingProperty) + '\' } '; + if (it.opts.messages !== false) { + out += ' , message: \''; + if (it.opts._errorDataPathProperty) { + out += 'is a required property'; + } else { + out += 'should have required property \\\'' + ($missingProperty) + '\\\''; + } + out += '\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + out += ' } else { '; + } + } else { + if ($loopRequired) { + if (!$isData) { + out += ' var ' + ($vSchema) + ' = validate.schema' + ($schemaPath) + '; '; + } + var $i = 'i' + $lvl, + $propertyPath = 'schema' + $lvl + '[' + $i + ']', + $missingProperty = '\' + ' + $propertyPath + ' + \''; + if (it.opts._errorDataPathProperty) { + it.errorPath = it.util.getPathExpr($currentErrorPath, $propertyPath, it.opts.jsonPointers); + } + if ($isData) { + out += ' if (' + ($vSchema) + ' && !Array.isArray(' + ($vSchema) + ')) { var err = '; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('required') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { missingProperty: \'' + ($missingProperty) + '\' } '; + if (it.opts.messages !== false) { + out += ' , message: \''; + if (it.opts._errorDataPathProperty) { + out += 'is a required property'; + } else { + out += 'should have required property \\\'' + ($missingProperty) + '\\\''; + } + out += '\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + out += '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; } else if (' + ($vSchema) + ' !== undefined) { '; + } + out += ' for (var ' + ($i) + ' = 0; ' + ($i) + ' < ' + ($vSchema) + '.length; ' + ($i) + '++) { if (' + ($data) + '[' + ($vSchema) + '[' + ($i) + ']] === undefined '; + if ($ownProperties) { + out += ' || ! Object.prototype.hasOwnProperty.call(' + ($data) + ', ' + ($vSchema) + '[' + ($i) + ']) '; + } + out += ') { var err = '; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('required') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { missingProperty: \'' + ($missingProperty) + '\' } '; + if (it.opts.messages !== false) { + out += ' , message: \''; + if (it.opts._errorDataPathProperty) { + out += 'is a required property'; + } else { + out += 'should have required property \\\'' + ($missingProperty) + '\\\''; + } + out += '\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + out += '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; } } '; + if ($isData) { + out += ' } '; + } + } else { + var arr3 = $required; + if (arr3) { + var $propertyKey, i3 = -1, + l3 = arr3.length - 1; + while (i3 < l3) { + $propertyKey = arr3[i3 += 1]; + var $prop = it.util.getProperty($propertyKey), + $missingProperty = it.util.escapeQuotes($propertyKey), + $useData = $data + $prop; + if (it.opts._errorDataPathProperty) { + it.errorPath = it.util.getPath($currentErrorPath, $propertyKey, it.opts.jsonPointers); + } + out += ' if ( ' + ($useData) + ' === undefined '; + if ($ownProperties) { + out += ' || ! Object.prototype.hasOwnProperty.call(' + ($data) + ', \'' + (it.util.escapeQuotes($propertyKey)) + '\') '; + } + out += ') { var err = '; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('required') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { missingProperty: \'' + ($missingProperty) + '\' } '; + if (it.opts.messages !== false) { + out += ' , message: \''; + if (it.opts._errorDataPathProperty) { + out += 'is a required property'; + } else { + out += 'should have required property \\\'' + ($missingProperty) + '\\\''; + } + out += '\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + out += '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; } '; + } + } + } + } + it.errorPath = $currentErrorPath; + } else if ($breakOnError) { + out += ' if (true) {'; + } + return out; +} + + +/***/ }), + +/***/ 866: +/***/ (function(module, __unusedexports, __webpack_require__) { + +// Copyright 2017 Joyent, Inc. + +module.exports = { + read: read, + verify: verify, + sign: sign, + signAsync: signAsync, + write: write +}; + +var assert = __webpack_require__(477); +var asn1 = __webpack_require__(62); +var Buffer = __webpack_require__(215).Buffer; +var algs = __webpack_require__(98); +var utils = __webpack_require__(270); +var Key = __webpack_require__(852); +var PrivateKey = __webpack_require__(502); +var pem = __webpack_require__(268); +var Identity = __webpack_require__(378); +var Signature = __webpack_require__(575); +var Certificate = __webpack_require__(752); +var pkcs8 = __webpack_require__(707); + +/* + * This file is based on RFC5280 (X.509). + */ + +/* Helper to read in a single mpint */ +function readMPInt(der, nm) { + assert.strictEqual(der.peek(), asn1.Ber.Integer, + nm + ' is not an Integer'); + return (utils.mpNormalize(der.readString(asn1.Ber.Integer, true))); +} + +function verify(cert, key) { + var sig = cert.signatures.x509; + assert.object(sig, 'x509 signature'); + + var algParts = sig.algo.split('-'); + if (algParts[0] !== key.type) + return (false); + + var blob = sig.cache; + if (blob === undefined) { + var der = new asn1.BerWriter(); + writeTBSCert(cert, der); + blob = der.buffer; + } + + var verifier = key.createVerify(algParts[1]); + verifier.write(blob); + return (verifier.verify(sig.signature)); +} + +function Local(i) { + return (asn1.Ber.Context | asn1.Ber.Constructor | i); +} + +function Context(i) { + return (asn1.Ber.Context | i); +} + +var SIGN_ALGS = { + 'rsa-md5': '1.2.840.113549.1.1.4', + 'rsa-sha1': '1.2.840.113549.1.1.5', + 'rsa-sha256': '1.2.840.113549.1.1.11', + 'rsa-sha384': '1.2.840.113549.1.1.12', + 'rsa-sha512': '1.2.840.113549.1.1.13', + 'dsa-sha1': '1.2.840.10040.4.3', + 'dsa-sha256': '2.16.840.1.101.3.4.3.2', + 'ecdsa-sha1': '1.2.840.10045.4.1', + 'ecdsa-sha256': '1.2.840.10045.4.3.2', + 'ecdsa-sha384': '1.2.840.10045.4.3.3', + 'ecdsa-sha512': '1.2.840.10045.4.3.4', + 'ed25519-sha512': '1.3.101.112' +}; +Object.keys(SIGN_ALGS).forEach(function (k) { + SIGN_ALGS[SIGN_ALGS[k]] = k; +}); +SIGN_ALGS['1.3.14.3.2.3'] = 'rsa-md5'; +SIGN_ALGS['1.3.14.3.2.29'] = 'rsa-sha1'; + +var EXTS = { + 'issuerKeyId': '2.5.29.35', + 'altName': '2.5.29.17', + 'basicConstraints': '2.5.29.19', + 'keyUsage': '2.5.29.15', + 'extKeyUsage': '2.5.29.37' +}; + +function read(buf, options) { + if (typeof (buf) === 'string') { + buf = Buffer.from(buf, 'binary'); + } + assert.buffer(buf, 'buf'); + + var der = new asn1.BerReader(buf); + + der.readSequence(); + if (Math.abs(der.length - der.remain) > 1) { + throw (new Error('DER sequence does not contain whole byte ' + + 'stream')); + } + + var tbsStart = der.offset; + der.readSequence(); + var sigOffset = der.offset + der.length; + var tbsEnd = sigOffset; + + if (der.peek() === Local(0)) { + der.readSequence(Local(0)); + var version = der.readInt(); + assert.ok(version <= 3, + 'only x.509 versions up to v3 supported'); + } + + var cert = {}; + cert.signatures = {}; + var sig = (cert.signatures.x509 = {}); + sig.extras = {}; + + cert.serial = readMPInt(der, 'serial'); + + der.readSequence(); + var after = der.offset + der.length; + var certAlgOid = der.readOID(); + var certAlg = SIGN_ALGS[certAlgOid]; + if (certAlg === undefined) + throw (new Error('unknown signature algorithm ' + certAlgOid)); + + der._offset = after; + cert.issuer = Identity.parseAsn1(der); + + der.readSequence(); + cert.validFrom = readDate(der); + cert.validUntil = readDate(der); + + cert.subjects = [Identity.parseAsn1(der)]; + + der.readSequence(); + after = der.offset + der.length; + cert.subjectKey = pkcs8.readPkcs8(undefined, 'public', der); + der._offset = after; + + /* issuerUniqueID */ + if (der.peek() === Local(1)) { + der.readSequence(Local(1)); + sig.extras.issuerUniqueID = + buf.slice(der.offset, der.offset + der.length); + der._offset += der.length; + } + + /* subjectUniqueID */ + if (der.peek() === Local(2)) { + der.readSequence(Local(2)); + sig.extras.subjectUniqueID = + buf.slice(der.offset, der.offset + der.length); + der._offset += der.length; + } + + /* extensions */ + if (der.peek() === Local(3)) { + der.readSequence(Local(3)); + var extEnd = der.offset + der.length; + der.readSequence(); + + while (der.offset < extEnd) + readExtension(cert, buf, der); + + assert.strictEqual(der.offset, extEnd); + } + + assert.strictEqual(der.offset, sigOffset); + + der.readSequence(); + after = der.offset + der.length; + var sigAlgOid = der.readOID(); + var sigAlg = SIGN_ALGS[sigAlgOid]; + if (sigAlg === undefined) + throw (new Error('unknown signature algorithm ' + sigAlgOid)); + der._offset = after; + + var sigData = der.readString(asn1.Ber.BitString, true); + if (sigData[0] === 0) + sigData = sigData.slice(1); + var algParts = sigAlg.split('-'); + + sig.signature = Signature.parse(sigData, algParts[0], 'asn1'); + sig.signature.hashAlgorithm = algParts[1]; + sig.algo = sigAlg; + sig.cache = buf.slice(tbsStart, tbsEnd); + + return (new Certificate(cert)); +} + +function readDate(der) { + if (der.peek() === asn1.Ber.UTCTime) { + return (utcTimeToDate(der.readString(asn1.Ber.UTCTime))); + } else if (der.peek() === asn1.Ber.GeneralizedTime) { + return (gTimeToDate(der.readString(asn1.Ber.GeneralizedTime))); + } else { + throw (new Error('Unsupported date format')); + } +} + +function writeDate(der, date) { + if (date.getUTCFullYear() >= 2050 || date.getUTCFullYear() < 1950) { + der.writeString(dateToGTime(date), asn1.Ber.GeneralizedTime); + } else { + der.writeString(dateToUTCTime(date), asn1.Ber.UTCTime); + } +} + +/* RFC5280, section 4.2.1.6 (GeneralName type) */ +var ALTNAME = { + OtherName: Local(0), + RFC822Name: Context(1), + DNSName: Context(2), + X400Address: Local(3), + DirectoryName: Local(4), + EDIPartyName: Local(5), + URI: Context(6), + IPAddress: Context(7), + OID: Context(8) +}; + +/* RFC5280, section 4.2.1.12 (KeyPurposeId) */ +var EXTPURPOSE = { + 'serverAuth': '1.3.6.1.5.5.7.3.1', + 'clientAuth': '1.3.6.1.5.5.7.3.2', + 'codeSigning': '1.3.6.1.5.5.7.3.3', + + /* See https://github.com/joyent/oid-docs/blob/master/root.md */ + 'joyentDocker': '1.3.6.1.4.1.38678.1.4.1', + 'joyentCmon': '1.3.6.1.4.1.38678.1.4.2' +}; +var EXTPURPOSE_REV = {}; +Object.keys(EXTPURPOSE).forEach(function (k) { + EXTPURPOSE_REV[EXTPURPOSE[k]] = k; +}); + +var KEYUSEBITS = [ + 'signature', 'identity', 'keyEncryption', + 'encryption', 'keyAgreement', 'ca', 'crl' +]; + +function readExtension(cert, buf, der) { + der.readSequence(); + var after = der.offset + der.length; + var extId = der.readOID(); + var id; + var sig = cert.signatures.x509; + if (!sig.extras.exts) + sig.extras.exts = []; + + var critical; + if (der.peek() === asn1.Ber.Boolean) + critical = der.readBoolean(); + + switch (extId) { + case (EXTS.basicConstraints): + der.readSequence(asn1.Ber.OctetString); + der.readSequence(); + var bcEnd = der.offset + der.length; + var ca = false; + if (der.peek() === asn1.Ber.Boolean) + ca = der.readBoolean(); + if (cert.purposes === undefined) + cert.purposes = []; + if (ca === true) + cert.purposes.push('ca'); + var bc = { oid: extId, critical: critical }; + if (der.offset < bcEnd && der.peek() === asn1.Ber.Integer) + bc.pathLen = der.readInt(); + sig.extras.exts.push(bc); + break; + case (EXTS.extKeyUsage): + der.readSequence(asn1.Ber.OctetString); + der.readSequence(); + if (cert.purposes === undefined) + cert.purposes = []; + var ekEnd = der.offset + der.length; + while (der.offset < ekEnd) { + var oid = der.readOID(); + cert.purposes.push(EXTPURPOSE_REV[oid] || oid); + } + /* + * This is a bit of a hack: in the case where we have a cert + * that's only allowed to do serverAuth or clientAuth (and not + * the other), we want to make sure all our Subjects are of + * the right type. But we already parsed our Subjects and + * decided if they were hosts or users earlier (since it appears + * first in the cert). + * + * So we go through and mutate them into the right kind here if + * it doesn't match. This might not be hugely beneficial, as it + * seems that single-purpose certs are not often seen in the + * wild. + */ + if (cert.purposes.indexOf('serverAuth') !== -1 && + cert.purposes.indexOf('clientAuth') === -1) { + cert.subjects.forEach(function (ide) { + if (ide.type !== 'host') { + ide.type = 'host'; + ide.hostname = ide.uid || + ide.email || + ide.components[0].value; + } + }); + } else if (cert.purposes.indexOf('clientAuth') !== -1 && + cert.purposes.indexOf('serverAuth') === -1) { + cert.subjects.forEach(function (ide) { + if (ide.type !== 'user') { + ide.type = 'user'; + ide.uid = ide.hostname || + ide.email || + ide.components[0].value; + } + }); + } + sig.extras.exts.push({ oid: extId, critical: critical }); + break; + case (EXTS.keyUsage): + der.readSequence(asn1.Ber.OctetString); + var bits = der.readString(asn1.Ber.BitString, true); + var setBits = readBitField(bits, KEYUSEBITS); + setBits.forEach(function (bit) { + if (cert.purposes === undefined) + cert.purposes = []; + if (cert.purposes.indexOf(bit) === -1) + cert.purposes.push(bit); + }); + sig.extras.exts.push({ oid: extId, critical: critical, + bits: bits }); + break; + case (EXTS.altName): + der.readSequence(asn1.Ber.OctetString); + der.readSequence(); + var aeEnd = der.offset + der.length; + while (der.offset < aeEnd) { + switch (der.peek()) { + case ALTNAME.OtherName: + case ALTNAME.EDIPartyName: + der.readSequence(); + der._offset += der.length; + break; + case ALTNAME.OID: + der.readOID(ALTNAME.OID); + break; + case ALTNAME.RFC822Name: + /* RFC822 specifies email addresses */ + var email = der.readString(ALTNAME.RFC822Name); + id = Identity.forEmail(email); + if (!cert.subjects[0].equals(id)) + cert.subjects.push(id); + break; + case ALTNAME.DirectoryName: + der.readSequence(ALTNAME.DirectoryName); + id = Identity.parseAsn1(der); + if (!cert.subjects[0].equals(id)) + cert.subjects.push(id); + break; + case ALTNAME.DNSName: + var host = der.readString( + ALTNAME.DNSName); + id = Identity.forHost(host); + if (!cert.subjects[0].equals(id)) + cert.subjects.push(id); + break; + default: + der.readString(der.peek()); + break; + } + } + sig.extras.exts.push({ oid: extId, critical: critical }); + break; + default: + sig.extras.exts.push({ + oid: extId, + critical: critical, + data: der.readString(asn1.Ber.OctetString, true) + }); + break; + } + + der._offset = after; +} + +var UTCTIME_RE = + /^([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})?Z$/; +function utcTimeToDate(t) { + var m = t.match(UTCTIME_RE); + assert.ok(m, 'timestamps must be in UTC'); + var d = new Date(); + + var thisYear = d.getUTCFullYear(); + var century = Math.floor(thisYear / 100) * 100; + + var year = parseInt(m[1], 10); + if (thisYear % 100 < 50 && year >= 60) + year += (century - 1); + else + year += century; + d.setUTCFullYear(year, parseInt(m[2], 10) - 1, parseInt(m[3], 10)); + d.setUTCHours(parseInt(m[4], 10), parseInt(m[5], 10)); + if (m[6] && m[6].length > 0) + d.setUTCSeconds(parseInt(m[6], 10)); + return (d); +} + +var GTIME_RE = + /^([0-9]{4})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})?Z$/; +function gTimeToDate(t) { + var m = t.match(GTIME_RE); + assert.ok(m); + var d = new Date(); + + d.setUTCFullYear(parseInt(m[1], 10), parseInt(m[2], 10) - 1, + parseInt(m[3], 10)); + d.setUTCHours(parseInt(m[4], 10), parseInt(m[5], 10)); + if (m[6] && m[6].length > 0) + d.setUTCSeconds(parseInt(m[6], 10)); + return (d); +} + +function zeroPad(n, m) { + if (m === undefined) + m = 2; + var s = '' + n; + while (s.length < m) + s = '0' + s; + return (s); +} + +function dateToUTCTime(d) { + var s = ''; + s += zeroPad(d.getUTCFullYear() % 100); + s += zeroPad(d.getUTCMonth() + 1); + s += zeroPad(d.getUTCDate()); + s += zeroPad(d.getUTCHours()); + s += zeroPad(d.getUTCMinutes()); + s += zeroPad(d.getUTCSeconds()); + s += 'Z'; + return (s); +} + +function dateToGTime(d) { + var s = ''; + s += zeroPad(d.getUTCFullYear(), 4); + s += zeroPad(d.getUTCMonth() + 1); + s += zeroPad(d.getUTCDate()); + s += zeroPad(d.getUTCHours()); + s += zeroPad(d.getUTCMinutes()); + s += zeroPad(d.getUTCSeconds()); + s += 'Z'; + return (s); +} + +function sign(cert, key) { + if (cert.signatures.x509 === undefined) + cert.signatures.x509 = {}; + var sig = cert.signatures.x509; + + sig.algo = key.type + '-' + key.defaultHashAlgorithm(); + if (SIGN_ALGS[sig.algo] === undefined) + return (false); + + var der = new asn1.BerWriter(); + writeTBSCert(cert, der); + var blob = der.buffer; + sig.cache = blob; + + var signer = key.createSign(); + signer.write(blob); + cert.signatures.x509.signature = signer.sign(); + + return (true); +} + +function signAsync(cert, signer, done) { + if (cert.signatures.x509 === undefined) + cert.signatures.x509 = {}; + var sig = cert.signatures.x509; + + var der = new asn1.BerWriter(); + writeTBSCert(cert, der); + var blob = der.buffer; + sig.cache = blob; + + signer(blob, function (err, signature) { + if (err) { + done(err); + return; + } + sig.algo = signature.type + '-' + signature.hashAlgorithm; + if (SIGN_ALGS[sig.algo] === undefined) { + done(new Error('Invalid signing algorithm "' + + sig.algo + '"')); + return; + } + sig.signature = signature; + done(); + }); +} + +function write(cert, options) { + var sig = cert.signatures.x509; + assert.object(sig, 'x509 signature'); + + var der = new asn1.BerWriter(); + der.startSequence(); + if (sig.cache) { + der._ensure(sig.cache.length); + sig.cache.copy(der._buf, der._offset); + der._offset += sig.cache.length; + } else { + writeTBSCert(cert, der); + } + + der.startSequence(); + der.writeOID(SIGN_ALGS[sig.algo]); + if (sig.algo.match(/^rsa-/)) + der.writeNull(); + der.endSequence(); + + var sigData = sig.signature.toBuffer('asn1'); + var data = Buffer.alloc(sigData.length + 1); + data[0] = 0; + sigData.copy(data, 1); + der.writeBuffer(data, asn1.Ber.BitString); + der.endSequence(); + + return (der.buffer); +} + +function writeTBSCert(cert, der) { + var sig = cert.signatures.x509; + assert.object(sig, 'x509 signature'); + + der.startSequence(); + + der.startSequence(Local(0)); + der.writeInt(2); + der.endSequence(); + + der.writeBuffer(utils.mpNormalize(cert.serial), asn1.Ber.Integer); + + der.startSequence(); + der.writeOID(SIGN_ALGS[sig.algo]); + if (sig.algo.match(/^rsa-/)) + der.writeNull(); + der.endSequence(); + + cert.issuer.toAsn1(der); + + der.startSequence(); + writeDate(der, cert.validFrom); + writeDate(der, cert.validUntil); + der.endSequence(); + + var subject = cert.subjects[0]; + var altNames = cert.subjects.slice(1); + subject.toAsn1(der); + + pkcs8.writePkcs8(der, cert.subjectKey); + + if (sig.extras && sig.extras.issuerUniqueID) { + der.writeBuffer(sig.extras.issuerUniqueID, Local(1)); + } + + if (sig.extras && sig.extras.subjectUniqueID) { + der.writeBuffer(sig.extras.subjectUniqueID, Local(2)); + } + + if (altNames.length > 0 || subject.type === 'host' || + (cert.purposes !== undefined && cert.purposes.length > 0) || + (sig.extras && sig.extras.exts)) { + der.startSequence(Local(3)); + der.startSequence(); + + var exts = []; + if (cert.purposes !== undefined && cert.purposes.length > 0) { + exts.push({ + oid: EXTS.basicConstraints, + critical: true + }); + exts.push({ + oid: EXTS.keyUsage, + critical: true + }); + exts.push({ + oid: EXTS.extKeyUsage, + critical: true + }); + } + exts.push({ oid: EXTS.altName }); + if (sig.extras && sig.extras.exts) + exts = sig.extras.exts; + + for (var i = 0; i < exts.length; ++i) { + der.startSequence(); + der.writeOID(exts[i].oid); + + if (exts[i].critical !== undefined) + der.writeBoolean(exts[i].critical); + + if (exts[i].oid === EXTS.altName) { + der.startSequence(asn1.Ber.OctetString); + der.startSequence(); + if (subject.type === 'host') { + der.writeString(subject.hostname, + Context(2)); + } + for (var j = 0; j < altNames.length; ++j) { + if (altNames[j].type === 'host') { + der.writeString( + altNames[j].hostname, + ALTNAME.DNSName); + } else if (altNames[j].type === + 'email') { + der.writeString( + altNames[j].email, + ALTNAME.RFC822Name); + } else { + /* + * Encode anything else as a + * DN style name for now. + */ + der.startSequence( + ALTNAME.DirectoryName); + altNames[j].toAsn1(der); + der.endSequence(); + } + } + der.endSequence(); + der.endSequence(); + } else if (exts[i].oid === EXTS.basicConstraints) { + der.startSequence(asn1.Ber.OctetString); + der.startSequence(); + var ca = (cert.purposes.indexOf('ca') !== -1); + var pathLen = exts[i].pathLen; + der.writeBoolean(ca); + if (pathLen !== undefined) + der.writeInt(pathLen); + der.endSequence(); + der.endSequence(); + } else if (exts[i].oid === EXTS.extKeyUsage) { + der.startSequence(asn1.Ber.OctetString); + der.startSequence(); + cert.purposes.forEach(function (purpose) { + if (purpose === 'ca') + return; + if (KEYUSEBITS.indexOf(purpose) !== -1) + return; + var oid = purpose; + if (EXTPURPOSE[purpose] !== undefined) + oid = EXTPURPOSE[purpose]; + der.writeOID(oid); + }); + der.endSequence(); + der.endSequence(); + } else if (exts[i].oid === EXTS.keyUsage) { + der.startSequence(asn1.Ber.OctetString); + /* + * If we parsed this certificate from a byte + * stream (i.e. we didn't generate it in sshpk) + * then we'll have a ".bits" property on the + * ext with the original raw byte contents. + * + * If we have this, use it here instead of + * regenerating it. This guarantees we output + * the same data we parsed, so signatures still + * validate. + */ + if (exts[i].bits !== undefined) { + der.writeBuffer(exts[i].bits, + asn1.Ber.BitString); + } else { + var bits = writeBitField(cert.purposes, + KEYUSEBITS); + der.writeBuffer(bits, + asn1.Ber.BitString); + } + der.endSequence(); + } else { + der.writeBuffer(exts[i].data, + asn1.Ber.OctetString); + } + + der.endSequence(); + } + + der.endSequence(); + der.endSequence(); + } + + der.endSequence(); +} + +/* + * Reads an ASN.1 BER bitfield out of the Buffer produced by doing + * `BerReader#readString(asn1.Ber.BitString)`. That function gives us the raw + * contents of the BitString tag, which is a count of unused bits followed by + * the bits as a right-padded byte string. + * + * `bits` is the Buffer, `bitIndex` should contain an array of string names + * for the bits in the string, ordered starting with bit #0 in the ASN.1 spec. + * + * Returns an array of Strings, the names of the bits that were set to 1. + */ +function readBitField(bits, bitIndex) { + var bitLen = 8 * (bits.length - 1) - bits[0]; + var setBits = {}; + for (var i = 0; i < bitLen; ++i) { + var byteN = 1 + Math.floor(i / 8); + var bit = 7 - (i % 8); + var mask = 1 << bit; + var bitVal = ((bits[byteN] & mask) !== 0); + var name = bitIndex[i]; + if (bitVal && typeof (name) === 'string') { + setBits[name] = true; + } + } + return (Object.keys(setBits)); +} + +/* + * `setBits` is an array of strings, containing the names for each bit that + * sould be set to 1. `bitIndex` is same as in `readBitField()`. + * + * Returns a Buffer, ready to be written out with `BerWriter#writeString()`. + */ +function writeBitField(setBits, bitIndex) { + var bitLen = bitIndex.length; + var blen = Math.ceil(bitLen / 8); + var unused = blen * 8 - bitLen; + var bits = Buffer.alloc(1 + blen); // zero-filled + bits[0] = unused; + for (var i = 0; i < bitLen; ++i) { + var byteN = 1 + Math.floor(i / 8); + var bit = 7 - (i % 8); + var mask = 1 << bit; + var name = bitIndex[i]; + if (name === undefined) + continue; + var bitVal = (setBits.indexOf(name) !== -1); + if (bitVal) { + bits[byteN] |= mask; + } + } + return (bits); +} + + +/***/ }), + +/***/ 867: +/***/ (function(module, __unusedexports, __webpack_require__) { + +"use strict"; + + +var URI = __webpack_require__(853) + , equal = __webpack_require__(832) + , util = __webpack_require__(855) + , SchemaObject = __webpack_require__(955) + , traverse = __webpack_require__(340); + +module.exports = resolve; + +resolve.normalizeId = normalizeId; +resolve.fullPath = getFullPath; +resolve.url = resolveUrl; +resolve.ids = resolveIds; +resolve.inlineRef = inlineRef; +resolve.schema = resolveSchema; + +/** + * [resolve and compile the references ($ref)] + * @this Ajv + * @param {Function} compile reference to schema compilation funciton (localCompile) + * @param {Object} root object with information about the root schema for the current schema + * @param {String} ref reference to resolve + * @return {Object|Function} schema object (if the schema can be inlined) or validation function + */ +function resolve(compile, root, ref) { + /* jshint validthis: true */ + var refVal = this._refs[ref]; + if (typeof refVal == 'string') { + if (this._refs[refVal]) refVal = this._refs[refVal]; + else return resolve.call(this, compile, root, refVal); + } + + refVal = refVal || this._schemas[ref]; + if (refVal instanceof SchemaObject) { + return inlineRef(refVal.schema, this._opts.inlineRefs) + ? refVal.schema + : refVal.validate || this._compile(refVal); + } + + var res = resolveSchema.call(this, root, ref); + var schema, v, baseId; + if (res) { + schema = res.schema; + root = res.root; + baseId = res.baseId; + } + + if (schema instanceof SchemaObject) { + v = schema.validate || compile.call(this, schema.schema, root, undefined, baseId); + } else if (schema !== undefined) { + v = inlineRef(schema, this._opts.inlineRefs) + ? schema + : compile.call(this, schema, root, undefined, baseId); + } + + return v; +} + + +/** + * Resolve schema, its root and baseId + * @this Ajv + * @param {Object} root root object with properties schema, refVal, refs + * @param {String} ref reference to resolve + * @return {Object} object with properties schema, root, baseId + */ +function resolveSchema(root, ref) { + /* jshint validthis: true */ + var p = URI.parse(ref) + , refPath = _getFullPath(p) + , baseId = getFullPath(this._getId(root.schema)); + if (Object.keys(root.schema).length === 0 || refPath !== baseId) { + var id = normalizeId(refPath); + var refVal = this._refs[id]; + if (typeof refVal == 'string') { + return resolveRecursive.call(this, root, refVal, p); + } else if (refVal instanceof SchemaObject) { + if (!refVal.validate) this._compile(refVal); + root = refVal; + } else { + refVal = this._schemas[id]; + if (refVal instanceof SchemaObject) { + if (!refVal.validate) this._compile(refVal); + if (id == normalizeId(ref)) + return { schema: refVal, root: root, baseId: baseId }; + root = refVal; + } else { + return; + } + } + if (!root.schema) return; + baseId = getFullPath(this._getId(root.schema)); + } + return getJsonPointer.call(this, p, baseId, root.schema, root); +} + + +/* @this Ajv */ +function resolveRecursive(root, ref, parsedRef) { + /* jshint validthis: true */ + var res = resolveSchema.call(this, root, ref); + if (res) { + var schema = res.schema; + var baseId = res.baseId; + root = res.root; + var id = this._getId(schema); + if (id) baseId = resolveUrl(baseId, id); + return getJsonPointer.call(this, parsedRef, baseId, schema, root); + } +} + + +var PREVENT_SCOPE_CHANGE = util.toHash(['properties', 'patternProperties', 'enum', 'dependencies', 'definitions']); +/* @this Ajv */ +function getJsonPointer(parsedRef, baseId, schema, root) { + /* jshint validthis: true */ + parsedRef.fragment = parsedRef.fragment || ''; + if (parsedRef.fragment.slice(0,1) != '/') return; + var parts = parsedRef.fragment.split('/'); + + for (var i = 1; i < parts.length; i++) { + var part = parts[i]; + if (part) { + part = util.unescapeFragment(part); + schema = schema[part]; + if (schema === undefined) break; + var id; + if (!PREVENT_SCOPE_CHANGE[part]) { + id = this._getId(schema); + if (id) baseId = resolveUrl(baseId, id); + if (schema.$ref) { + var $ref = resolveUrl(baseId, schema.$ref); + var res = resolveSchema.call(this, root, $ref); + if (res) { + schema = res.schema; + root = res.root; + baseId = res.baseId; + } + } + } + } + } + if (schema !== undefined && schema !== root.schema) + return { schema: schema, root: root, baseId: baseId }; +} + + +var SIMPLE_INLINED = util.toHash([ + 'type', 'format', 'pattern', + 'maxLength', 'minLength', + 'maxProperties', 'minProperties', + 'maxItems', 'minItems', + 'maximum', 'minimum', + 'uniqueItems', 'multipleOf', + 'required', 'enum' +]); +function inlineRef(schema, limit) { + if (limit === false) return false; + if (limit === undefined || limit === true) return checkNoRef(schema); + else if (limit) return countKeys(schema) <= limit; +} + + +function checkNoRef(schema) { + var item; + if (Array.isArray(schema)) { + for (var i=0; i%\\^`{|}]|%[0-9a-f]{2})|\{[+#./;?&=,!@|]?(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?(?:,(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?)*\})*$/i; +// For the source: https://gist.github.com/dperini/729294 +// For test cases: https://mathiasbynens.be/demo/url-regex +// @todo Delete current URL in favour of the commented out URL rule when this issue is fixed https://github.com/eslint/eslint/issues/7983. +// var URL = /^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\d{1,3}){3})(?!127(?:\.\d{1,3}){3})(?!169\.254(?:\.\d{1,3}){2})(?!192\.168(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u{00a1}-\u{ffff}0-9]+-?)*[a-z\u{00a1}-\u{ffff}0-9]+)(?:\.(?:[a-z\u{00a1}-\u{ffff}0-9]+-?)*[a-z\u{00a1}-\u{ffff}0-9]+)*(?:\.(?:[a-z\u{00a1}-\u{ffff}]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?$/iu; +var URL = /^(?:(?:http[s\u017F]?|ftp):\/\/)(?:(?:[\0-\x08\x0E-\x1F!-\x9F\xA1-\u167F\u1681-\u1FFF\u200B-\u2027\u202A-\u202E\u2030-\u205E\u2060-\u2FFF\u3001-\uD7FF\uE000-\uFEFE\uFF00-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+(?::(?:[\0-\x08\x0E-\x1F!-\x9F\xA1-\u167F\u1681-\u1FFF\u200B-\u2027\u202A-\u202E\u2030-\u205E\u2060-\u2FFF\u3001-\uD7FF\uE000-\uFEFE\uFF00-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])*)?@)?(?:(?!10(?:\.[0-9]{1,3}){3})(?!127(?:\.[0-9]{1,3}){3})(?!169\.254(?:\.[0-9]{1,3}){2})(?!192\.168(?:\.[0-9]{1,3}){2})(?!172\.(?:1[6-9]|2[0-9]|3[01])(?:\.[0-9]{1,3}){2})(?:[1-9][0-9]?|1[0-9][0-9]|2[01][0-9]|22[0-3])(?:\.(?:1?[0-9]{1,2}|2[0-4][0-9]|25[0-5])){2}(?:\.(?:[1-9][0-9]?|1[0-9][0-9]|2[0-4][0-9]|25[0-4]))|(?:(?:(?:[0-9KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+-?)*(?:[0-9KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+)(?:\.(?:(?:[0-9KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+-?)*(?:[0-9KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])+)*(?:\.(?:(?:[KSa-z\xA1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]){2,})))(?::[0-9]{2,5})?(?:\/(?:[\0-\x08\x0E-\x1F!-\x9F\xA1-\u167F\u1681-\u1FFF\u200B-\u2027\u202A-\u202E\u2030-\u205E\u2060-\u2FFF\u3001-\uD7FF\uE000-\uFEFE\uFF00-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])*)?$/i; +var UUID = /^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i; +var JSON_POINTER = /^(?:\/(?:[^~/]|~0|~1)*)*$/; +var JSON_POINTER_URI_FRAGMENT = /^#(?:\/(?:[a-z0-9_\-.!$&'()*+,;:=@]|%[0-9a-f]{2}|~0|~1)*)*$/i; +var RELATIVE_JSON_POINTER = /^(?:0|[1-9][0-9]*)(?:#|(?:\/(?:[^~/]|~0|~1)*)*)$/; + + +module.exports = formats; + +function formats(mode) { + mode = mode == 'full' ? 'full' : 'fast'; + return util.copy(formats[mode]); +} + + +formats.fast = { + // date: http://tools.ietf.org/html/rfc3339#section-5.6 + date: /^\d\d\d\d-[0-1]\d-[0-3]\d$/, + // date-time: http://tools.ietf.org/html/rfc3339#section-5.6 + time: /^(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)?$/i, + 'date-time': /^\d\d\d\d-[0-1]\d-[0-3]\d[t\s](?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/i, + // uri: https://github.com/mafintosh/is-my-json-valid/blob/master/formats.js + uri: /^(?:[a-z][a-z0-9+-.]*:)(?:\/?\/)?[^\s]*$/i, + 'uri-reference': /^(?:(?:[a-z][a-z0-9+-.]*:)?\/?\/)?(?:[^\\\s#][^\s#]*)?(?:#[^\\\s]*)?$/i, + 'uri-template': URITEMPLATE, + url: URL, + // email (sources from jsen validator): + // http://stackoverflow.com/questions/201323/using-a-regular-expression-to-validate-an-email-address#answer-8829363 + // http://www.w3.org/TR/html5/forms.html#valid-e-mail-address (search for 'willful violation') + email: /^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/i, + hostname: HOSTNAME, + // optimized https://www.safaribooksonline.com/library/view/regular-expressions-cookbook/9780596802837/ch07s16.html + ipv4: /^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/, + // optimized http://stackoverflow.com/questions/53497/regular-expression-that-matches-valid-ipv6-addresses + ipv6: /^\s*(?:(?:(?:[0-9a-f]{1,4}:){7}(?:[0-9a-f]{1,4}|:))|(?:(?:[0-9a-f]{1,4}:){6}(?::[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(?:(?:[0-9a-f]{1,4}:){5}(?:(?:(?::[0-9a-f]{1,4}){1,2})|:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(?:(?:[0-9a-f]{1,4}:){4}(?:(?:(?::[0-9a-f]{1,4}){1,3})|(?:(?::[0-9a-f]{1,4})?:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){3}(?:(?:(?::[0-9a-f]{1,4}){1,4})|(?:(?::[0-9a-f]{1,4}){0,2}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){2}(?:(?:(?::[0-9a-f]{1,4}){1,5})|(?:(?::[0-9a-f]{1,4}){0,3}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){1}(?:(?:(?::[0-9a-f]{1,4}){1,6})|(?:(?::[0-9a-f]{1,4}){0,4}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?::(?:(?:(?::[0-9a-f]{1,4}){1,7})|(?:(?::[0-9a-f]{1,4}){0,5}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(?:%.+)?\s*$/i, + regex: regex, + // uuid: http://tools.ietf.org/html/rfc4122 + uuid: UUID, + // JSON-pointer: https://tools.ietf.org/html/rfc6901 + // uri fragment: https://tools.ietf.org/html/rfc3986#appendix-A + 'json-pointer': JSON_POINTER, + 'json-pointer-uri-fragment': JSON_POINTER_URI_FRAGMENT, + // relative JSON-pointer: http://tools.ietf.org/html/draft-luff-relative-json-pointer-00 + 'relative-json-pointer': RELATIVE_JSON_POINTER +}; + + +formats.full = { + date: date, + time: time, + 'date-time': date_time, + uri: uri, + 'uri-reference': URIREF, + 'uri-template': URITEMPLATE, + url: URL, + email: /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i, + hostname: hostname, + ipv4: /^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/, + ipv6: /^\s*(?:(?:(?:[0-9a-f]{1,4}:){7}(?:[0-9a-f]{1,4}|:))|(?:(?:[0-9a-f]{1,4}:){6}(?::[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(?:(?:[0-9a-f]{1,4}:){5}(?:(?:(?::[0-9a-f]{1,4}){1,2})|:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(?:(?:[0-9a-f]{1,4}:){4}(?:(?:(?::[0-9a-f]{1,4}){1,3})|(?:(?::[0-9a-f]{1,4})?:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){3}(?:(?:(?::[0-9a-f]{1,4}){1,4})|(?:(?::[0-9a-f]{1,4}){0,2}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){2}(?:(?:(?::[0-9a-f]{1,4}){1,5})|(?:(?::[0-9a-f]{1,4}){0,3}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?:(?:[0-9a-f]{1,4}:){1}(?:(?:(?::[0-9a-f]{1,4}){1,6})|(?:(?::[0-9a-f]{1,4}){0,4}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(?::(?:(?:(?::[0-9a-f]{1,4}){1,7})|(?:(?::[0-9a-f]{1,4}){0,5}:(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(?:%.+)?\s*$/i, + regex: regex, + uuid: UUID, + 'json-pointer': JSON_POINTER, + 'json-pointer-uri-fragment': JSON_POINTER_URI_FRAGMENT, + 'relative-json-pointer': RELATIVE_JSON_POINTER +}; + + +function isLeapYear(year) { + // https://tools.ietf.org/html/rfc3339#appendix-C + return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); +} + + +function date(str) { + // full-date from http://tools.ietf.org/html/rfc3339#section-5.6 + var matches = str.match(DATE); + if (!matches) return false; + + var year = +matches[1]; + var month = +matches[2]; + var day = +matches[3]; + + return month >= 1 && month <= 12 && day >= 1 && + day <= (month == 2 && isLeapYear(year) ? 29 : DAYS[month]); +} + + +function time(str, full) { + var matches = str.match(TIME); + if (!matches) return false; + + var hour = matches[1]; + var minute = matches[2]; + var second = matches[3]; + var timeZone = matches[5]; + return ((hour <= 23 && minute <= 59 && second <= 59) || + (hour == 23 && minute == 59 && second == 60)) && + (!full || timeZone); +} + + +var DATE_TIME_SEPARATOR = /t|\s/i; +function date_time(str) { + // http://tools.ietf.org/html/rfc3339#section-5.6 + var dateTime = str.split(DATE_TIME_SEPARATOR); + return dateTime.length == 2 && date(dateTime[0]) && time(dateTime[1], true); +} + + +function hostname(str) { + // https://tools.ietf.org/html/rfc1034#section-3.5 + // https://tools.ietf.org/html/rfc1123#section-2 + return str.length <= 255 && HOSTNAME.test(str); +} + + +var NOT_URI_FRAGMENT = /\/|:/; +function uri(str) { + // http://jmrware.com/articles/2009/uri_regexp/URI_regex.html + optional protocol + required "." + return NOT_URI_FRAGMENT.test(str) && URI.test(str); +} + + +var Z_ANCHOR = /[^\\]\\Z/; +function regex(str) { + if (Z_ANCHOR.test(str)) return false; + try { + new RegExp(str); + return true; + } catch(e) { + return false; + } +} + + +/***/ }), + +/***/ 883: +/***/ (function(module) { + +module.exports = {"$id":"header.json#","$schema":"http://json-schema.org/draft-06/schema#","type":"object","required":["name","value"],"properties":{"name":{"type":"string"},"value":{"type":"string"},"comment":{"type":"string"}}}; + +/***/ }), + +/***/ 886: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +var crypto = __webpack_require__(417); +var BigInteger = __webpack_require__(242).BigInteger; +var ECPointFp = __webpack_require__(729).ECPointFp; +var Buffer = __webpack_require__(215).Buffer; +exports.ECCurves = __webpack_require__(959); + +// zero prepad +function unstupid(hex,len) +{ + return (hex.length >= len) ? hex : unstupid("0"+hex,len); +} + +exports.ECKey = function(curve, key, isPublic) +{ + var priv; + var c = curve(); + var n = c.getN(); + var bytes = Math.floor(n.bitLength()/8); + + if(key) + { + if(isPublic) + { + var curve = c.getCurve(); +// var x = key.slice(1,bytes+1); // skip the 04 for uncompressed format +// var y = key.slice(bytes+1); +// this.P = new ECPointFp(curve, +// curve.fromBigInteger(new BigInteger(x.toString("hex"), 16)), +// curve.fromBigInteger(new BigInteger(y.toString("hex"), 16))); + this.P = curve.decodePointHex(key.toString("hex")); + }else{ + if(key.length != bytes) return false; + priv = new BigInteger(key.toString("hex"), 16); + } + }else{ + var n1 = n.subtract(BigInteger.ONE); + var r = new BigInteger(crypto.randomBytes(n.bitLength())); + priv = r.mod(n1).add(BigInteger.ONE); + this.P = c.getG().multiply(priv); + } + if(this.P) + { +// var pubhex = unstupid(this.P.getX().toBigInteger().toString(16),bytes*2)+unstupid(this.P.getY().toBigInteger().toString(16),bytes*2); +// this.PublicKey = Buffer.from("04"+pubhex,"hex"); + this.PublicKey = Buffer.from(c.getCurve().encodeCompressedPointHex(this.P),"hex"); + } + if(priv) + { + this.PrivateKey = Buffer.from(unstupid(priv.toString(16),bytes*2),"hex"); + this.deriveSharedSecret = function(key) + { + if(!key || !key.P) return false; + var S = key.P.multiply(priv); + return Buffer.from(unstupid(S.getX().toBigInteger().toString(16),bytes*2),"hex"); + } + } +} + + + +/***/ }), + +/***/ 890: +/***/ (function(module, __unusedexports, __webpack_require__) { + +"use strict"; + + +var MissingRefError = __webpack_require__(844).MissingRef; + +module.exports = compileAsync; + + +/** + * Creates validating function for passed schema with asynchronous loading of missing schemas. + * `loadSchema` option should be a function that accepts schema uri and returns promise that resolves with the schema. + * @this Ajv + * @param {Object} schema schema object + * @param {Boolean} meta optional true to compile meta-schema; this parameter can be skipped + * @param {Function} callback an optional node-style callback, it is called with 2 parameters: error (or null) and validating function. + * @return {Promise} promise that resolves with a validating function. + */ +function compileAsync(schema, meta, callback) { + /* eslint no-shadow: 0 */ + /* global Promise */ + /* jshint validthis: true */ + var self = this; + if (typeof this._opts.loadSchema != 'function') + throw new Error('options.loadSchema should be a function'); + + if (typeof meta == 'function') { + callback = meta; + meta = undefined; + } + + var p = loadMetaSchemaOf(schema).then(function () { + var schemaObj = self._addSchema(schema, undefined, meta); + return schemaObj.validate || _compileAsync(schemaObj); + }); + + if (callback) { + p.then( + function(v) { callback(null, v); }, + callback + ); + } + + return p; + + + function loadMetaSchemaOf(sch) { + var $schema = sch.$schema; + return $schema && !self.getSchema($schema) + ? compileAsync.call(self, { $ref: $schema }, true) + : Promise.resolve(); + } + + + function _compileAsync(schemaObj) { + try { return self._compile(schemaObj); } + catch(e) { + if (e instanceof MissingRefError) return loadMissingSchema(e); + throw e; + } + + + function loadMissingSchema(e) { + var ref = e.missingSchema; + if (added(ref)) throw new Error('Schema ' + ref + ' is loaded but ' + e.missingRef + ' cannot be resolved'); + + var schemaPromise = self._loadingSchemas[ref]; + if (!schemaPromise) { + schemaPromise = self._loadingSchemas[ref] = self._opts.loadSchema(ref); + schemaPromise.then(removePromise, removePromise); + } + + return schemaPromise.then(function (sch) { + if (!added(ref)) { + return loadMetaSchemaOf(sch).then(function () { + if (!added(ref)) self.addSchema(sch, ref, undefined, meta); + }); + } + }).then(function() { + return _compileAsync(schemaObj); + }); + + function removePromise() { + delete self._loadingSchemas[ref]; + } + + function added(ref) { + return self._refs[ref] || self._schemas[ref]; + } + } + } +} + + +/***/ }), + +/***/ 892: +/***/ (function(module, __unusedexports, __webpack_require__) { + +var iterate = __webpack_require__(157) + , initState = __webpack_require__(147) + , terminator = __webpack_require__(939) + ; + +// 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); +} + + +/***/ }), + +/***/ 893: +/***/ (function(module, __unusedexports, __webpack_require__) { + +// Copyright 2017 Joyent, Inc. + +module.exports = { + read: read, + verify: verify, + sign: sign, + signAsync: signAsync, + write: write, + + /* Internal private API */ + fromBuffer: fromBuffer, + toBuffer: toBuffer +}; + +var assert = __webpack_require__(477); +var SSHBuffer = __webpack_require__(940); +var crypto = __webpack_require__(417); +var Buffer = __webpack_require__(215).Buffer; +var algs = __webpack_require__(98); +var Key = __webpack_require__(852); +var PrivateKey = __webpack_require__(502); +var Identity = __webpack_require__(378); +var rfc4253 = __webpack_require__(538); +var Signature = __webpack_require__(575); +var utils = __webpack_require__(270); +var Certificate = __webpack_require__(752); + +function verify(cert, key) { + /* + * We always give an issuerKey, so if our verify() is being called then + * there was no signature. Return false. + */ + return (false); +} + +var TYPES = { + 'user': 1, + 'host': 2 +}; +Object.keys(TYPES).forEach(function (k) { TYPES[TYPES[k]] = k; }); + +var ECDSA_ALGO = /^ecdsa-sha2-([^@-]+)-cert-v01@openssh.com$/; + +function read(buf, options) { + if (Buffer.isBuffer(buf)) + buf = buf.toString('ascii'); + var parts = buf.trim().split(/[ \t\n]+/g); + if (parts.length < 2 || parts.length > 3) + throw (new Error('Not a valid SSH certificate line')); + + var algo = parts[0]; + var data = parts[1]; + + data = Buffer.from(data, 'base64'); + return (fromBuffer(data, algo)); +} + +function fromBuffer(data, algo, partial) { + var sshbuf = new SSHBuffer({ buffer: data }); + var innerAlgo = sshbuf.readString(); + if (algo !== undefined && innerAlgo !== algo) + throw (new Error('SSH certificate algorithm mismatch')); + if (algo === undefined) + algo = innerAlgo; + + var cert = {}; + cert.signatures = {}; + cert.signatures.openssh = {}; + + cert.signatures.openssh.nonce = sshbuf.readBuffer(); + + var key = {}; + var parts = (key.parts = []); + key.type = getAlg(algo); + + var partCount = algs.info[key.type].parts.length; + while (parts.length < partCount) + parts.push(sshbuf.readPart()); + assert.ok(parts.length >= 1, 'key must have at least one part'); + + var algInfo = algs.info[key.type]; + if (key.type === 'ecdsa') { + var res = ECDSA_ALGO.exec(algo); + assert.ok(res !== null); + assert.strictEqual(res[1], parts[0].data.toString()); + } + + for (var i = 0; i < algInfo.parts.length; ++i) { + parts[i].name = algInfo.parts[i]; + if (parts[i].name !== 'curve' && + algInfo.normalize !== false) { + var p = parts[i]; + p.data = utils.mpNormalize(p.data); + } + } + + cert.subjectKey = new Key(key); + + cert.serial = sshbuf.readInt64(); + + var type = TYPES[sshbuf.readInt()]; + assert.string(type, 'valid cert type'); + + cert.signatures.openssh.keyId = sshbuf.readString(); + + var principals = []; + var pbuf = sshbuf.readBuffer(); + var psshbuf = new SSHBuffer({ buffer: pbuf }); + while (!psshbuf.atEnd()) + principals.push(psshbuf.readString()); + if (principals.length === 0) + principals = ['*']; + + cert.subjects = principals.map(function (pr) { + if (type === 'user') + return (Identity.forUser(pr)); + else if (type === 'host') + return (Identity.forHost(pr)); + throw (new Error('Unknown identity type ' + type)); + }); + + cert.validFrom = int64ToDate(sshbuf.readInt64()); + cert.validUntil = int64ToDate(sshbuf.readInt64()); + + var exts = []; + var extbuf = new SSHBuffer({ buffer: sshbuf.readBuffer() }); + var ext; + while (!extbuf.atEnd()) { + ext = { critical: true }; + ext.name = extbuf.readString(); + ext.data = extbuf.readBuffer(); + exts.push(ext); + } + extbuf = new SSHBuffer({ buffer: sshbuf.readBuffer() }); + while (!extbuf.atEnd()) { + ext = { critical: false }; + ext.name = extbuf.readString(); + ext.data = extbuf.readBuffer(); + exts.push(ext); + } + cert.signatures.openssh.exts = exts; + + /* reserved */ + sshbuf.readBuffer(); + + var signingKeyBuf = sshbuf.readBuffer(); + cert.issuerKey = rfc4253.read(signingKeyBuf); + + /* + * OpenSSH certs don't give the identity of the issuer, just their + * public key. So, we use an Identity that matches anything. The + * isSignedBy() function will later tell you if the key matches. + */ + cert.issuer = Identity.forHost('**'); + + var sigBuf = sshbuf.readBuffer(); + cert.signatures.openssh.signature = + Signature.parse(sigBuf, cert.issuerKey.type, 'ssh'); + + if (partial !== undefined) { + partial.remainder = sshbuf.remainder(); + partial.consumed = sshbuf._offset; + } + + return (new Certificate(cert)); +} + +function int64ToDate(buf) { + var i = buf.readUInt32BE(0) * 4294967296; + i += buf.readUInt32BE(4); + var d = new Date(); + d.setTime(i * 1000); + d.sourceInt64 = buf; + return (d); +} + +function dateToInt64(date) { + if (date.sourceInt64 !== undefined) + return (date.sourceInt64); + var i = Math.round(date.getTime() / 1000); + var upper = Math.floor(i / 4294967296); + var lower = Math.floor(i % 4294967296); + var buf = Buffer.alloc(8); + buf.writeUInt32BE(upper, 0); + buf.writeUInt32BE(lower, 4); + return (buf); +} + +function sign(cert, key) { + if (cert.signatures.openssh === undefined) + cert.signatures.openssh = {}; + try { + var blob = toBuffer(cert, true); + } catch (e) { + delete (cert.signatures.openssh); + return (false); + } + var sig = cert.signatures.openssh; + var hashAlgo = undefined; + if (key.type === 'rsa' || key.type === 'dsa') + hashAlgo = 'sha1'; + var signer = key.createSign(hashAlgo); + signer.write(blob); + sig.signature = signer.sign(); + return (true); +} + +function signAsync(cert, signer, done) { + if (cert.signatures.openssh === undefined) + cert.signatures.openssh = {}; + try { + var blob = toBuffer(cert, true); + } catch (e) { + delete (cert.signatures.openssh); + done(e); + return; + } + var sig = cert.signatures.openssh; + + signer(blob, function (err, signature) { + if (err) { + done(err); + return; + } + try { + /* + * This will throw if the signature isn't of a + * type/algo that can be used for SSH. + */ + signature.toBuffer('ssh'); + } catch (e) { + done(e); + return; + } + sig.signature = signature; + done(); + }); +} + +function write(cert, options) { + if (options === undefined) + options = {}; + + var blob = toBuffer(cert); + var out = getCertType(cert.subjectKey) + ' ' + blob.toString('base64'); + if (options.comment) + out = out + ' ' + options.comment; + return (out); +} + + +function toBuffer(cert, noSig) { + assert.object(cert.signatures.openssh, 'signature for openssh format'); + var sig = cert.signatures.openssh; + + if (sig.nonce === undefined) + sig.nonce = crypto.randomBytes(16); + var buf = new SSHBuffer({}); + buf.writeString(getCertType(cert.subjectKey)); + buf.writeBuffer(sig.nonce); + + var key = cert.subjectKey; + var algInfo = algs.info[key.type]; + algInfo.parts.forEach(function (part) { + buf.writePart(key.part[part]); + }); + + buf.writeInt64(cert.serial); + + var type = cert.subjects[0].type; + assert.notStrictEqual(type, 'unknown'); + cert.subjects.forEach(function (id) { + assert.strictEqual(id.type, type); + }); + type = TYPES[type]; + buf.writeInt(type); + + if (sig.keyId === undefined) { + sig.keyId = cert.subjects[0].type + '_' + + (cert.subjects[0].uid || cert.subjects[0].hostname); + } + buf.writeString(sig.keyId); + + var sub = new SSHBuffer({}); + cert.subjects.forEach(function (id) { + if (type === TYPES.host) + sub.writeString(id.hostname); + else if (type === TYPES.user) + sub.writeString(id.uid); + }); + buf.writeBuffer(sub.toBuffer()); + + buf.writeInt64(dateToInt64(cert.validFrom)); + buf.writeInt64(dateToInt64(cert.validUntil)); + + var exts = sig.exts; + if (exts === undefined) + exts = []; + + var extbuf = new SSHBuffer({}); + exts.forEach(function (ext) { + if (ext.critical !== true) + return; + extbuf.writeString(ext.name); + extbuf.writeBuffer(ext.data); + }); + buf.writeBuffer(extbuf.toBuffer()); + + extbuf = new SSHBuffer({}); + exts.forEach(function (ext) { + if (ext.critical === true) + return; + extbuf.writeString(ext.name); + extbuf.writeBuffer(ext.data); + }); + buf.writeBuffer(extbuf.toBuffer()); + + /* reserved */ + buf.writeBuffer(Buffer.alloc(0)); + + sub = rfc4253.write(cert.issuerKey); + buf.writeBuffer(sub); + + if (!noSig) + buf.writeBuffer(sig.signature.toBuffer('ssh')); + + return (buf.toBuffer()); +} + +function getAlg(certType) { + if (certType === 'ssh-rsa-cert-v01@openssh.com') + return ('rsa'); + if (certType === 'ssh-dss-cert-v01@openssh.com') + return ('dsa'); + if (certType.match(ECDSA_ALGO)) + return ('ecdsa'); + if (certType === 'ssh-ed25519-cert-v01@openssh.com') + return ('ed25519'); + throw (new Error('Unsupported cert type ' + certType)); +} + +function getCertType(key) { + if (key.type === 'rsa') + return ('ssh-rsa-cert-v01@openssh.com'); + if (key.type === 'dsa') + return ('ssh-dss-cert-v01@openssh.com'); + if (key.type === 'ecdsa') + return ('ecdsa-sha2-' + key.curve + '-cert-v01@openssh.com'); + if (key.type === 'ed25519') + return ('ssh-ed25519-cert-v01@openssh.com'); + throw (new Error('Unsupported key type ' + key.type)); +} + + +/***/ }), + +/***/ 894: +/***/ (function(module, __unusedexports, __webpack_require__) { + +"use strict"; + + +//all requires must be explicit because browserify won't work with dynamic requires +module.exports = { + '$ref': __webpack_require__(266), + allOf: __webpack_require__(107), + anyOf: __webpack_require__(902), + '$comment': __webpack_require__(28), + const: __webpack_require__(662), + contains: __webpack_require__(154), + dependencies: __webpack_require__(233), + 'enum': __webpack_require__(281), + format: __webpack_require__(687), + 'if': __webpack_require__(479), + items: __webpack_require__(643), + maximum: __webpack_require__(341), + minimum: __webpack_require__(341), + maxItems: __webpack_require__(85), + minItems: __webpack_require__(85), + maxLength: __webpack_require__(772), + minLength: __webpack_require__(772), + maxProperties: __webpack_require__(560), + minProperties: __webpack_require__(560), + multipleOf: __webpack_require__(397), + not: __webpack_require__(673), + oneOf: __webpack_require__(653), + pattern: __webpack_require__(542), + properties: __webpack_require__(343), + propertyNames: __webpack_require__(35), + required: __webpack_require__(858), + uniqueItems: __webpack_require__(899), + validate: __webpack_require__(967) +}; + + +/***/ }), + +/***/ 897: +/***/ (function(module, __unusedexports, __webpack_require__) { + +"use strict"; + + +var utils = __webpack_require__(581); +var formats = __webpack_require__(13); + +var arrayPrefixGenerators = { + brackets: function brackets(prefix) { // eslint-disable-line func-name-matching + return prefix + '[]'; + }, + indices: function indices(prefix, key) { // eslint-disable-line func-name-matching + return prefix + '[' + key + ']'; + }, + repeat: function repeat(prefix) { // eslint-disable-line func-name-matching + return prefix; + } +}; + +var toISO = Date.prototype.toISOString; + +var defaults = { + delimiter: '&', + encode: true, + encoder: utils.encode, + encodeValuesOnly: false, + serializeDate: function serializeDate(date) { // eslint-disable-line func-name-matching + return toISO.call(date); + }, + skipNulls: false, + strictNullHandling: false +}; + +var stringify = function stringify( // eslint-disable-line func-name-matching + object, + prefix, + generateArrayPrefix, + strictNullHandling, + skipNulls, + encoder, + filter, + sort, + allowDots, + serializeDate, + formatter, + encodeValuesOnly +) { + var obj = object; + if (typeof filter === 'function') { + obj = filter(prefix, obj); + } else if (obj instanceof Date) { + obj = serializeDate(obj); + } else if (obj === null) { + if (strictNullHandling) { + return encoder && !encodeValuesOnly ? encoder(prefix, defaults.encoder) : prefix; + } + + obj = ''; + } + + if (typeof obj === 'string' || typeof obj === 'number' || typeof obj === 'boolean' || utils.isBuffer(obj)) { + if (encoder) { + var keyValue = encodeValuesOnly ? prefix : encoder(prefix, defaults.encoder); + return [formatter(keyValue) + '=' + formatter(encoder(obj, defaults.encoder))]; + } + return [formatter(prefix) + '=' + formatter(String(obj))]; + } + + var values = []; + + if (typeof obj === 'undefined') { + return values; + } + + var objKeys; + if (Array.isArray(filter)) { + objKeys = filter; + } else { + var keys = Object.keys(obj); + objKeys = sort ? keys.sort(sort) : keys; + } + + for (var i = 0; i < objKeys.length; ++i) { + var key = objKeys[i]; + + if (skipNulls && obj[key] === null) { + continue; + } + + if (Array.isArray(obj)) { + values = values.concat(stringify( + obj[key], + generateArrayPrefix(prefix, key), + generateArrayPrefix, + strictNullHandling, + skipNulls, + encoder, + filter, + sort, + allowDots, + serializeDate, + formatter, + encodeValuesOnly + )); + } else { + values = values.concat(stringify( + obj[key], + prefix + (allowDots ? '.' + key : '[' + key + ']'), + generateArrayPrefix, + strictNullHandling, + skipNulls, + encoder, + filter, + sort, + allowDots, + serializeDate, + formatter, + encodeValuesOnly + )); + } + } + + return values; +}; + +module.exports = function (object, opts) { + var obj = object; + var options = opts ? utils.assign({}, opts) : {}; + + if (options.encoder !== null && options.encoder !== undefined && typeof options.encoder !== 'function') { + throw new TypeError('Encoder has to be a function.'); + } + + var delimiter = typeof options.delimiter === 'undefined' ? defaults.delimiter : options.delimiter; + var strictNullHandling = typeof options.strictNullHandling === 'boolean' ? options.strictNullHandling : defaults.strictNullHandling; + var skipNulls = typeof options.skipNulls === 'boolean' ? options.skipNulls : defaults.skipNulls; + var encode = typeof options.encode === 'boolean' ? options.encode : defaults.encode; + var encoder = typeof options.encoder === 'function' ? options.encoder : defaults.encoder; + var sort = typeof options.sort === 'function' ? options.sort : null; + var allowDots = typeof options.allowDots === 'undefined' ? false : options.allowDots; + var serializeDate = typeof options.serializeDate === 'function' ? options.serializeDate : defaults.serializeDate; + var encodeValuesOnly = typeof options.encodeValuesOnly === 'boolean' ? options.encodeValuesOnly : defaults.encodeValuesOnly; + if (typeof options.format === 'undefined') { + options.format = formats['default']; + } else if (!Object.prototype.hasOwnProperty.call(formats.formatters, options.format)) { + throw new TypeError('Unknown format option provided.'); + } + var formatter = formats.formatters[options.format]; + var objKeys; + var filter; + + if (typeof options.filter === 'function') { + filter = options.filter; + obj = filter('', obj); + } else if (Array.isArray(options.filter)) { + filter = options.filter; + objKeys = filter; + } + + var keys = []; + + if (typeof obj !== 'object' || obj === null) { + return ''; + } + + var arrayFormat; + if (options.arrayFormat in arrayPrefixGenerators) { + arrayFormat = options.arrayFormat; + } else if ('indices' in options) { + arrayFormat = options.indices ? 'indices' : 'repeat'; + } else { + arrayFormat = 'indices'; + } + + var generateArrayPrefix = arrayPrefixGenerators[arrayFormat]; + + if (!objKeys) { + objKeys = Object.keys(obj); + } + + if (sort) { + objKeys.sort(sort); + } + + for (var i = 0; i < objKeys.length; ++i) { + var key = objKeys[i]; + + if (skipNulls && obj[key] === null) { + continue; + } + + keys = keys.concat(stringify( + obj[key], + key, + generateArrayPrefix, + strictNullHandling, + skipNulls, + encode ? encoder : null, + filter, + sort, + allowDots, + serializeDate, + formatter, + encodeValuesOnly + )); + } + + var joined = keys.join(delimiter); + var prefix = options.addQueryPrefix === true ? '?' : ''; + + return joined.length > 0 ? prefix + joined : ''; +}; + + +/***/ }), + +/***/ 899: +/***/ (function(module) { + +"use strict"; + +module.exports = function generate_uniqueItems(it, $keyword, $ruleType) { + var out = ' '; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $data = 'data' + ($dataLvl || ''); + var $valid = 'valid' + $lvl; + var $isData = it.opts.$data && $schema && $schema.$data, + $schemaValue; + if ($isData) { + out += ' var schema' + ($lvl) + ' = ' + (it.util.getData($schema.$data, $dataLvl, it.dataPathArr)) + '; '; + $schemaValue = 'schema' + $lvl; + } else { + $schemaValue = $schema; + } + if (($schema || $isData) && it.opts.uniqueItems !== false) { + if ($isData) { + out += ' var ' + ($valid) + '; if (' + ($schemaValue) + ' === false || ' + ($schemaValue) + ' === undefined) ' + ($valid) + ' = true; else if (typeof ' + ($schemaValue) + ' != \'boolean\') ' + ($valid) + ' = false; else { '; + } + out += ' var i = ' + ($data) + '.length , ' + ($valid) + ' = true , j; if (i > 1) { '; + var $itemType = it.schema.items && it.schema.items.type, + $typeIsArray = Array.isArray($itemType); + if (!$itemType || $itemType == 'object' || $itemType == 'array' || ($typeIsArray && ($itemType.indexOf('object') >= 0 || $itemType.indexOf('array') >= 0))) { + out += ' outer: for (;i--;) { for (j = i; j--;) { if (equal(' + ($data) + '[i], ' + ($data) + '[j])) { ' + ($valid) + ' = false; break outer; } } } '; + } else { + out += ' var itemIndices = {}, item; for (;i--;) { var item = ' + ($data) + '[i]; '; + var $method = 'checkDataType' + ($typeIsArray ? 's' : ''); + out += ' if (' + (it.util[$method]($itemType, 'item', true)) + ') continue; '; + if ($typeIsArray) { + out += ' if (typeof item == \'string\') item = \'"\' + item; '; + } + out += ' if (typeof itemIndices[item] == \'number\') { ' + ($valid) + ' = false; j = itemIndices[item]; break; } itemIndices[item] = i; } '; + } + out += ' } '; + if ($isData) { + out += ' } '; + } + out += ' if (!' + ($valid) + ') { '; + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('uniqueItems') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { i: i, j: j } '; + if (it.opts.messages !== false) { + out += ' , message: \'should NOT have duplicate items (items ## \' + j + \' and \' + i + \' are identical)\' '; + } + if (it.opts.verbose) { + out += ' , schema: '; + if ($isData) { + out += 'validate.schema' + ($schemaPath); + } else { + out += '' + ($schema); + } + out += ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + out += ' } '; + if ($breakOnError) { + out += ' else { '; + } + } else { + if ($breakOnError) { + out += ' if (true) { '; + } + } + return out; +} + + +/***/ }), + +/***/ 902: +/***/ (function(module) { + +"use strict"; + +module.exports = function generate_anyOf(it, $keyword, $ruleType) { + var out = ' '; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $data = 'data' + ($dataLvl || ''); + var $valid = 'valid' + $lvl; + var $errs = 'errs__' + $lvl; + var $it = it.util.copy(it); + var $closingBraces = ''; + $it.level++; + var $nextValid = 'valid' + $it.level; + var $noEmptySchema = $schema.every(function($sch) { + return (it.opts.strictKeywords ? typeof $sch == 'object' && Object.keys($sch).length > 0 : it.util.schemaHasRules($sch, it.RULES.all)); + }); + if ($noEmptySchema) { + var $currentBaseId = $it.baseId; + out += ' var ' + ($errs) + ' = errors; var ' + ($valid) + ' = false; '; + var $wasComposite = it.compositeRule; + it.compositeRule = $it.compositeRule = true; + var arr1 = $schema; + if (arr1) { + var $sch, $i = -1, + l1 = arr1.length - 1; + while ($i < l1) { + $sch = arr1[$i += 1]; + $it.schema = $sch; + $it.schemaPath = $schemaPath + '[' + $i + ']'; + $it.errSchemaPath = $errSchemaPath + '/' + $i; + out += ' ' + (it.validate($it)) + ' '; + $it.baseId = $currentBaseId; + out += ' ' + ($valid) + ' = ' + ($valid) + ' || ' + ($nextValid) + '; if (!' + ($valid) + ') { '; + $closingBraces += '}'; + } + } + it.compositeRule = $it.compositeRule = $wasComposite; + out += ' ' + ($closingBraces) + ' if (!' + ($valid) + ') { var err = '; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ('anyOf') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: {} '; + if (it.opts.messages !== false) { + out += ' , message: \'should match some schema in anyOf\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + out += '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError(vErrors); '; + } else { + out += ' validate.errors = vErrors; return false; '; + } + } + out += ' } else { errors = ' + ($errs) + '; if (vErrors !== null) { if (' + ($errs) + ') vErrors.length = ' + ($errs) + '; else vErrors = null; } '; + if (it.opts.allErrors) { + out += ' } '; + } + out = it.util.cleanUpCode(out); + } else { + if ($breakOnError) { + out += ' if (true) { '; + } + } + return out; +} + + +/***/ }), + +/***/ 909: +/***/ (function(module, __unusedexports, __webpack_require__) { + +// Copyright 2012 Joyent, Inc. All rights reserved. + +var assert = __webpack_require__(477); +var sshpk = __webpack_require__(650); +var util = __webpack_require__(669); + +var HASH_ALGOS = { + 'sha1': true, + 'sha256': true, + 'sha512': true +}; + +var PK_ALGOS = { + 'rsa': true, + 'dsa': true, + 'ecdsa': true +}; + +function HttpSignatureError(message, caller) { + if (Error.captureStackTrace) + Error.captureStackTrace(this, caller || HttpSignatureError); + + this.message = message; + this.name = caller.name; +} +util.inherits(HttpSignatureError, Error); + +function InvalidAlgorithmError(message) { + HttpSignatureError.call(this, message, InvalidAlgorithmError); +} +util.inherits(InvalidAlgorithmError, HttpSignatureError); + +function validateAlgorithm(algorithm) { + var alg = algorithm.toLowerCase().split('-'); + + if (alg.length !== 2) { + throw (new InvalidAlgorithmError(alg[0].toUpperCase() + ' is not a ' + + 'valid algorithm')); + } + + if (alg[0] !== 'hmac' && !PK_ALGOS[alg[0]]) { + throw (new InvalidAlgorithmError(alg[0].toUpperCase() + ' type keys ' + + 'are not supported')); + } + + if (!HASH_ALGOS[alg[1]]) { + throw (new InvalidAlgorithmError(alg[1].toUpperCase() + ' is not a ' + + 'supported hash algorithm')); + } + + return (alg); +} + +///--- API + +module.exports = { + + HASH_ALGOS: HASH_ALGOS, + PK_ALGOS: PK_ALGOS, + + HttpSignatureError: HttpSignatureError, + InvalidAlgorithmError: InvalidAlgorithmError, + + validateAlgorithm: validateAlgorithm, + + /** + * Converts an OpenSSH public key (rsa only) to a PKCS#8 PEM file. + * + * The intent of this module is to interoperate with OpenSSL only, + * specifically the node crypto module's `verify` method. + * + * @param {String} key an OpenSSH public key. + * @return {String} PEM encoded form of the RSA public key. + * @throws {TypeError} on bad input. + * @throws {Error} on invalid ssh key formatted data. + */ + sshKeyToPEM: function sshKeyToPEM(key) { + assert.string(key, 'ssh_key'); + + var k = sshpk.parseKey(key, 'ssh'); + return (k.toString('pem')); + }, + + + /** + * Generates an OpenSSH fingerprint from an ssh public key. + * + * @param {String} key an OpenSSH public key. + * @return {String} key fingerprint. + * @throws {TypeError} on bad input. + * @throws {Error} if what you passed doesn't look like an ssh public key. + */ + fingerprint: function fingerprint(key) { + assert.string(key, 'ssh_key'); + + var k = sshpk.parseKey(key, 'ssh'); + return (k.fingerprint('md5').toString('hex')); + }, + + /** + * Converts a PKGCS#8 PEM file to an OpenSSH public key (rsa) + * + * The reverse of the above function. + */ + pemToRsaSSHKey: function pemToRsaSSHKey(pem, comment) { + assert.equal('string', typeof (pem), 'typeof pem'); + + var k = sshpk.parseKey(pem, 'pem'); + k.comment = comment; + return (k.toString('ssh')); + } +}; + + +/***/ }), + +/***/ 919: +/***/ (function(module) { + +module.exports = {"$id":"entry.json#","$schema":"http://json-schema.org/draft-06/schema#","type":"object","optional":true,"required":["startedDateTime","time","request","response","cache","timings"],"properties":{"pageref":{"type":"string"},"startedDateTime":{"type":"string","format":"date-time","pattern":"^(\\d{4})(-)?(\\d\\d)(-)?(\\d\\d)(T)?(\\d\\d)(:)?(\\d\\d)(:)?(\\d\\d)(\\.\\d+)?(Z|([+-])(\\d\\d)(:)?(\\d\\d))"},"time":{"type":"number","min":0},"request":{"$ref":"request.json#"},"response":{"$ref":"response.json#"},"cache":{"$ref":"cache.json#"},"timings":{"$ref":"timings.json#"},"serverIPAddress":{"type":"string","oneOf":[{"format":"ipv4"},{"format":"ipv6"}]},"connection":{"type":"string"},"comment":{"type":"string"}}}; + +/***/ }), + +/***/ 921: +/***/ (function(module) { + +"use strict"; + + + +var Cache = module.exports = function Cache() { + this._cache = {}; +}; + + +Cache.prototype.put = function Cache_put(key, value) { + this._cache[key] = value; +}; + + +Cache.prototype.get = function Cache_get(key) { + return this._cache[key]; +}; + + +Cache.prototype.del = function Cache_del(key) { + delete this._cache[key]; +}; + + +Cache.prototype.clear = function Cache_clear() { + this._cache = {}; +}; + + +/***/ }), + +/***/ 928: +/***/ (function(module, __unusedexports, __webpack_require__) { + +var CombinedStream = __webpack_require__(547); +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__(779); +var asynckit = __webpack_require__(334); +var populate = __webpack_require__(69); + +// 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._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]'; +}; + + +/***/ }), + +/***/ 939: +/***/ (function(module, __unusedexports, __webpack_require__) { + +var abort = __webpack_require__(566) + , async = __webpack_require__(751) + ; + +// 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); +} + + +/***/ }), + +/***/ 940: +/***/ (function(module, __unusedexports, __webpack_require__) { + +// Copyright 2015 Joyent, Inc. + +module.exports = SSHBuffer; + +var assert = __webpack_require__(477); +var Buffer = __webpack_require__(215).Buffer; + +function SSHBuffer(opts) { + assert.object(opts, 'options'); + if (opts.buffer !== undefined) + assert.buffer(opts.buffer, 'options.buffer'); + + this._size = opts.buffer ? opts.buffer.length : 1024; + this._buffer = opts.buffer || Buffer.alloc(this._size); + this._offset = 0; +} + +SSHBuffer.prototype.toBuffer = function () { + return (this._buffer.slice(0, this._offset)); +}; + +SSHBuffer.prototype.atEnd = function () { + return (this._offset >= this._buffer.length); +}; + +SSHBuffer.prototype.remainder = function () { + return (this._buffer.slice(this._offset)); +}; + +SSHBuffer.prototype.skip = function (n) { + this._offset += n; +}; + +SSHBuffer.prototype.expand = function () { + this._size *= 2; + var buf = Buffer.alloc(this._size); + this._buffer.copy(buf, 0); + this._buffer = buf; +}; + +SSHBuffer.prototype.readPart = function () { + return ({data: this.readBuffer()}); +}; + +SSHBuffer.prototype.readBuffer = function () { + var len = this._buffer.readUInt32BE(this._offset); + this._offset += 4; + assert.ok(this._offset + len <= this._buffer.length, + 'length out of bounds at +0x' + this._offset.toString(16) + + ' (data truncated?)'); + var buf = this._buffer.slice(this._offset, this._offset + len); + this._offset += len; + return (buf); +}; + +SSHBuffer.prototype.readString = function () { + return (this.readBuffer().toString()); +}; + +SSHBuffer.prototype.readCString = function () { + var offset = this._offset; + while (offset < this._buffer.length && + this._buffer[offset] !== 0x00) + offset++; + assert.ok(offset < this._buffer.length, 'c string does not terminate'); + var str = this._buffer.slice(this._offset, offset).toString(); + this._offset = offset + 1; + return (str); +}; + +SSHBuffer.prototype.readInt = function () { + var v = this._buffer.readUInt32BE(this._offset); + this._offset += 4; + return (v); +}; + +SSHBuffer.prototype.readInt64 = function () { + assert.ok(this._offset + 8 < this._buffer.length, + 'buffer not long enough to read Int64'); + var v = this._buffer.slice(this._offset, this._offset + 8); + this._offset += 8; + return (v); +}; + +SSHBuffer.prototype.readChar = function () { + var v = this._buffer[this._offset++]; + return (v); +}; + +SSHBuffer.prototype.writeBuffer = function (buf) { + while (this._offset + 4 + buf.length > this._size) + this.expand(); + this._buffer.writeUInt32BE(buf.length, this._offset); + this._offset += 4; + buf.copy(this._buffer, this._offset); + this._offset += buf.length; +}; + +SSHBuffer.prototype.writeString = function (str) { + this.writeBuffer(Buffer.from(str, 'utf8')); +}; + +SSHBuffer.prototype.writeCString = function (str) { + while (this._offset + 1 + str.length > this._size) + this.expand(); + this._buffer.write(str, this._offset); + this._offset += str.length; + this._buffer[this._offset++] = 0; +}; + +SSHBuffer.prototype.writeInt = function (v) { + while (this._offset + 4 > this._size) + this.expand(); + this._buffer.writeUInt32BE(v, this._offset); + this._offset += 4; +}; + +SSHBuffer.prototype.writeInt64 = function (v) { + assert.buffer(v, 'value'); + if (v.length > 8) { + var lead = v.slice(0, v.length - 8); + for (var i = 0; i < lead.length; ++i) { + assert.strictEqual(lead[i], 0, + 'must fit in 64 bits of precision'); + } + v = v.slice(v.length - 8, v.length); + } + while (this._offset + 8 > this._size) + this.expand(); + v.copy(this._buffer, this._offset); + this._offset += 8; +}; + +SSHBuffer.prototype.writeChar = function (v) { + while (this._offset + 1 > this._size) + this.expand(); + this._buffer[this._offset++] = v; +}; + +SSHBuffer.prototype.writePart = function (p) { + this.writeBuffer(p.data); +}; + +SSHBuffer.prototype.write = function (buf) { + while (this._offset + buf.length > this._size) + this.expand(); + buf.copy(this._buffer, this._offset); + this._offset += buf.length; +}; + + +/***/ }), + +/***/ 942: +/***/ (function(module, __unusedexports, __webpack_require__) { + + +/*! + * Copyright 2010 LearnBoost + * + * 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. + */ + +/** + * Module dependencies. + */ + +var crypto = __webpack_require__(417) + , parse = __webpack_require__(835).parse + ; + +/** + * Valid keys. + */ + +var keys = + [ 'acl' + , 'location' + , 'logging' + , 'notification' + , 'partNumber' + , 'policy' + , 'requestPayment' + , 'torrent' + , 'uploadId' + , 'uploads' + , 'versionId' + , 'versioning' + , 'versions' + , 'website' + ] + +/** + * Return an "Authorization" header value with the given `options` + * in the form of "AWS :" + * + * @param {Object} options + * @return {String} + * @api private + */ + +function authorization (options) { + return 'AWS ' + options.key + ':' + sign(options) +} + +module.exports = authorization +module.exports.authorization = authorization + +/** + * Simple HMAC-SHA1 Wrapper + * + * @param {Object} options + * @return {String} + * @api private + */ + +function hmacSha1 (options) { + return crypto.createHmac('sha1', options.secret).update(options.message).digest('base64') +} + +module.exports.hmacSha1 = hmacSha1 + +/** + * Create a base64 sha1 HMAC for `options`. + * + * @param {Object} options + * @return {String} + * @api private + */ + +function sign (options) { + options.message = stringToSign(options) + return hmacSha1(options) +} +module.exports.sign = sign + +/** + * Create a base64 sha1 HMAC for `options`. + * + * Specifically to be used with S3 presigned URLs + * + * @param {Object} options + * @return {String} + * @api private + */ + +function signQuery (options) { + options.message = queryStringToSign(options) + return hmacSha1(options) +} +module.exports.signQuery= signQuery + +/** + * Return a string for sign() with the given `options`. + * + * Spec: + * + * \n + * \n + * \n + * \n + * [headers\n] + * + * + * @param {Object} options + * @return {String} + * @api private + */ + +function stringToSign (options) { + var headers = options.amazonHeaders || '' + if (headers) headers += '\n' + var r = + [ options.verb + , options.md5 + , options.contentType + , options.date ? options.date.toUTCString() : '' + , headers + options.resource + ] + return r.join('\n') +} +module.exports.stringToSign = stringToSign + +/** + * Return a string for sign() with the given `options`, but is meant exclusively + * for S3 presigned URLs + * + * Spec: + * + * \n + * + * + * @param {Object} options + * @return {String} + * @api private + */ + +function queryStringToSign (options){ + return 'GET\n\n\n' + options.date + '\n' + options.resource +} +module.exports.queryStringToSign = queryStringToSign + +/** + * Perform the following: + * + * - ignore non-amazon headers + * - lowercase fields + * - sort lexicographically + * - trim whitespace between ":" + * - join with newline + * + * @param {Object} headers + * @return {String} + * @api private + */ + +function canonicalizeHeaders (headers) { + var buf = [] + , fields = Object.keys(headers) + ; + for (var i = 0, len = fields.length; i < len; ++i) { + var field = fields[i] + , val = headers[field] + , field = field.toLowerCase() + ; + if (0 !== field.indexOf('x-amz')) continue + buf.push(field + ':' + val) + } + return buf.sort().join('\n') +} +module.exports.canonicalizeHeaders = canonicalizeHeaders + +/** + * Perform the following: + * + * - ignore non sub-resources + * - sort lexicographically + * + * @param {String} resource + * @return {String} + * @api private + */ + +function canonicalizeResource (resource) { + var url = parse(resource, true) + , path = url.pathname + , buf = [] + ; + + Object.keys(url.query).forEach(function(key){ + if (!~keys.indexOf(key)) return + var val = '' == url.query[key] ? '' : '=' + encodeURIComponent(url.query[key]) + buf.push(key + val) + }) + + return path + (buf.length ? '?' + buf.sort().join('&') : '') +} +module.exports.canonicalizeResource = canonicalizeResource + + +/***/ }), + +/***/ 944: +/***/ (function(module) { + +module.exports = isTypedArray +isTypedArray.strict = isStrictTypedArray +isTypedArray.loose = isLooseTypedArray + +var toString = Object.prototype.toString +var names = { + '[object Int8Array]': true + , '[object Int16Array]': true + , '[object Int32Array]': true + , '[object Uint8Array]': true + , '[object Uint8ClampedArray]': true + , '[object Uint16Array]': true + , '[object Uint32Array]': true + , '[object Float32Array]': true + , '[object Float64Array]': true +} + +function isTypedArray(arr) { + return ( + isStrictTypedArray(arr) + || isLooseTypedArray(arr) + ) +} + +function isStrictTypedArray(arr) { + return ( + arr instanceof Int8Array + || arr instanceof Int16Array + || arr instanceof Int32Array + || arr instanceof Uint8Array + || arr instanceof Uint8ClampedArray + || arr instanceof Uint16Array + || arr instanceof Uint32Array + || arr instanceof Float32Array + || arr instanceof Float64Array + ) +} + +function isLooseTypedArray(arr) { + return names[toString.call(arr)] +} + + +/***/ }), + +/***/ 952: +/***/ (function(module, __unusedexports, __webpack_require__) { + +"use strict"; + + +var metaSchema = __webpack_require__(522); + +module.exports = { + $id: 'https://github.com/epoberezkin/ajv/blob/master/lib/definition_schema.js', + definitions: { + simpleTypes: metaSchema.definitions.simpleTypes + }, + type: 'object', + dependencies: { + schema: ['validate'], + $data: ['validate'], + statements: ['inline'], + valid: {not: {required: ['macro']}} + }, + properties: { + type: metaSchema.properties.type, + schema: {type: 'boolean'}, + statements: {type: 'boolean'}, + dependencies: { + type: 'array', + items: {type: 'string'} + }, + metaSchema: {type: 'object'}, + modifying: {type: 'boolean'}, + valid: {type: 'boolean'}, + $data: {type: 'boolean'}, + async: {type: 'boolean'}, + errors: { + anyOf: [ + {type: 'boolean'}, + {const: 'full'} + ] + } + } +}; + + +/***/ }), + +/***/ 955: +/***/ (function(module, __unusedexports, __webpack_require__) { + +"use strict"; + + +var util = __webpack_require__(855); + +module.exports = SchemaObject; + +function SchemaObject(obj) { + util.copy(obj, this); +} + + +/***/ }), + +/***/ 956: +/***/ (function(module, __unusedexports, __webpack_require__) { + +/* + * verror.js: richer JavaScript errors + */ + +var mod_assertplus = __webpack_require__(477); +var mod_util = __webpack_require__(669); + +var mod_extsprintf = __webpack_require__(697); +var mod_isError = __webpack_require__(286).isError; +var sprintf = mod_extsprintf.sprintf; + +/* + * Public interface + */ + +/* So you can 'var VError = require('verror')' */ +module.exports = VError; +/* For compatibility */ +VError.VError = VError; +/* Other exported classes */ +VError.SError = SError; +VError.WError = WError; +VError.MultiError = MultiError; + +/* + * Common function used to parse constructor arguments for VError, WError, and + * SError. Named arguments to this function: + * + * strict force strict interpretation of sprintf arguments, even + * if the options in "argv" don't say so + * + * argv error's constructor arguments, which are to be + * interpreted as described in README.md. For quick + * reference, "argv" has one of the following forms: + * + * [ sprintf_args... ] (argv[0] is a string) + * [ cause, sprintf_args... ] (argv[0] is an Error) + * [ options, sprintf_args... ] (argv[0] is an object) + * + * This function normalizes these forms, producing an object with the following + * properties: + * + * options equivalent to "options" in third form. This will never + * be a direct reference to what the caller passed in + * (i.e., it may be a shallow copy), so it can be freely + * modified. + * + * shortmessage result of sprintf(sprintf_args), taking options.strict + * into account as described in README.md. + */ +function parseConstructorArguments(args) +{ + var argv, options, sprintf_args, shortmessage, k; + + mod_assertplus.object(args, 'args'); + mod_assertplus.bool(args.strict, 'args.strict'); + mod_assertplus.array(args.argv, 'args.argv'); + argv = args.argv; + + /* + * First, figure out which form of invocation we've been given. + */ + if (argv.length === 0) { + options = {}; + sprintf_args = []; + } else if (mod_isError(argv[0])) { + options = { 'cause': argv[0] }; + sprintf_args = argv.slice(1); + } else if (typeof (argv[0]) === 'object') { + options = {}; + for (k in argv[0]) { + options[k] = argv[0][k]; + } + sprintf_args = argv.slice(1); + } else { + mod_assertplus.string(argv[0], + 'first argument to VError, SError, or WError ' + + 'constructor must be a string, object, or Error'); + options = {}; + sprintf_args = argv; + } + + /* + * Now construct the error's message. + * + * extsprintf (which we invoke here with our caller's arguments in order + * to construct this Error's message) is strict in its interpretation of + * values to be processed by the "%s" specifier. The value passed to + * extsprintf must actually be a string or something convertible to a + * String using .toString(). Passing other values (notably "null" and + * "undefined") is considered a programmer error. The assumption is + * that if you actually want to print the string "null" or "undefined", + * then that's easy to do that when you're calling extsprintf; on the + * other hand, if you did NOT want that (i.e., there's actually a bug + * where the program assumes some variable is non-null and tries to + * print it, which might happen when constructing a packet or file in + * some specific format), then it's better to stop immediately than + * produce bogus output. + * + * However, sometimes the bug is only in the code calling VError, and a + * programmer might prefer to have the error message contain "null" or + * "undefined" rather than have the bug in the error path crash the + * program (making the first bug harder to identify). For that reason, + * by default VError converts "null" or "undefined" arguments to their + * string representations and passes those to extsprintf. Programmers + * desiring the strict behavior can use the SError class or pass the + * "strict" option to the VError constructor. + */ + mod_assertplus.object(options); + if (!options.strict && !args.strict) { + sprintf_args = sprintf_args.map(function (a) { + return (a === null ? 'null' : + a === undefined ? 'undefined' : a); + }); + } + + if (sprintf_args.length === 0) { + shortmessage = ''; + } else { + shortmessage = sprintf.apply(null, sprintf_args); + } + + return ({ + 'options': options, + 'shortmessage': shortmessage + }); +} + +/* + * See README.md for reference documentation. + */ +function VError() +{ + var args, obj, parsed, cause, ctor, message, k; + + args = Array.prototype.slice.call(arguments, 0); + + /* + * This is a regrettable pattern, but JavaScript's built-in Error class + * is defined to work this way, so we allow the constructor to be called + * without "new". + */ + if (!(this instanceof VError)) { + obj = Object.create(VError.prototype); + VError.apply(obj, arguments); + return (obj); + } + + /* + * For convenience and backwards compatibility, we support several + * different calling forms. Normalize them here. + */ + parsed = parseConstructorArguments({ + 'argv': args, + 'strict': false + }); + + /* + * If we've been given a name, apply it now. + */ + if (parsed.options.name) { + mod_assertplus.string(parsed.options.name, + 'error\'s "name" must be a string'); + this.name = parsed.options.name; + } + + /* + * For debugging, we keep track of the original short message (attached + * this Error particularly) separately from the complete message (which + * includes the messages of our cause chain). + */ + this.jse_shortmsg = parsed.shortmessage; + message = parsed.shortmessage; + + /* + * If we've been given a cause, record a reference to it and update our + * message appropriately. + */ + cause = parsed.options.cause; + if (cause) { + mod_assertplus.ok(mod_isError(cause), 'cause is not an Error'); + this.jse_cause = cause; + + if (!parsed.options.skipCauseMessage) { + message += ': ' + cause.message; + } + } + + /* + * If we've been given an object with properties, shallow-copy that + * here. We don't want to use a deep copy in case there are non-plain + * objects here, but we don't want to use the original object in case + * the caller modifies it later. + */ + this.jse_info = {}; + if (parsed.options.info) { + for (k in parsed.options.info) { + this.jse_info[k] = parsed.options.info[k]; + } + } + + this.message = message; + Error.call(this, message); + + if (Error.captureStackTrace) { + ctor = parsed.options.constructorOpt || this.constructor; + Error.captureStackTrace(this, ctor); + } + + return (this); +} + +mod_util.inherits(VError, Error); +VError.prototype.name = 'VError'; + +VError.prototype.toString = function ve_toString() +{ + var str = (this.hasOwnProperty('name') && this.name || + this.constructor.name || this.constructor.prototype.name); + if (this.message) + str += ': ' + this.message; + + return (str); +}; + +/* + * This method is provided for compatibility. New callers should use + * VError.cause() instead. That method also uses the saner `null` return value + * when there is no cause. + */ +VError.prototype.cause = function ve_cause() +{ + var cause = VError.cause(this); + return (cause === null ? undefined : cause); +}; + +/* + * Static methods + * + * These class-level methods are provided so that callers can use them on + * instances of Errors that are not VErrors. New interfaces should be provided + * only using static methods to eliminate the class of programming mistake where + * people fail to check whether the Error object has the corresponding methods. + */ + +VError.cause = function (err) +{ + mod_assertplus.ok(mod_isError(err), 'err must be an Error'); + return (mod_isError(err.jse_cause) ? err.jse_cause : null); +}; + +VError.info = function (err) +{ + var rv, cause, k; + + mod_assertplus.ok(mod_isError(err), 'err must be an Error'); + cause = VError.cause(err); + if (cause !== null) { + rv = VError.info(cause); + } else { + rv = {}; + } + + if (typeof (err.jse_info) == 'object' && err.jse_info !== null) { + for (k in err.jse_info) { + rv[k] = err.jse_info[k]; + } + } + + return (rv); +}; + +VError.findCauseByName = function (err, name) +{ + var cause; + + mod_assertplus.ok(mod_isError(err), 'err must be an Error'); + mod_assertplus.string(name, 'name'); + mod_assertplus.ok(name.length > 0, 'name cannot be empty'); + + for (cause = err; cause !== null; cause = VError.cause(cause)) { + mod_assertplus.ok(mod_isError(cause)); + if (cause.name == name) { + return (cause); + } + } + + return (null); +}; + +VError.hasCauseWithName = function (err, name) +{ + return (VError.findCauseByName(err, name) !== null); +}; + +VError.fullStack = function (err) +{ + mod_assertplus.ok(mod_isError(err), 'err must be an Error'); + + var cause = VError.cause(err); + + if (cause) { + return (err.stack + '\ncaused by: ' + VError.fullStack(cause)); + } + + return (err.stack); +}; + +VError.errorFromList = function (errors) +{ + mod_assertplus.arrayOfObject(errors, 'errors'); + + if (errors.length === 0) { + return (null); + } + + errors.forEach(function (e) { + mod_assertplus.ok(mod_isError(e)); + }); + + if (errors.length == 1) { + return (errors[0]); + } + + return (new MultiError(errors)); +}; + +VError.errorForEach = function (err, func) +{ + mod_assertplus.ok(mod_isError(err), 'err must be an Error'); + mod_assertplus.func(func, 'func'); + + if (err instanceof MultiError) { + err.errors().forEach(function iterError(e) { func(e); }); + } else { + func(err); + } +}; + + +/* + * SError is like VError, but stricter about types. You cannot pass "null" or + * "undefined" as string arguments to the formatter. + */ +function SError() +{ + var args, obj, parsed, options; + + args = Array.prototype.slice.call(arguments, 0); + if (!(this instanceof SError)) { + obj = Object.create(SError.prototype); + SError.apply(obj, arguments); + return (obj); + } + + parsed = parseConstructorArguments({ + 'argv': args, + 'strict': true + }); + + options = parsed.options; + VError.call(this, options, '%s', parsed.shortmessage); + + return (this); +} + +/* + * We don't bother setting SError.prototype.name because once constructed, + * SErrors are just like VErrors. + */ +mod_util.inherits(SError, VError); + + +/* + * Represents a collection of errors for the purpose of consumers that generally + * only deal with one error. Callers can extract the individual errors + * contained in this object, but may also just treat it as a normal single + * error, in which case a summary message will be printed. + */ +function MultiError(errors) +{ + mod_assertplus.array(errors, 'list of errors'); + mod_assertplus.ok(errors.length > 0, 'must be at least one error'); + this.ase_errors = errors; + + VError.call(this, { + 'cause': errors[0] + }, 'first of %d error%s', errors.length, errors.length == 1 ? '' : 's'); +} + +mod_util.inherits(MultiError, VError); +MultiError.prototype.name = 'MultiError'; + +MultiError.prototype.errors = function me_errors() +{ + return (this.ase_errors.slice(0)); +}; + + +/* + * See README.md for reference details. + */ +function WError() +{ + var args, obj, parsed, options; + + args = Array.prototype.slice.call(arguments, 0); + if (!(this instanceof WError)) { + obj = Object.create(WError.prototype); + WError.apply(obj, args); + return (obj); + } + + parsed = parseConstructorArguments({ + 'argv': args, + 'strict': false + }); + + options = parsed.options; + options['skipCauseMessage'] = true; + VError.call(this, options, '%s', parsed.shortmessage); + + return (this); +} + +mod_util.inherits(WError, VError); +WError.prototype.name = 'WError'; + +WError.prototype.toString = function we_toString() +{ + var str = (this.hasOwnProperty('name') && this.name || + this.constructor.name || this.constructor.prototype.name); + if (this.message) + str += ': ' + this.message; + if (this.jse_cause && this.jse_cause.message) + str += '; caused by ' + this.jse_cause.toString(); + + return (str); +}; + +/* + * For purely historical reasons, WError's cause() function allows you to set + * the cause. + */ +WError.prototype.cause = function we_cause(c) +{ + if (mod_isError(c)) + this.jse_cause = c; + + return (this.jse_cause); +}; + + +/***/ }), + +/***/ 959: +/***/ (function(module, __unusedexports, __webpack_require__) { + +// Named EC curves + +// Requires ec.js, jsbn.js, and jsbn2.js +var BigInteger = __webpack_require__(242).BigInteger +var ECCurveFp = __webpack_require__(729).ECCurveFp + + +// ---------------- +// X9ECParameters + +// constructor +function X9ECParameters(curve,g,n,h) { + this.curve = curve; + this.g = g; + this.n = n; + this.h = h; +} + +function x9getCurve() { + return this.curve; +} + +function x9getG() { + return this.g; +} + +function x9getN() { + return this.n; +} + +function x9getH() { + return this.h; +} + +X9ECParameters.prototype.getCurve = x9getCurve; +X9ECParameters.prototype.getG = x9getG; +X9ECParameters.prototype.getN = x9getN; +X9ECParameters.prototype.getH = x9getH; + +// ---------------- +// SECNamedCurves + +function fromHex(s) { return new BigInteger(s, 16); } + +function secp128r1() { + // p = 2^128 - 2^97 - 1 + var p = fromHex("FFFFFFFDFFFFFFFFFFFFFFFFFFFFFFFF"); + var a = fromHex("FFFFFFFDFFFFFFFFFFFFFFFFFFFFFFFC"); + var b = fromHex("E87579C11079F43DD824993C2CEE5ED3"); + //byte[] S = Hex.decode("000E0D4D696E6768756151750CC03A4473D03679"); + var n = fromHex("FFFFFFFE0000000075A30D1B9038A115"); + var h = BigInteger.ONE; + var curve = new ECCurveFp(p, a, b); + var G = curve.decodePointHex("04" + + "161FF7528B899B2D0C28607CA52C5B86" + + "CF5AC8395BAFEB13C02DA292DDED7A83"); + return new X9ECParameters(curve, G, n, h); +} + +function secp160k1() { + // p = 2^160 - 2^32 - 2^14 - 2^12 - 2^9 - 2^8 - 2^7 - 2^3 - 2^2 - 1 + var p = fromHex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFAC73"); + var a = BigInteger.ZERO; + var b = fromHex("7"); + //byte[] S = null; + var n = fromHex("0100000000000000000001B8FA16DFAB9ACA16B6B3"); + var h = BigInteger.ONE; + var curve = new ECCurveFp(p, a, b); + var G = curve.decodePointHex("04" + + "3B4C382CE37AA192A4019E763036F4F5DD4D7EBB" + + "938CF935318FDCED6BC28286531733C3F03C4FEE"); + return new X9ECParameters(curve, G, n, h); +} + +function secp160r1() { + // p = 2^160 - 2^31 - 1 + var p = fromHex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7FFFFFFF"); + var a = fromHex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7FFFFFFC"); + var b = fromHex("1C97BEFC54BD7A8B65ACF89F81D4D4ADC565FA45"); + //byte[] S = Hex.decode("1053CDE42C14D696E67687561517533BF3F83345"); + var n = fromHex("0100000000000000000001F4C8F927AED3CA752257"); + var h = BigInteger.ONE; + var curve = new ECCurveFp(p, a, b); + var G = curve.decodePointHex("04" + + "4A96B5688EF573284664698968C38BB913CBFC82" + + "23A628553168947D59DCC912042351377AC5FB32"); + return new X9ECParameters(curve, G, n, h); +} + +function secp192k1() { + // p = 2^192 - 2^32 - 2^12 - 2^8 - 2^7 - 2^6 - 2^3 - 1 + var p = fromHex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFEE37"); + var a = BigInteger.ZERO; + var b = fromHex("3"); + //byte[] S = null; + var n = fromHex("FFFFFFFFFFFFFFFFFFFFFFFE26F2FC170F69466A74DEFD8D"); + var h = BigInteger.ONE; + var curve = new ECCurveFp(p, a, b); + var G = curve.decodePointHex("04" + + "DB4FF10EC057E9AE26B07D0280B7F4341DA5D1B1EAE06C7D" + + "9B2F2F6D9C5628A7844163D015BE86344082AA88D95E2F9D"); + return new X9ECParameters(curve, G, n, h); +} + +function secp192r1() { + // p = 2^192 - 2^64 - 1 + var p = fromHex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFF"); + var a = fromHex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFC"); + var b = fromHex("64210519E59C80E70FA7E9AB72243049FEB8DEECC146B9B1"); + //byte[] S = Hex.decode("3045AE6FC8422F64ED579528D38120EAE12196D5"); + var n = fromHex("FFFFFFFFFFFFFFFFFFFFFFFF99DEF836146BC9B1B4D22831"); + var h = BigInteger.ONE; + var curve = new ECCurveFp(p, a, b); + var G = curve.decodePointHex("04" + + "188DA80EB03090F67CBF20EB43A18800F4FF0AFD82FF1012" + + "07192B95FFC8DA78631011ED6B24CDD573F977A11E794811"); + return new X9ECParameters(curve, G, n, h); +} + +function secp224r1() { + // p = 2^224 - 2^96 + 1 + var p = fromHex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000000000000000000000001"); + var a = fromHex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFE"); + var b = fromHex("B4050A850C04B3ABF54132565044B0B7D7BFD8BA270B39432355FFB4"); + //byte[] S = Hex.decode("BD71344799D5C7FCDC45B59FA3B9AB8F6A948BC5"); + var n = fromHex("FFFFFFFFFFFFFFFFFFFFFFFFFFFF16A2E0B8F03E13DD29455C5C2A3D"); + var h = BigInteger.ONE; + var curve = new ECCurveFp(p, a, b); + var G = curve.decodePointHex("04" + + "B70E0CBD6BB4BF7F321390B94A03C1D356C21122343280D6115C1D21" + + "BD376388B5F723FB4C22DFE6CD4375A05A07476444D5819985007E34"); + return new X9ECParameters(curve, G, n, h); +} + +function secp256r1() { + // p = 2^224 (2^32 - 1) + 2^192 + 2^96 - 1 + var p = fromHex("FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF"); + var a = fromHex("FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC"); + var b = fromHex("5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B"); + //byte[] S = Hex.decode("C49D360886E704936A6678E1139D26B7819F7E90"); + var n = fromHex("FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551"); + var h = BigInteger.ONE; + var curve = new ECCurveFp(p, a, b); + var G = curve.decodePointHex("04" + + "6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296" + + "4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5"); + return new X9ECParameters(curve, G, n, h); +} + +// TODO: make this into a proper hashtable +function getSECCurveByName(name) { + if(name == "secp128r1") return secp128r1(); + if(name == "secp160k1") return secp160k1(); + if(name == "secp160r1") return secp160r1(); + if(name == "secp192k1") return secp192k1(); + if(name == "secp192r1") return secp192r1(); + if(name == "secp224r1") return secp224r1(); + if(name == "secp256r1") return secp256r1(); + return null; +} + +module.exports = { + "secp128r1":secp128r1, + "secp160k1":secp160k1, + "secp160r1":secp160r1, + "secp192k1":secp192k1, + "secp192r1":secp192r1, + "secp224r1":secp224r1, + "secp256r1":secp256r1 +} + + +/***/ }), + +/***/ 964: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; + + +var crypto = __webpack_require__(417) + +function randomString (size) { + var bits = (size + 1) * 6 + var buffer = crypto.randomBytes(Math.ceil(bits / 8)) + var string = buffer.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '') + return string.slice(0, size) +} + +function calculatePayloadHash (payload, algorithm, contentType) { + var hash = crypto.createHash(algorithm) + hash.update('hawk.1.payload\n') + hash.update((contentType ? contentType.split(';')[0].trim().toLowerCase() : '') + '\n') + hash.update(payload || '') + hash.update('\n') + return hash.digest('base64') +} + +exports.calculateMac = function (credentials, opts) { + var normalized = 'hawk.1.header\n' + + opts.ts + '\n' + + opts.nonce + '\n' + + (opts.method || '').toUpperCase() + '\n' + + opts.resource + '\n' + + opts.host.toLowerCase() + '\n' + + opts.port + '\n' + + (opts.hash || '') + '\n' + + if (opts.ext) { + normalized = normalized + opts.ext.replace('\\', '\\\\').replace('\n', '\\n') + } + + normalized = normalized + '\n' + + if (opts.app) { + normalized = normalized + opts.app + '\n' + (opts.dlg || '') + '\n' + } + + var hmac = crypto.createHmac(credentials.algorithm, credentials.key).update(normalized) + var digest = hmac.digest('base64') + return digest +} + +exports.header = function (uri, method, opts) { + var timestamp = opts.timestamp || Math.floor((Date.now() + (opts.localtimeOffsetMsec || 0)) / 1000) + var credentials = opts.credentials + if (!credentials || !credentials.id || !credentials.key || !credentials.algorithm) { + return '' + } + + if (['sha1', 'sha256'].indexOf(credentials.algorithm) === -1) { + return '' + } + + var artifacts = { + ts: timestamp, + nonce: opts.nonce || randomString(6), + method: method, + resource: uri.pathname + (uri.search || ''), + host: uri.hostname, + port: uri.port || (uri.protocol === 'http:' ? 80 : 443), + hash: opts.hash, + ext: opts.ext, + app: opts.app, + dlg: opts.dlg + } + + if (!artifacts.hash && (opts.payload || opts.payload === '')) { + artifacts.hash = calculatePayloadHash(opts.payload, credentials.algorithm, opts.contentType) + } + + var mac = exports.calculateMac(credentials, artifacts) + + var hasExt = artifacts.ext !== null && artifacts.ext !== undefined && artifacts.ext !== '' + var header = 'Hawk id="' + credentials.id + + '", ts="' + artifacts.ts + + '", nonce="' + artifacts.nonce + + (artifacts.hash ? '", hash="' + artifacts.hash : '') + + (hasExt ? '", ext="' + artifacts.ext.replace(/\\/g, '\\\\').replace(/"/g, '\\"') : '') + + '", mac="' + mac + '"' + + if (artifacts.app) { + header = header + ', app="' + artifacts.app + (artifacts.dlg ? '", dlg="' + artifacts.dlg : '') + '"' + } + + return header +} + + +/***/ }), + +/***/ 967: +/***/ (function(module) { + +"use strict"; + +module.exports = function generate_validate(it, $keyword, $ruleType) { + var out = ''; + var $async = it.schema.$async === true, + $refKeywords = it.util.schemaHasRulesExcept(it.schema, it.RULES.all, '$ref'), + $id = it.self._getId(it.schema); + if (it.opts.strictKeywords) { + var $unknownKwd = it.util.schemaUnknownRules(it.schema, it.RULES.keywords); + if ($unknownKwd) { + var $keywordsMsg = 'unknown keyword: ' + $unknownKwd; + if (it.opts.strictKeywords === 'log') it.logger.warn($keywordsMsg); + else throw new Error($keywordsMsg); + } + } + if (it.isTop) { + out += ' var validate = '; + if ($async) { + it.async = true; + out += 'async '; + } + out += 'function(data, dataPath, parentData, parentDataProperty, rootData) { \'use strict\'; '; + if ($id && (it.opts.sourceCode || it.opts.processCode)) { + out += ' ' + ('/\*# sourceURL=' + $id + ' */') + ' '; + } + } + if (typeof it.schema == 'boolean' || !($refKeywords || it.schema.$ref)) { + var $keyword = 'false schema'; + var $lvl = it.level; + var $dataLvl = it.dataLevel; + var $schema = it.schema[$keyword]; + var $schemaPath = it.schemaPath + it.util.getProperty($keyword); + var $errSchemaPath = it.errSchemaPath + '/' + $keyword; + var $breakOnError = !it.opts.allErrors; + var $errorKeyword; + var $data = 'data' + ($dataLvl || ''); + var $valid = 'valid' + $lvl; + if (it.schema === false) { + if (it.isTop) { + $breakOnError = true; + } else { + out += ' var ' + ($valid) + ' = false; '; + } + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ($errorKeyword || 'false schema') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: {} '; + if (it.opts.messages !== false) { + out += ' , message: \'boolean schema is false\' '; + } + if (it.opts.verbose) { + out += ' , schema: false , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + } else { + if (it.isTop) { + if ($async) { + out += ' return data; '; + } else { + out += ' validate.errors = null; return true; '; + } + } else { + out += ' var ' + ($valid) + ' = true; '; + } + } + if (it.isTop) { + out += ' }; return validate; '; + } + return out; + } + if (it.isTop) { + var $top = it.isTop, + $lvl = it.level = 0, + $dataLvl = it.dataLevel = 0, + $data = 'data'; + it.rootId = it.resolve.fullPath(it.self._getId(it.root.schema)); + it.baseId = it.baseId || it.rootId; + delete it.isTop; + it.dataPathArr = [undefined]; + if (it.schema.default !== undefined && it.opts.useDefaults && it.opts.strictDefaults) { + var $defaultMsg = 'default is ignored in the schema root'; + if (it.opts.strictDefaults === 'log') it.logger.warn($defaultMsg); + else throw new Error($defaultMsg); + } + out += ' var vErrors = null; '; + out += ' var errors = 0; '; + out += ' if (rootData === undefined) rootData = data; '; + } else { + var $lvl = it.level, + $dataLvl = it.dataLevel, + $data = 'data' + ($dataLvl || ''); + if ($id) it.baseId = it.resolve.url(it.baseId, $id); + if ($async && !it.async) throw new Error('async schema in sync schema'); + out += ' var errs_' + ($lvl) + ' = errors;'; + } + var $valid = 'valid' + $lvl, + $breakOnError = !it.opts.allErrors, + $closingBraces1 = '', + $closingBraces2 = ''; + var $errorKeyword; + var $typeSchema = it.schema.type, + $typeIsArray = Array.isArray($typeSchema); + if ($typeSchema && it.opts.nullable && it.schema.nullable === true) { + if ($typeIsArray) { + if ($typeSchema.indexOf('null') == -1) $typeSchema = $typeSchema.concat('null'); + } else if ($typeSchema != 'null') { + $typeSchema = [$typeSchema, 'null']; + $typeIsArray = true; + } + } + if ($typeIsArray && $typeSchema.length == 1) { + $typeSchema = $typeSchema[0]; + $typeIsArray = false; + } + if (it.schema.$ref && $refKeywords) { + if (it.opts.extendRefs == 'fail') { + throw new Error('$ref: validation keywords used in schema at path "' + it.errSchemaPath + '" (see option extendRefs)'); + } else if (it.opts.extendRefs !== true) { + $refKeywords = false; + it.logger.warn('$ref: keywords ignored in schema at path "' + it.errSchemaPath + '"'); + } + } + if (it.schema.$comment && it.opts.$comment) { + out += ' ' + (it.RULES.all.$comment.code(it, '$comment')); + } + if ($typeSchema) { + if (it.opts.coerceTypes) { + var $coerceToTypes = it.util.coerceToTypes(it.opts.coerceTypes, $typeSchema); + } + var $rulesGroup = it.RULES.types[$typeSchema]; + if ($coerceToTypes || $typeIsArray || $rulesGroup === true || ($rulesGroup && !$shouldUseGroup($rulesGroup))) { + var $schemaPath = it.schemaPath + '.type', + $errSchemaPath = it.errSchemaPath + '/type'; + var $schemaPath = it.schemaPath + '.type', + $errSchemaPath = it.errSchemaPath + '/type', + $method = $typeIsArray ? 'checkDataTypes' : 'checkDataType'; + out += ' if (' + (it.util[$method]($typeSchema, $data, true)) + ') { '; + if ($coerceToTypes) { + var $dataType = 'dataType' + $lvl, + $coerced = 'coerced' + $lvl; + out += ' var ' + ($dataType) + ' = typeof ' + ($data) + '; '; + if (it.opts.coerceTypes == 'array') { + out += ' if (' + ($dataType) + ' == \'object\' && Array.isArray(' + ($data) + ')) ' + ($dataType) + ' = \'array\'; '; + } + out += ' var ' + ($coerced) + ' = undefined; '; + var $bracesCoercion = ''; + var arr1 = $coerceToTypes; + if (arr1) { + var $type, $i = -1, + l1 = arr1.length - 1; + while ($i < l1) { + $type = arr1[$i += 1]; + if ($i) { + out += ' if (' + ($coerced) + ' === undefined) { '; + $bracesCoercion += '}'; + } + if (it.opts.coerceTypes == 'array' && $type != 'array') { + out += ' if (' + ($dataType) + ' == \'array\' && ' + ($data) + '.length == 1) { ' + ($coerced) + ' = ' + ($data) + ' = ' + ($data) + '[0]; ' + ($dataType) + ' = typeof ' + ($data) + '; } '; + } + if ($type == 'string') { + out += ' if (' + ($dataType) + ' == \'number\' || ' + ($dataType) + ' == \'boolean\') ' + ($coerced) + ' = \'\' + ' + ($data) + '; else if (' + ($data) + ' === null) ' + ($coerced) + ' = \'\'; '; + } else if ($type == 'number' || $type == 'integer') { + out += ' if (' + ($dataType) + ' == \'boolean\' || ' + ($data) + ' === null || (' + ($dataType) + ' == \'string\' && ' + ($data) + ' && ' + ($data) + ' == +' + ($data) + ' '; + if ($type == 'integer') { + out += ' && !(' + ($data) + ' % 1)'; + } + out += ')) ' + ($coerced) + ' = +' + ($data) + '; '; + } else if ($type == 'boolean') { + out += ' if (' + ($data) + ' === \'false\' || ' + ($data) + ' === 0 || ' + ($data) + ' === null) ' + ($coerced) + ' = false; else if (' + ($data) + ' === \'true\' || ' + ($data) + ' === 1) ' + ($coerced) + ' = true; '; + } else if ($type == 'null') { + out += ' if (' + ($data) + ' === \'\' || ' + ($data) + ' === 0 || ' + ($data) + ' === false) ' + ($coerced) + ' = null; '; + } else if (it.opts.coerceTypes == 'array' && $type == 'array') { + out += ' if (' + ($dataType) + ' == \'string\' || ' + ($dataType) + ' == \'number\' || ' + ($dataType) + ' == \'boolean\' || ' + ($data) + ' == null) ' + ($coerced) + ' = [' + ($data) + ']; '; + } + } + } + out += ' ' + ($bracesCoercion) + ' if (' + ($coerced) + ' === undefined) { '; + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ($errorKeyword || 'type') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { type: \''; + if ($typeIsArray) { + out += '' + ($typeSchema.join(",")); + } else { + out += '' + ($typeSchema); + } + out += '\' } '; + if (it.opts.messages !== false) { + out += ' , message: \'should be '; + if ($typeIsArray) { + out += '' + ($typeSchema.join(",")); + } else { + out += '' + ($typeSchema); + } + out += '\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + out += ' } else { '; + var $parentData = $dataLvl ? 'data' + (($dataLvl - 1) || '') : 'parentData', + $parentDataProperty = $dataLvl ? it.dataPathArr[$dataLvl] : 'parentDataProperty'; + out += ' ' + ($data) + ' = ' + ($coerced) + '; '; + if (!$dataLvl) { + out += 'if (' + ($parentData) + ' !== undefined)'; + } + out += ' ' + ($parentData) + '[' + ($parentDataProperty) + '] = ' + ($coerced) + '; } '; + } else { + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ($errorKeyword || 'type') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { type: \''; + if ($typeIsArray) { + out += '' + ($typeSchema.join(",")); + } else { + out += '' + ($typeSchema); + } + out += '\' } '; + if (it.opts.messages !== false) { + out += ' , message: \'should be '; + if ($typeIsArray) { + out += '' + ($typeSchema.join(",")); + } else { + out += '' + ($typeSchema); + } + out += '\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + } + out += ' } '; + } + } + if (it.schema.$ref && !$refKeywords) { + out += ' ' + (it.RULES.all.$ref.code(it, '$ref')) + ' '; + if ($breakOnError) { + out += ' } if (errors === '; + if ($top) { + out += '0'; + } else { + out += 'errs_' + ($lvl); + } + out += ') { '; + $closingBraces2 += '}'; + } + } else { + var arr2 = it.RULES; + if (arr2) { + var $rulesGroup, i2 = -1, + l2 = arr2.length - 1; + while (i2 < l2) { + $rulesGroup = arr2[i2 += 1]; + if ($shouldUseGroup($rulesGroup)) { + if ($rulesGroup.type) { + out += ' if (' + (it.util.checkDataType($rulesGroup.type, $data)) + ') { '; + } + if (it.opts.useDefaults) { + if ($rulesGroup.type == 'object' && it.schema.properties) { + var $schema = it.schema.properties, + $schemaKeys = Object.keys($schema); + var arr3 = $schemaKeys; + if (arr3) { + var $propertyKey, i3 = -1, + l3 = arr3.length - 1; + while (i3 < l3) { + $propertyKey = arr3[i3 += 1]; + var $sch = $schema[$propertyKey]; + if ($sch.default !== undefined) { + var $passData = $data + it.util.getProperty($propertyKey); + if (it.compositeRule) { + if (it.opts.strictDefaults) { + var $defaultMsg = 'default is ignored for: ' + $passData; + if (it.opts.strictDefaults === 'log') it.logger.warn($defaultMsg); + else throw new Error($defaultMsg); + } + } else { + out += ' if (' + ($passData) + ' === undefined '; + if (it.opts.useDefaults == 'empty') { + out += ' || ' + ($passData) + ' === null || ' + ($passData) + ' === \'\' '; + } + out += ' ) ' + ($passData) + ' = '; + if (it.opts.useDefaults == 'shared') { + out += ' ' + (it.useDefault($sch.default)) + ' '; + } else { + out += ' ' + (JSON.stringify($sch.default)) + ' '; + } + out += '; '; + } + } + } + } + } else if ($rulesGroup.type == 'array' && Array.isArray(it.schema.items)) { + var arr4 = it.schema.items; + if (arr4) { + var $sch, $i = -1, + l4 = arr4.length - 1; + while ($i < l4) { + $sch = arr4[$i += 1]; + if ($sch.default !== undefined) { + var $passData = $data + '[' + $i + ']'; + if (it.compositeRule) { + if (it.opts.strictDefaults) { + var $defaultMsg = 'default is ignored for: ' + $passData; + if (it.opts.strictDefaults === 'log') it.logger.warn($defaultMsg); + else throw new Error($defaultMsg); + } + } else { + out += ' if (' + ($passData) + ' === undefined '; + if (it.opts.useDefaults == 'empty') { + out += ' || ' + ($passData) + ' === null || ' + ($passData) + ' === \'\' '; + } + out += ' ) ' + ($passData) + ' = '; + if (it.opts.useDefaults == 'shared') { + out += ' ' + (it.useDefault($sch.default)) + ' '; + } else { + out += ' ' + (JSON.stringify($sch.default)) + ' '; + } + out += '; '; + } + } + } + } + } + } + var arr5 = $rulesGroup.rules; + if (arr5) { + var $rule, i5 = -1, + l5 = arr5.length - 1; + while (i5 < l5) { + $rule = arr5[i5 += 1]; + if ($shouldUseRule($rule)) { + var $code = $rule.code(it, $rule.keyword, $rulesGroup.type); + if ($code) { + out += ' ' + ($code) + ' '; + if ($breakOnError) { + $closingBraces1 += '}'; + } + } + } + } + } + if ($breakOnError) { + out += ' ' + ($closingBraces1) + ' '; + $closingBraces1 = ''; + } + if ($rulesGroup.type) { + out += ' } '; + if ($typeSchema && $typeSchema === $rulesGroup.type && !$coerceToTypes) { + out += ' else { '; + var $schemaPath = it.schemaPath + '.type', + $errSchemaPath = it.errSchemaPath + '/type'; + var $$outStack = $$outStack || []; + $$outStack.push(out); + out = ''; /* istanbul ignore else */ + if (it.createErrors !== false) { + out += ' { keyword: \'' + ($errorKeyword || 'type') + '\' , dataPath: (dataPath || \'\') + ' + (it.errorPath) + ' , schemaPath: ' + (it.util.toQuotedString($errSchemaPath)) + ' , params: { type: \''; + if ($typeIsArray) { + out += '' + ($typeSchema.join(",")); + } else { + out += '' + ($typeSchema); + } + out += '\' } '; + if (it.opts.messages !== false) { + out += ' , message: \'should be '; + if ($typeIsArray) { + out += '' + ($typeSchema.join(",")); + } else { + out += '' + ($typeSchema); + } + out += '\' '; + } + if (it.opts.verbose) { + out += ' , schema: validate.schema' + ($schemaPath) + ' , parentSchema: validate.schema' + (it.schemaPath) + ' , data: ' + ($data) + ' '; + } + out += ' } '; + } else { + out += ' {} '; + } + var __err = out; + out = $$outStack.pop(); + if (!it.compositeRule && $breakOnError) { + /* istanbul ignore if */ + if (it.async) { + out += ' throw new ValidationError([' + (__err) + ']); '; + } else { + out += ' validate.errors = [' + (__err) + ']; return false; '; + } + } else { + out += ' var err = ' + (__err) + '; if (vErrors === null) vErrors = [err]; else vErrors.push(err); errors++; '; + } + out += ' } '; + } + } + if ($breakOnError) { + out += ' if (errors === '; + if ($top) { + out += '0'; + } else { + out += 'errs_' + ($lvl); + } + out += ') { '; + $closingBraces2 += '}'; + } + } + } + } + } + if ($breakOnError) { + out += ' ' + ($closingBraces2) + ' '; + } + if ($top) { + if ($async) { + out += ' if (errors === 0) return data; '; + out += ' else throw new ValidationError(vErrors); '; + } else { + out += ' validate.errors = vErrors; '; + out += ' return errors === 0; '; + } + out += ' }; return validate;'; + } else { + out += ' var ' + ($valid) + ' = errors === errs_' + ($lvl) + ';'; + } + out = it.util.cleanUpCode(out); + if ($top) { + out = it.util.finalCleanUpCode(out, $async); + } + + function $shouldUseGroup($rulesGroup) { + var rules = $rulesGroup.rules; + for (var i = 0; i < rules.length; i++) + if ($shouldUseRule(rules[i])) return true; + } + + function $shouldUseRule($rule) { + return it.schema[$rule.keyword] !== undefined || ($rule.implements && $ruleImplementsSomeKeyword($rule)); + } + + function $ruleImplementsSomeKeyword($rule) { + var impl = $rule.implements; + for (var i = 0; i < impl.length; i++) + if (it.schema[impl[i]] !== undefined) return true; + } + return out; +} + + +/***/ }), + +/***/ 972: +/***/ (function(module, __unusedexports, __webpack_require__) { + +/*! + * mime-db + * Copyright(c) 2014 Jonathan Ong + * MIT Licensed + */ + +/** + * Module exports. + */ + +module.exports = __webpack_require__(512) + + +/***/ }), + +/***/ 982: +/***/ (function(module, __unusedexports, __webpack_require__) { + +// Copyright 2017 Joyent, Inc. + +module.exports = { + read: read, + write: write +}; + +var assert = __webpack_require__(477); +var Buffer = __webpack_require__(215).Buffer; +var Key = __webpack_require__(852); +var PrivateKey = __webpack_require__(502); +var utils = __webpack_require__(270); +var SSHBuffer = __webpack_require__(940); +var Dhe = __webpack_require__(290); + +var supportedAlgos = { + 'rsa-sha1' : 5, + 'rsa-sha256' : 8, + 'rsa-sha512' : 10, + 'ecdsa-p256-sha256' : 13, + 'ecdsa-p384-sha384' : 14 + /* + * ed25519 is hypothetically supported with id 15 + * but the common tools available don't appear to be + * capable of generating/using ed25519 keys + */ +}; + +var supportedAlgosById = {}; +Object.keys(supportedAlgos).forEach(function (k) { + supportedAlgosById[supportedAlgos[k]] = k.toUpperCase(); +}); + +function read(buf, options) { + if (typeof (buf) !== 'string') { + assert.buffer(buf, 'buf'); + buf = buf.toString('ascii'); + } + var lines = buf.split('\n'); + if (lines[0].match(/^Private-key-format\: v1/)) { + var algElems = lines[1].split(' '); + var algoNum = parseInt(algElems[1], 10); + var algoName = algElems[2]; + if (!supportedAlgosById[algoNum]) + throw (new Error('Unsupported algorithm: ' + algoName)); + return (readDNSSECPrivateKey(algoNum, lines.slice(2))); + } + + // skip any comment-lines + var line = 0; + /* JSSTYLED */ + while (lines[line].match(/^\;/)) + line++; + // we should now have *one single* line left with our KEY on it. + if ((lines[line].match(/\. IN KEY /) || + lines[line].match(/\. IN DNSKEY /)) && lines[line+1].length === 0) { + return (readRFC3110(lines[line])); + } + throw (new Error('Cannot parse dnssec key')); +} + +function readRFC3110(keyString) { + var elems = keyString.split(' '); + //unused var flags = parseInt(elems[3], 10); + //unused var protocol = parseInt(elems[4], 10); + var algorithm = parseInt(elems[5], 10); + if (!supportedAlgosById[algorithm]) + throw (new Error('Unsupported algorithm: ' + algorithm)); + var base64key = elems.slice(6, elems.length).join(); + var keyBuffer = Buffer.from(base64key, 'base64'); + if (supportedAlgosById[algorithm].match(/^RSA-/)) { + // join the rest of the body into a single base64-blob + var publicExponentLen = keyBuffer.readUInt8(0); + if (publicExponentLen != 3 && publicExponentLen != 1) + throw (new Error('Cannot parse dnssec key: ' + + 'unsupported exponent length')); + + var publicExponent = keyBuffer.slice(1, publicExponentLen+1); + publicExponent = utils.mpNormalize(publicExponent); + var modulus = keyBuffer.slice(1+publicExponentLen); + modulus = utils.mpNormalize(modulus); + // now, make the key + var rsaKey = { + type: 'rsa', + parts: [] + }; + rsaKey.parts.push({ name: 'e', data: publicExponent}); + rsaKey.parts.push({ name: 'n', data: modulus}); + return (new Key(rsaKey)); + } + if (supportedAlgosById[algorithm] === 'ECDSA-P384-SHA384' || + supportedAlgosById[algorithm] === 'ECDSA-P256-SHA256') { + var curve = 'nistp384'; + var size = 384; + if (supportedAlgosById[algorithm].match(/^ECDSA-P256-SHA256/)) { + curve = 'nistp256'; + size = 256; + } + + var ecdsaKey = { + type: 'ecdsa', + curve: curve, + size: size, + parts: [ + {name: 'curve', data: Buffer.from(curve) }, + {name: 'Q', data: utils.ecNormalize(keyBuffer) } + ] + }; + return (new Key(ecdsaKey)); + } + throw (new Error('Unsupported algorithm: ' + + supportedAlgosById[algorithm])); +} + +function elementToBuf(e) { + return (Buffer.from(e.split(' ')[1], 'base64')); +} + +function readDNSSECRSAPrivateKey(elements) { + var rsaParams = {}; + elements.forEach(function (element) { + if (element.split(' ')[0] === 'Modulus:') + rsaParams['n'] = elementToBuf(element); + else if (element.split(' ')[0] === 'PublicExponent:') + rsaParams['e'] = elementToBuf(element); + else if (element.split(' ')[0] === 'PrivateExponent:') + rsaParams['d'] = elementToBuf(element); + else if (element.split(' ')[0] === 'Prime1:') + rsaParams['p'] = elementToBuf(element); + else if (element.split(' ')[0] === 'Prime2:') + rsaParams['q'] = elementToBuf(element); + else if (element.split(' ')[0] === 'Exponent1:') + rsaParams['dmodp'] = elementToBuf(element); + else if (element.split(' ')[0] === 'Exponent2:') + rsaParams['dmodq'] = elementToBuf(element); + else if (element.split(' ')[0] === 'Coefficient:') + rsaParams['iqmp'] = elementToBuf(element); + }); + // now, make the key + var key = { + type: 'rsa', + parts: [ + { name: 'e', data: utils.mpNormalize(rsaParams['e'])}, + { name: 'n', data: utils.mpNormalize(rsaParams['n'])}, + { name: 'd', data: utils.mpNormalize(rsaParams['d'])}, + { name: 'p', data: utils.mpNormalize(rsaParams['p'])}, + { name: 'q', data: utils.mpNormalize(rsaParams['q'])}, + { name: 'dmodp', + data: utils.mpNormalize(rsaParams['dmodp'])}, + { name: 'dmodq', + data: utils.mpNormalize(rsaParams['dmodq'])}, + { name: 'iqmp', + data: utils.mpNormalize(rsaParams['iqmp'])} + ] + }; + return (new PrivateKey(key)); +} + +function readDNSSECPrivateKey(alg, elements) { + if (supportedAlgosById[alg].match(/^RSA-/)) { + return (readDNSSECRSAPrivateKey(elements)); + } + if (supportedAlgosById[alg] === 'ECDSA-P384-SHA384' || + supportedAlgosById[alg] === 'ECDSA-P256-SHA256') { + var d = Buffer.from(elements[0].split(' ')[1], 'base64'); + var curve = 'nistp384'; + var size = 384; + if (supportedAlgosById[alg] === 'ECDSA-P256-SHA256') { + curve = 'nistp256'; + size = 256; + } + // DNSSEC generates the public-key on the fly (go calculate it) + var publicKey = utils.publicFromPrivateECDSA(curve, d); + var Q = publicKey.part['Q'].data; + var ecdsaKey = { + type: 'ecdsa', + curve: curve, + size: size, + parts: [ + {name: 'curve', data: Buffer.from(curve) }, + {name: 'd', data: d }, + {name: 'Q', data: Q } + ] + }; + return (new PrivateKey(ecdsaKey)); + } + throw (new Error('Unsupported algorithm: ' + supportedAlgosById[alg])); +} + +function dnssecTimestamp(date) { + var year = date.getFullYear() + ''; //stringify + var month = (date.getMonth() + 1); + var timestampStr = year + month + date.getUTCDate(); + timestampStr += '' + date.getUTCHours() + date.getUTCMinutes(); + timestampStr += date.getUTCSeconds(); + return (timestampStr); +} + +function rsaAlgFromOptions(opts) { + if (!opts || !opts.hashAlgo || opts.hashAlgo === 'sha1') + return ('5 (RSASHA1)'); + else if (opts.hashAlgo === 'sha256') + return ('8 (RSASHA256)'); + else if (opts.hashAlgo === 'sha512') + return ('10 (RSASHA512)'); + else + throw (new Error('Unknown or unsupported hash: ' + + opts.hashAlgo)); +} + +function writeRSA(key, options) { + // if we're missing parts, add them. + if (!key.part.dmodp || !key.part.dmodq) { + utils.addRSAMissing(key); + } + + var out = ''; + out += 'Private-key-format: v1.3\n'; + out += 'Algorithm: ' + rsaAlgFromOptions(options) + '\n'; + var n = utils.mpDenormalize(key.part['n'].data); + out += 'Modulus: ' + n.toString('base64') + '\n'; + var e = utils.mpDenormalize(key.part['e'].data); + out += 'PublicExponent: ' + e.toString('base64') + '\n'; + var d = utils.mpDenormalize(key.part['d'].data); + out += 'PrivateExponent: ' + d.toString('base64') + '\n'; + var p = utils.mpDenormalize(key.part['p'].data); + out += 'Prime1: ' + p.toString('base64') + '\n'; + var q = utils.mpDenormalize(key.part['q'].data); + out += 'Prime2: ' + q.toString('base64') + '\n'; + var dmodp = utils.mpDenormalize(key.part['dmodp'].data); + out += 'Exponent1: ' + dmodp.toString('base64') + '\n'; + var dmodq = utils.mpDenormalize(key.part['dmodq'].data); + out += 'Exponent2: ' + dmodq.toString('base64') + '\n'; + var iqmp = utils.mpDenormalize(key.part['iqmp'].data); + out += 'Coefficient: ' + iqmp.toString('base64') + '\n'; + // Assume that we're valid as-of now + var timestamp = new Date(); + out += 'Created: ' + dnssecTimestamp(timestamp) + '\n'; + out += 'Publish: ' + dnssecTimestamp(timestamp) + '\n'; + out += 'Activate: ' + dnssecTimestamp(timestamp) + '\n'; + return (Buffer.from(out, 'ascii')); +} + +function writeECDSA(key, options) { + var out = ''; + out += 'Private-key-format: v1.3\n'; + + if (key.curve === 'nistp256') { + out += 'Algorithm: 13 (ECDSAP256SHA256)\n'; + } else if (key.curve === 'nistp384') { + out += 'Algorithm: 14 (ECDSAP384SHA384)\n'; + } else { + throw (new Error('Unsupported curve')); + } + var base64Key = key.part['d'].data.toString('base64'); + out += 'PrivateKey: ' + base64Key + '\n'; + + // Assume that we're valid as-of now + var timestamp = new Date(); + out += 'Created: ' + dnssecTimestamp(timestamp) + '\n'; + out += 'Publish: ' + dnssecTimestamp(timestamp) + '\n'; + out += 'Activate: ' + dnssecTimestamp(timestamp) + '\n'; + + return (Buffer.from(out, 'ascii')); +} + +function write(key, options) { + if (PrivateKey.isPrivateKey(key)) { + if (key.type === 'rsa') { + return (writeRSA(key, options)); + } else if (key.type === 'ecdsa') { + return (writeECDSA(key, options)); + } else { + throw (new Error('Unsupported algorithm: ' + key.type)); + } + } else if (Key.isKey(key)) { + /* + * RFC3110 requires a keyname, and a keytype, which we + * don't really have a mechanism for specifying such + * additional metadata. + */ + throw (new Error('Format "dnssec" only supports ' + + 'writing private keys')); + } else { + throw (new Error('key is not a Key or PrivateKey')); + } +} + + +/***/ }), + +/***/ 985: +/***/ (function(module) { + +module.exports = function(size) { + return new LruCache(size) +} + +function LruCache(size) { + this.capacity = size | 0 + this.map = Object.create(null) + this.list = new DoublyLinkedList() +} + +LruCache.prototype.get = function(key) { + var node = this.map[key] + if (node == null) return undefined + this.used(node) + return node.val +} + +LruCache.prototype.set = function(key, val) { + var node = this.map[key] + if (node != null) { + node.val = val + } else { + if (!this.capacity) this.prune() + if (!this.capacity) return false + node = new DoublyLinkedNode(key, val) + this.map[key] = node + this.capacity-- + } + this.used(node) + return true +} + +LruCache.prototype.used = function(node) { + this.list.moveToFront(node) +} + +LruCache.prototype.prune = function() { + var node = this.list.pop() + if (node != null) { + delete this.map[node.key] + this.capacity++ + } +} + + +function DoublyLinkedList() { + this.firstNode = null + this.lastNode = null +} + +DoublyLinkedList.prototype.moveToFront = function(node) { + if (this.firstNode == node) return + + this.remove(node) + + if (this.firstNode == null) { + this.firstNode = node + this.lastNode = node + node.prev = null + node.next = null + } else { + node.prev = null + node.next = this.firstNode + node.next.prev = node + this.firstNode = node + } +} + +DoublyLinkedList.prototype.pop = function() { + var lastNode = this.lastNode + if (lastNode != null) { + this.remove(lastNode) + } + return lastNode +} + +DoublyLinkedList.prototype.remove = function(node) { + if (this.firstNode == node) { + this.firstNode = node.next + } else if (node.prev != null) { + node.prev.next = node.next + } + if (this.lastNode == node) { + this.lastNode = node.prev + } else if (node.next != null) { + node.next.prev = node.prev + } +} + + +function DoublyLinkedNode(key, val) { + this.key = key + this.val = val + this.prev = null + this.next = null +} + + +/***/ }), + +/***/ 993: +/***/ (function(module) { + +module.exports = {"$id":"cache.json#","$schema":"http://json-schema.org/draft-06/schema#","properties":{"beforeRequest":{"oneOf":[{"type":"null"},{"$ref":"beforeRequest.json#"}]},"afterRequest":{"oneOf":[{"type":"null"},{"$ref":"afterRequest.json#"}]},"comment":{"type":"string"}}}; + +/***/ }), + +/***/ 998: +/***/ (function(module, __unusedexports, __webpack_require__) { + +// Copyright 2011 Mark Cavage All rights reserved. + +var assert = __webpack_require__(357); +var Buffer = __webpack_require__(215).Buffer; +var ASN1 = __webpack_require__(362); +var errors = __webpack_require__(584); + + +// --- Globals + +var newInvalidAsn1Error = errors.newInvalidAsn1Error; + +var DEFAULT_OPTS = { + size: 1024, + growthFactor: 8 +}; + + +// --- Helpers + +function merge(from, to) { + assert.ok(from); + assert.equal(typeof (from), 'object'); + assert.ok(to); + assert.equal(typeof (to), 'object'); + + var keys = Object.getOwnPropertyNames(from); + keys.forEach(function (key) { + if (to[key]) + return; + + var value = Object.getOwnPropertyDescriptor(from, key); + Object.defineProperty(to, key, value); + }); + + return to; +} + + + +// --- API + +function Writer(options) { + options = merge(DEFAULT_OPTS, options || {}); + + this._buf = Buffer.alloc(options.size || 1024); + this._size = this._buf.length; + this._offset = 0; + this._options = options; + + // A list of offsets in the buffer where we need to insert + // sequence tag/len pairs. + this._seq = []; +} + +Object.defineProperty(Writer.prototype, 'buffer', { + get: function () { + if (this._seq.length) + throw newInvalidAsn1Error(this._seq.length + ' unended sequence(s)'); + + return (this._buf.slice(0, this._offset)); + } +}); + +Writer.prototype.writeByte = function (b) { + if (typeof (b) !== 'number') + throw new TypeError('argument must be a Number'); + + this._ensure(1); + this._buf[this._offset++] = b; +}; + + +Writer.prototype.writeInt = function (i, tag) { + if (typeof (i) !== 'number') + throw new TypeError('argument must be a Number'); + if (typeof (tag) !== 'number') + tag = ASN1.Integer; + + var sz = 4; + + while ((((i & 0xff800000) === 0) || ((i & 0xff800000) === 0xff800000 >> 0)) && + (sz > 1)) { + sz--; + i <<= 8; + } + + if (sz > 4) + throw newInvalidAsn1Error('BER ints cannot be > 0xffffffff'); + + this._ensure(2 + sz); + this._buf[this._offset++] = tag; + this._buf[this._offset++] = sz; + + while (sz-- > 0) { + this._buf[this._offset++] = ((i & 0xff000000) >>> 24); + i <<= 8; + } + +}; + + +Writer.prototype.writeNull = function () { + this.writeByte(ASN1.Null); + this.writeByte(0x00); +}; + + +Writer.prototype.writeEnumeration = function (i, tag) { + if (typeof (i) !== 'number') + throw new TypeError('argument must be a Number'); + if (typeof (tag) !== 'number') + tag = ASN1.Enumeration; + + return this.writeInt(i, tag); +}; + + +Writer.prototype.writeBoolean = function (b, tag) { + if (typeof (b) !== 'boolean') + throw new TypeError('argument must be a Boolean'); + if (typeof (tag) !== 'number') + tag = ASN1.Boolean; + + this._ensure(3); + this._buf[this._offset++] = tag; + this._buf[this._offset++] = 0x01; + this._buf[this._offset++] = b ? 0xff : 0x00; +}; + + +Writer.prototype.writeString = function (s, tag) { + if (typeof (s) !== 'string') + throw new TypeError('argument must be a string (was: ' + typeof (s) + ')'); + if (typeof (tag) !== 'number') + tag = ASN1.OctetString; + + var len = Buffer.byteLength(s); + this.writeByte(tag); + this.writeLength(len); + if (len) { + this._ensure(len); + this._buf.write(s, this._offset); + this._offset += len; + } +}; + + +Writer.prototype.writeBuffer = function (buf, tag) { + if (typeof (tag) !== 'number') + throw new TypeError('tag must be a number'); + if (!Buffer.isBuffer(buf)) + throw new TypeError('argument must be a buffer'); + + this.writeByte(tag); + this.writeLength(buf.length); + this._ensure(buf.length); + buf.copy(this._buf, this._offset, 0, buf.length); + this._offset += buf.length; +}; + + +Writer.prototype.writeStringArray = function (strings) { + if ((!strings instanceof Array)) + throw new TypeError('argument must be an Array[String]'); + + var self = this; + strings.forEach(function (s) { + self.writeString(s); + }); +}; + +// This is really to solve DER cases, but whatever for now +Writer.prototype.writeOID = function (s, tag) { + if (typeof (s) !== 'string') + throw new TypeError('argument must be a string'); + if (typeof (tag) !== 'number') + tag = ASN1.OID; + + if (!/^([0-9]+\.){3,}[0-9]+$/.test(s)) + throw new Error('argument is not a valid OID string'); + + function encodeOctet(bytes, octet) { + if (octet < 128) { + bytes.push(octet); + } else if (octet < 16384) { + bytes.push((octet >>> 7) | 0x80); + bytes.push(octet & 0x7F); + } else if (octet < 2097152) { + bytes.push((octet >>> 14) | 0x80); + bytes.push(((octet >>> 7) | 0x80) & 0xFF); + bytes.push(octet & 0x7F); + } else if (octet < 268435456) { + bytes.push((octet >>> 21) | 0x80); + bytes.push(((octet >>> 14) | 0x80) & 0xFF); + bytes.push(((octet >>> 7) | 0x80) & 0xFF); + bytes.push(octet & 0x7F); + } else { + bytes.push(((octet >>> 28) | 0x80) & 0xFF); + bytes.push(((octet >>> 21) | 0x80) & 0xFF); + bytes.push(((octet >>> 14) | 0x80) & 0xFF); + bytes.push(((octet >>> 7) | 0x80) & 0xFF); + bytes.push(octet & 0x7F); + } + } + + var tmp = s.split('.'); + var bytes = []; + bytes.push(parseInt(tmp[0], 10) * 40 + parseInt(tmp[1], 10)); + tmp.slice(2).forEach(function (b) { + encodeOctet(bytes, parseInt(b, 10)); + }); + + var self = this; + this._ensure(2 + bytes.length); + this.writeByte(tag); + this.writeLength(bytes.length); + bytes.forEach(function (b) { + self.writeByte(b); + }); +}; + + +Writer.prototype.writeLength = function (len) { + if (typeof (len) !== 'number') + throw new TypeError('argument must be a Number'); + + this._ensure(4); + + if (len <= 0x7f) { + this._buf[this._offset++] = len; + } else if (len <= 0xff) { + this._buf[this._offset++] = 0x81; + this._buf[this._offset++] = len; + } else if (len <= 0xffff) { + this._buf[this._offset++] = 0x82; + this._buf[this._offset++] = len >> 8; + this._buf[this._offset++] = len; + } else if (len <= 0xffffff) { + this._buf[this._offset++] = 0x83; + this._buf[this._offset++] = len >> 16; + this._buf[this._offset++] = len >> 8; + this._buf[this._offset++] = len; + } else { + throw newInvalidAsn1Error('Length too long (> 4 bytes)'); + } +}; + +Writer.prototype.startSequence = function (tag) { + if (typeof (tag) !== 'number') + tag = ASN1.Sequence | ASN1.Constructor; + + this.writeByte(tag); + this._seq.push(this._offset); + this._ensure(3); + this._offset += 3; +}; + + +Writer.prototype.endSequence = function () { + var seq = this._seq.pop(); + var start = seq + 3; + var len = this._offset - start; + + if (len <= 0x7f) { + this._shift(start, len, -2); + this._buf[seq] = len; + } else if (len <= 0xff) { + this._shift(start, len, -1); + this._buf[seq] = 0x81; + this._buf[seq + 1] = len; + } else if (len <= 0xffff) { + this._buf[seq] = 0x82; + this._buf[seq + 1] = len >> 8; + this._buf[seq + 2] = len; + } else if (len <= 0xffffff) { + this._shift(start, len, 1); + this._buf[seq] = 0x83; + this._buf[seq + 1] = len >> 16; + this._buf[seq + 2] = len >> 8; + this._buf[seq + 3] = len; + } else { + throw newInvalidAsn1Error('Sequence too long'); + } +}; + + +Writer.prototype._shift = function (start, len, shift) { + assert.ok(start !== undefined); + assert.ok(len !== undefined); + assert.ok(shift); + + this._buf.copy(this._buf, start + shift, start, start + len); + this._offset += shift; +}; + +Writer.prototype._ensure = function (len) { + assert.ok(len); + + if (this._size - this._offset < len) { + var sz = this._size * this._options.growthFactor; + if (sz - this._offset < len) + sz += len; + + var buf = Buffer.alloc(sz); + + this._buf.copy(buf, 0, 0, this._offset); + this._buf = buf; + this._size = sz; + } +}; + + + +// --- Exported API + +module.exports = Writer; + + +/***/ }) + +/******/ }); \ No newline at end of file diff --git a/.github/actions/send-tweet/index.js b/.github/actions/send-tweet/index.js new file mode 100644 index 0000000000..aa6140ba8b --- /dev/null +++ b/.github/actions/send-tweet/index.js @@ -0,0 +1,37 @@ +/*! + * 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. + */ + +const core = require('@actions/core'); +const Twitter = require('twitter'); + +function sendTweet() { + const twitter = new Twitter({ + consumer_key: core.getInput('consumer-key'), + consumer_secret: core.getInput('consumer-secret'), + access_token_key: core.getInput('access-token'), + access_token_secret: core.getInput('access-token-secret') + }); + + return twitter.post('/statuses/update', {status: core.getInput('status')}) + .then(() => { + return; + }) + .catch((err) => { + core.setFailed(err.message); + }); +} + +sendTweet(); diff --git a/.github/actions/send-tweet/package-lock.json b/.github/actions/send-tweet/package-lock.json new file mode 100644 index 0000000000..fc0d99cbe2 --- /dev/null +++ b/.github/actions/send-tweet/package-lock.json @@ -0,0 +1,388 @@ +{ + "name": "send-tweet", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@actions/core": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.9.1.tgz", + "integrity": "sha512-5ad+U2YGrmmiw6du20AQW5XuWo7UKN2052FjSV7MX+Wfjf8sCqcsZe62NfgHys4QI4/Y+vQvLKYL8jWtA1ZBTA==", + "requires": { + "@actions/http-client": "^2.0.1", + "uuid": "^8.3.2" + }, + "dependencies": { + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + } + } + }, + "@actions/http-client": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz", + "integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==", + "requires": { + "tunnel": "^0.0.6" + } + }, + "@zeit/ncc": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@zeit/ncc/-/ncc-0.21.1.tgz", + "integrity": "sha512-M9WzgquSOt2nsjRkYM9LRylBLmmlwNCwYbm3Up3PDEshfvdmIfqpFNSK8EJvR18NwZjGHE5z2avlDtYQx2JQnw==", + "dev": true + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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" + } + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "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=" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "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=" + }, + "aws4": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz", + "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==" + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "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=" + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "deep-extend": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.5.1.tgz", + "integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w==" + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "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==" + }, + "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", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "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==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, + "mime-db": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", + "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==" + }, + "mime-types": { + "version": "2.1.26", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", + "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", + "requires": { + "mime-db": "1.43.0" + } + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "psl": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.7.0.tgz", + "integrity": "sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==" + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==" + }, + "request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, + "safe-buffer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", + "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "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==", + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==" + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "twitter": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/twitter/-/twitter-1.7.1.tgz", + "integrity": "sha1-B2I3jx3BwFDkj2ZqypBOJLGpYvQ=", + "requires": { + "deep-extend": "^0.5.0", + "request": "^2.72.0" + } + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "^2.1.0" + } + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + } + } +} diff --git a/.github/actions/send-tweet/package.json b/.github/actions/send-tweet/package.json new file mode 100644 index 0000000000..520b128152 --- /dev/null +++ b/.github/actions/send-tweet/package.json @@ -0,0 +1,23 @@ +{ + "name": "send-tweet", + "version": "1.0.0", + "description": "Send Tweets from GitHub Actions workflows.", + "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.9.1", + "twitter": "^1.7.1" + }, + "devDependencies": { + "@zeit/ncc": "^0.21.1" + } +} 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" diff --git a/.github/resources/integ-service-account.json.gpg b/.github/resources/integ-service-account.json.gpg new file mode 100644 index 0000000000..fbfa531167 Binary files /dev/null and b/.github/resources/integ-service-account.json.gpg differ diff --git a/.github/scripts/generate_changelog.sh b/.github/scripts/generate_changelog.sh new file mode 100755 index 0000000000..3c97dca0cf --- /dev/null +++ b/.github/scripts/generate_changelog.sh @@ -0,0 +1,65 @@ +#!/bin/bash + +set -e +set -u + +function printChangelog() { + local TITLE=$1 + shift + # Skip the sentinel value. + local ENTRIES=("${@:2}") + if [ ${#ENTRIES[@]} -ne 0 ]; then + echo "### ${TITLE}" + echo "" + for ((i = 0; i < ${#ENTRIES[@]}; i++)) + do + echo "* ${ENTRIES[$i]}" + done + echo "" + fi +} + +if [[ -z "${GITHUB_SHA}" ]]; then + GITHUB_SHA="HEAD" +fi + +LAST_TAG=`git describe --tags $(git rev-list --tags --max-count=1) 2> /dev/null` || true +if [[ -z "${LAST_TAG}" ]]; then + echo "[INFO] No tags found. Including all commits up to ${GITHUB_SHA}." + VERSION_RANGE="${GITHUB_SHA}" +else + echo "[INFO] Last release tag: ${LAST_TAG}." + COMMIT_SHA=`git show-ref -s ${LAST_TAG}` + echo "[INFO] Last release commit: ${COMMIT_SHA}." + VERSION_RANGE="${COMMIT_SHA}..${GITHUB_SHA}" + echo "[INFO] Including all commits in the range ${VERSION_RANGE}." +fi + +echo "" + +# Older versions of Bash (< 4.4) treat empty arrays as unbound variables, which triggers +# errors when referencing them. Therefore we initialize each of these arrays with an empty +# sentinel value, and later skip them. +CHANGES=("") +FIXES=("") +FEATS=("") +MISC=("") + +while read -r line +do + COMMIT_MSG=`echo ${line} | cut -d ' ' -f 2-` + if [[ $COMMIT_MSG =~ ^change(\(.*\))?: ]]; then + CHANGES+=("$COMMIT_MSG") + elif [[ $COMMIT_MSG =~ ^fix(\(.*\))?: ]]; then + FIXES+=("$COMMIT_MSG") + elif [[ $COMMIT_MSG =~ ^feat(\(.*\))?: ]]; then + FEATS+=("$COMMIT_MSG") + else + MISC+=("${COMMIT_MSG}") + fi +done < <(git log ${VERSION_RANGE} --oneline) + +printChangelog "Breaking Changes" "${CHANGES[@]}" +printChangelog "New Features" "${FEATS[@]}" +printChangelog "Bug Fixes" "${FIXES[@]}" +printChangelog "Miscellaneous" "${MISC[@]}" diff --git a/.github/scripts/publish_package.sh b/.github/scripts/publish_package.sh new file mode 100755 index 0000000000..42d40c199e --- /dev/null +++ b/.github/scripts/publish_package.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# 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. + +set -e +set -u + +echo "//wombat-dressing-room.appspot.com/:_authToken=${NPM_AUTH_TOKEN}" >> .npmrc + +readonly UNPREFIXED_VERSION=`echo ${VERSION} | cut -c 2-` +npm publish ./dist/firebase-admin-${UNPREFIXED_VERSION}.tgz --registry https://wombat-dressing-room.appspot.com diff --git a/.github/scripts/publish_preflight_check.sh b/.github/scripts/publish_preflight_check.sh new file mode 100755 index 0000000000..8d2f4a14cd --- /dev/null +++ b/.github/scripts/publish_preflight_check.sh @@ -0,0 +1,171 @@ +#!/bin/bash + +# 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. + +###################################### Outputs ##################################### + +# 1. version: The version of this release including the 'v' prefix (e.g. v1.2.3). +# 2. changelog: Formatted changelog text for this release. + +#################################################################################### + +set -e +set -u + +function echo_info() { + local MESSAGE=$1 + echo "[INFO] ${MESSAGE}" +} + +function echo_warn() { + local MESSAGE=$1 + echo "[WARN] ${MESSAGE}" +} + +function terminate() { + echo "" + echo_warn "--------------------------------------------" + echo_warn "PREFLIGHT FAILED" + echo_warn "--------------------------------------------" + exit 1 +} + + +echo_info "Starting publish preflight check..." +echo_info "Git revision : ${GITHUB_SHA}" +echo_info "Workflow triggered by : ${GITHUB_ACTOR}" +echo_info "GitHub event : ${GITHUB_EVENT_NAME}" + + +echo_info "" +echo_info "--------------------------------------------" +echo_info "Extracting release version" +echo_info "--------------------------------------------" +echo_info "" + +echo_info "Loading version from: package.json" + +readonly VERSION_SCRIPT="const pkg = require('./package.json'); console.log(pkg.version);" +readonly RELEASE_VERSION=`node -e "${VERSION_SCRIPT}"` || true +if [[ -z "${RELEASE_VERSION}" ]]; then + echo_warn "Failed to extract release version from: package.json" + terminate +fi + +if [[ ! "${RELEASE_VERSION}" =~ ^([0-9]*)\.([0-9]*)\.([0-9]*)$ ]]; then + echo_warn "Malformed release version string: ${RELEASE_VERSION}. Exiting." + terminate +fi + +echo_info "Extracted release version: ${RELEASE_VERSION}" +echo "version=v${RELEASE_VERSION}" >> $GITHUB_OUTPUT + + +echo_info "" +echo_info "--------------------------------------------" +echo_info "Check release artifacts" +echo_info "--------------------------------------------" +echo_info "" + +if [[ ! -d dist ]]; then + echo_warn "dist directory does not exist." + terminate +fi + +readonly RELEASE_ARTIFACT="dist/firebase-admin-${RELEASE_VERSION}.tgz" +if [[ -f "${RELEASE_ARTIFACT}" ]]; then + echo_info "Found release artifact: ${RELEASE_ARTIFACT}" +else + echo_warn "Release artifact ${RELEASE_ARTIFACT} not found." + terminate +fi + +readonly ARTIFACT_COUNT=`ls dist/ | wc -l` +if [[ $ARTIFACT_COUNT -ne 1 ]]; then + echo_warn "Unexpected artifacts in the distribution directory." + ls -l dist + terminate +fi + + +echo_info "" +echo_info "--------------------------------------------" +echo_info "Checking previous releases" +echo_info "--------------------------------------------" +echo_info "" + +readonly NPM_DIST_TARBALL=`npm show firebase-admin@${RELEASE_VERSION} dist.tarball` +if [[ -n "${NPM_DIST_TARBALL}" ]]; then + echo_warn "Release version ${RELEASE_VERSION} already present in NPM." + terminate +else + echo_info "Release version ${RELEASE_VERSION} not found in NPM." +fi + + +echo_info "" +echo_info "--------------------------------------------" +echo_info "Checking release tag" +echo_info "--------------------------------------------" +echo_info "" + +echo_info "---< git fetch --depth=1 origin +refs/tags/*:refs/tags/* >---" +git fetch --depth=1 origin +refs/tags/*:refs/tags/* +echo "" + +readonly EXISTING_TAG=`git rev-parse -q --verify "refs/tags/v${RELEASE_VERSION}"` || true +if [[ -n "${EXISTING_TAG}" ]]; then + echo_warn "Tag v${RELEASE_VERSION} already exists. Exiting." + echo_warn "If the tag was created in a previous unsuccessful attempt, delete it and try again." + echo_warn " $ git tag -d v${RELEASE_VERSION}" + echo_warn " $ git push --delete origin v${RELEASE_VERSION}" + + readonly RELEASE_URL="https://github.com/firebase/firebase-admin-node/releases/tag/v${RELEASE_VERSION}" + echo_warn "Delete any corresponding releases at ${RELEASE_URL}." + terminate +fi + +echo_info "Tag v${RELEASE_VERSION} does not exist." + + +echo_info "" +echo_info "--------------------------------------------" +echo_info "Generating changelog" +echo_info "--------------------------------------------" +echo_info "" + +echo_info "---< git fetch origin master --prune --unshallow >---" +git fetch origin master --prune --unshallow +echo "" + +echo_info "Generating changelog from history..." +readonly CURRENT_DIR=$(dirname "$0") +readonly CHANGELOG=`${CURRENT_DIR}/generate_changelog.sh` +echo "$CHANGELOG" + +# Parse and preformat the text to handle multi-line output. +# See https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#example-of-a-multiline-string +# and https://github.com/github/docs/issues/21529#issue-1418590935 +FILTERED_CHANGELOG=`echo "$CHANGELOG" | grep -v "\\[INFO\\]"` || true +FILTERED_CHANGELOG="${FILTERED_CHANGELOG//$'\''/'"'}" +echo "changelog<> $GITHUB_OUTPUT +echo -e "$FILTERED_CHANGELOG" >> $GITHUB_OUTPUT +echo "CHANGELOGEOF" >> $GITHUB_OUTPUT + + +echo "" +echo_info "--------------------------------------------" +echo_info "PREFLIGHT SUCCESSFUL" +echo_info "--------------------------------------------" diff --git a/.github/scripts/run_integration_tests.sh b/.github/scripts/run_integration_tests.sh new file mode 100755 index 0000000000..37dc7d1216 --- /dev/null +++ b/.github/scripts/run_integration_tests.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +# 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. + +set -e +set -u + +gpg --quiet --batch --yes --decrypt --passphrase="${FIREBASE_SERVICE_ACCT_KEY}" \ + --output test/resources/key.json .github/resources/integ-service-account.json.gpg + +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/verifyReleaseTarball.sh b/.github/scripts/verify_package.sh similarity index 61% rename from verifyReleaseTarball.sh rename to .github/scripts/verify_package.sh index 2835477c1b..2b295a63b9 100755 --- a/verifyReleaseTarball.sh +++ b/.github/scripts/verify_package.sh @@ -21,55 +21,57 @@ # applications. set -e +set -u if [ -z "$1" ]; then echo "[ERROR] No package name provided." - echo "[INFO] Usage: ./verifyReleaseTarball.sh " + echo "[INFO] Usage: ./verify_package.sh " exit 1 fi -# Variables PKG_NAME="$1" -ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -MOCHA_CLI="$ROOT/node_modules/.bin/mocha -r ts-node/register" -DIR="$ROOT/test/integration/typescript" -WORK_DIR=`mktemp -d` - -if [ ! -f "$ROOT/$PKG_NAME" ]; then - echo "Package $PKG_NAME does not exist." +if [ ! -f "${PKG_NAME}" ]; then + echo "Package ${PKG_NAME} does not exist." exit 1 fi -# check if tmp dir was created -if [[ ! "$WORK_DIR" || ! -d "$WORK_DIR" ]]; then +# create a temporary directory +WORK_DIR=`mktemp -d` +if [[ ! "${WORK_DIR}" || ! -d "${WORK_DIR}" ]]; then echo "Could not create temp dir" exit 1 fi # deletes the temp directory -function cleanup { - rm -rf "$WORK_DIR" - echo "Deleted temp working directory $WORK_DIR" +function cleanup { + rm -rf "${WORK_DIR}" + echo "Deleted temp working directory ${WORK_DIR}" } # register the cleanup function to be called on the EXIT signal trap cleanup EXIT -# Enter work dir -pushd "$WORK_DIR" +# Copy package and test sources into working directory +cp "${PKG_NAME}" "${WORK_DIR}" +cp -r test/integration/postcheck/* "${WORK_DIR}" +cp test/resources/mock.key.json "${WORK_DIR}" -# Copy test sources into working directory -cp -r $DIR/* . -cp "$ROOT/test/resources/mock.key.json" . +# Enter work dir +pushd "${WORK_DIR}" # Install the test package npm install # Install firebase-admin package -npm install -S "$ROOT/$PKG_NAME" +npm install -S "${PKG_NAME}" echo "> tsc -p tsconfig.json" -tsc -p tsconfig.json +./node_modules/.bin/tsc -p tsconfig.json + +MOCHA_CLI="./node_modules/.bin/mocha" +TS_MOCHA_CLI="${MOCHA_CLI} -r ts-node/register" +echo "> $TS_MOCHA_CLI typescript/*.test.ts" +$TS_MOCHA_CLI typescript/*.test.ts -echo "> $MOCHA_CLI src/*.test.ts" -$MOCHA_CLI src/*.test.ts \ No newline at end of file +echo "> $MOCHA_CLI esm/*.js" +$MOCHA_CLI esm/*.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000..e4fb651cc4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +name: Continuous Integration + +on: pull_request + +jobs: + build: + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [14.x, 16.x] + + steps: + - uses: actions/checkout@v4 + - name: Set up Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + - name: Install and build + run: | + npm ci + npm run build + npm run build:tests + - 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@11.30.0 + 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/.github/workflows/nightly.yml b/.github/workflows/nightly.yml new file mode 100644 index 0000000000..c3edd0b335 --- /dev/null +++ b/.github/workflows/nightly.yml @@ -0,0 +1,108 @@ +# 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) + # or on 'firebase_nightly_build' repository dispatch event. + schedule: + - cron: "0 4,14 * * *" + repository_dispatch: + types: [firebase_nightly_build] + +jobs: + nightly: + + runs-on: ubuntu-latest + + steps: + - name: Checkout source for staging + uses: actions/checkout@v4 + with: + ref: ${{ github.event.client_payload.ref || github.ref }} + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 14.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 emulator-based integration tests + run: | + npm install -g firebase-tools@11.30.0 + 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' + + - 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 }} + FIREBASE_APP_ID: ${{ secrets.FIREBASE_APP_ID }} + + - 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 + + - 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: 'Nightly build ${{github.run_id}} of ${{github.repository}} failed!' + html: > + Nightly workflow ${{github.run_id}} 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: 'Nightly build ${{github.run_id}} of ${{github.repository}} cancelled!' + html: > + Nightly workflow ${{github.run_id}} cancelled on: ${{github.repository}} +

Navigate to the + cancelled workflow. + continue-on-error: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..e183bac114 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,152 @@ +# 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. + +name: Release Candidate + +on: + # Only run the workflow when a PR is updated or when a developer explicitly requests + # a build by sending a 'firebase_build' event. + pull_request: + types: [opened, synchronize, closed] + + repository_dispatch: + types: + - firebase_build + +jobs: + stage_release: + # To publish a release, merge the release PR with the label 'release:publish'. + # To stage a release without publishing it, send a 'firebase_build' event or apply + # the 'release:stage' label to a PR. + if: github.event.action == 'firebase_build' || + contains(github.event.pull_request.labels.*.name, 'release:stage') || + (github.event.pull_request.merged && + contains(github.event.pull_request.labels.*.name, 'release:publish')) + + runs-on: ubuntu-latest + + # When manually triggering the build, the requester can specify a target branch or a tag + # via the 'ref' client parameter. + steps: + - name: Checkout source for staging + uses: actions/checkout@v4 + with: + ref: ${{ github.event.client_payload.ref || github.ref }} + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 14.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 }} + FIREBASE_APP_ID: ${{ secrets.FIREBASE_APP_ID }} + + - 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 + + - name: Verify tarball + run: | + PACKAGE_TARBALL=`ls firebase-admin-*.tgz` + ./.github/scripts/verify_package.sh $PACKAGE_TARBALL + + publish_release: + needs: stage_release + + # Check whether the release should be published. We publish only when the trigger PR is + # 1. merged + # 2. to the master branch + # 3. with the label 'release:publish', and + # 4. the title prefix '[chore] Release '. + if: github.event.pull_request.merged && + github.ref == 'refs/heads/master' && + contains(github.event.pull_request.labels.*.name, 'release:publish') && + startsWith(github.event.pull_request.title, '[chore] Release ') + + runs-on: ubuntu-latest + + steps: + - name: Checkout source for publish + uses: actions/checkout@v4 + + # Download the artifacts created by the stage_release job. + - name: Download release candidates + uses: actions/download-artifact@v1 + with: + name: dist + + # Node.js and NPM are needed to complete the publish. + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 14.x + + - name: Publish preflight check + id: preflight + run: ./.github/scripts/publish_preflight_check.sh + + # See: https://cli.github.com/manual/gh_release_create + - name: Create release tag + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh release create ${{ steps.preflight.outputs.version }} + --title "Firebase Admin Node.js SDK ${{ steps.preflight.outputs.version }}" + --notes '${{ steps.preflight.outputs.changelog }}' + + - name: Publish to NPM + run: ./.github/scripts/publish_package.sh + env: + NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} + VERSION: ${{ steps.preflight.outputs.version }} + + # Post to Twitter if explicitly opted-in by adding the label 'release:tweet'. + - name: Post to Twitter + if: success() && + contains(github.event.pull_request.labels.*.name, 'release:tweet') + uses: ./.github/actions/send-tweet + with: + status: > + ${{ steps.preflight.outputs.version }} of @Firebase Admin Node.js SDK is available. + https://github.com/firebase/firebase-admin-node/releases/tag/${{ steps.preflight.outputs.version }} + consumer-key: ${{ secrets.TWITTER_CONSUMER_KEY }} + consumer-secret: ${{ secrets.TWITTER_CONSUMER_SECRET }} + access-token: ${{ secrets.TWITTER_ACCESS_TOKEN }} + access-token-secret: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} + continue-on-error: true diff --git a/.gitignore b/.gitignore index 36bdc4d823..9331c650de 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,11 @@ .idea +.vscode .DS_Store npm-debug.log lib/ .tmp/ +temp/ typings/ coverage/ node_modules/ @@ -12,6 +14,9 @@ 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 + +docgen/markdown/ diff --git a/.opensource/project.json b/.opensource/project.json new file mode 100644 index 0000000000..1f78544e44 --- /dev/null +++ b/.opensource/project.json @@ -0,0 +1,14 @@ +{ + "name": "Firebase Admin SDK - Node", + "platforms": [ + "Node", + "Admin" + ], + "content": "README.md", + "pages": [], + "related": [ + "firebase/firebase-admin-java", + "firebase/firebase-admin-go", + "firebase/firebase-admin-python" + ] +} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 49615f1694..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,6 +0,0 @@ -language: node_js -node_js: - - "8" - - "7" - - "6" - - "4" diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 746766c500..0000000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,456 +0,0 @@ -# Unreleased - -- - -# v5.10.0 - -- [changed] Upgraded Realtime Database client to v0.2.0. With this upgrade - developers can call the `admin.database().ref()` method with another - `Reference` instance as the argument. -- [changed] Upgraded Cloud Firestire client to v0.13.0. - -# v5.9.1 - -- [changed] The `admin.initializeApp()` method can now be invoked without an - explicit `credential` option. In that case the SDK will get initialized with - Google application default credentials. -- [changed] Upgraded Realtime Database client to v0.1.11. -- [changed] Modified the Realtime Database client integration to report the - correct user agent header. -- [changed] Upgraded Cloud Firestire client to v0.12.0. -- [changed] Improved error handling in FCM by mapping more server-side errors - to client-side error codes. - -# v5.9.0 - -- [added] Added the `messaging.send()` method and the new `Message` type for - sending Cloud Messaging notifications via the - [new FCM REST endpoint](https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages). - -# v5.8.2 - -- [changed] Exposed `admin.firestore.DocumentReference` and - `admin.firestore.DocumentSnapshot` types from the Admin SDK typings. -- [changed] Upgraded Firestore dependency version to - [0.11.2](https://github.com/googleapis/nodejs-firestore/releases/tag/v0.11.2). - -# v5.8.1 - -- [changed] Upgraded Firestore dependency version from 0.10.0 to 0.11.1. - This includes several bug fixes in Cloud Firestore. - -# v5.8.0 - -### Initialization - -- [added] The [`admin.initializeApp()`](https://firebase.google.com/docs/reference/admin/node/admin#.initializeApp) - method can now be invoked without any arguments. This initializes an app - using Google Application Default Credentials, and other - [`AppOptions`](https://firebase.google.com/docs/reference/admin/node/admin.app.AppOptions) loaded from - the `FIREBASE_CONFIG` environment variable. - -### Authentication - -- [changed] Upgraded the `jsonwebtoken` library to 8.1.0. - -# v5.7.0 - -### Authentication - -- [added] A new [`revokeRefreshTokens()`](https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#revokeRefreshTokens) - method for revoking refresh tokens issued to a user. -- [added] The [`verifyIdToken()`](https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#verifyIdToken) - method now accepts an optional `checkRevoked` argument, which can be used to - check if a given ID token has been revoked. - -# v5.6.0 - -- [added] A new [`admin.instanceId()`](https://firebase.google.com/docs/reference/admin/node/admin.instanceId) - API that facilitates deleting instance IDs and associated user data from - Firebase projects. -- [changed] Updated the TypeScript typings for `admin.AppOptions` to reflect the - introduction of the `projectId` option. -- [changed] Removed some unused third party dependencies. - -# v5.5.1 - -### Cloud Firestore - -- [changed] Upgraded the Cloud Firestore client to the latest available - version, which adds input validation to several operations, and retry logic - to handle network errors. - -### Realtime Database - -- [changed] Fixed an issue in the TypeScript typings of the Realtime Database API. - -# v5.5.0 - -### Realtime Database - -- [added] [`app.database()`](https://firebase.google.com/docs/reference/admin/node/admin.app.App#database) - method now optionally accepts a database URL. This feature can be used to - access multiple Realtime Database instances from the same app. -- [changed] Upgraded the Realtime Database client to the latest available - version. - -### Cloud Firestore - -- [changed] Upgraded the Cloud Firestore client to the latest available - version. - -# v5.4.3 - -- [changed] Fixed a regression in module loading that prevented using - the Admin SDK in environments like AWS Lambda. This regression was - introduced in the 5.4.0 release, which added a new dependency to Firestore - and gRPC. This fix lazily loads Firestore and gRPC, thus enabling - Admin SDK usage in the affected environments as long as no explicit - attempts are made to use the Firestore API. - - -# v5.4.2 - -- [changed] Upgraded the Cloud Firestore client dependency to 0.8.2, which - resolves an issue with saving objects with nested document references. - -# v5.4.1 - -- [changed] Upgraded the Firestore client dependency to 0.8.1, which resolves - the installation issues reported in the Yarn environment. - -# v5.4.0 - -- [added] A new [`admin.firestore()`](https://firebase.google.com/docs/reference/admin/node/admin.firestore) - API that facilitates accessing [Google Cloud Firestore](https://firebase.google.com/docs/firestore) - databases using the - [`@google-cloud/firestore`](https://cloud.google.com/nodejshttps://firebase.google.com/docs/reference/firestore/latest/) - library. See [Set Up Your Node.js App for Cloud Firestore](https://firebase.google.com/docs/firestore/server/setup-node) - to get started. - -# v5.3.0 - -- [changed] SDK now retries outbound HTTP calls on all low-level I/O errors. - -### Authentication - -- [added] A new [`setCustomUserClaims()`](https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#setCustomUserClaims) - method for setting custom claims on user accounts. Custom claims set via this - method become available on the ID tokens of the corresponding users when they - sign in. To learn how to use this API for controlling access to Firebase - resources, see - [Control Access with Custom Claims and Security Rules](https://firebase.google.com/docs/auth/admin/custom-claims). -- [added] A new [`listUsers()`](https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#listUsers) - method for listing all the users in a Firebase project in batches. - -### Storage - -- [changed] Declared a more concrete TypeScript return type (`Bucket`) for the - [`bucket()`](https://firebase.google.com/docs/reference/admin/node/admin.storage.Storage#bucket) method - in the Storage API. - -# v5.2.1 - -- [changed] A bug in the TypeScript type declarations that come bundled with the - SDK (`index.d.ts`) has been fixed. - -# v5.2.0 -- [added] A new [Cloud Storage API](https://firebase.google.com/docs/reference/admin/node/admin.storage) - that facilitates accessing Google Cloud Storage buckets using the - [`@google-cloud/storage`](https://googlecloudplatform.github.io/google-cloud-node/#https://firebase.google.com/docs/storage/latest/storage) - library. - -### Authentication - -- [changed] New type definitions for the arguments of `createUser()` and - `updateUser()` methods. - -### Cloud Messaging - -- [changed] Redefined the arguments of `sendToDevice()` using intersection - instead of overloading. - -# v5.1.0 - -### Authentication - -- [added] Added the method - [`getUserByPhoneNumber()`](https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#getUserByPhoneNumber) - to the [`admin.auth`](https://firebase.google.com/docs/reference/admin/node/admin.auth) interface. This method - enables retrieving user profile information by a phone number. -- [added] [`createUser()`](https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#createUser) - and [`updateUser()`](https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#updateUser) methods - now accept a `phoneNumber` property, which can be used to create users with a phone - number field and/or update the phone number associated with a user. -- [added] Added the `phoneNumber` field to - [`admin.auth.UserRecord`](https://firebase.google.com/docs/reference/admin/node/admin.auth.UserRecord), - which exposes the phone number associated with a user account. -- [added] Added the `phoneNumber` field to - [`admin.auth.UserInfo`](https://firebase.google.com/docs/reference/admin/node/admin.auth.UserInfo), - which exposes the phone number associated with a user account by a linked - identity provider. - -# v5.0.1 - -- [changed] Improved the error messages thrown in the case of network and RPC - errors. These errors now include outgoing HTTP request details that make - it easier to localize and debug issues. - -### Authentication - -- [changed] Implemented support in the user management API for handling photo - URLs with special characters. - -# v5.0.0 - -### Initialization - -- [changed] The deprecated `serviceAccount` property in the - [`admin.App.Options`](https://firebase.google.com/docs/reference/admin/node/admin.app.AppOptions) - type has been removed in favor of the `credential` property. -- [changed] Initializing the SDK without setting a credential - results in an exception. -- [changed] Initializing the SDK with a malformed private key string - results in an exception. - -### Authentication - -- [changed] `createdAt` and `lastSignedInAt` properties in - [`admin.auth.UserMetadata`](https://firebase.google.com/docs/reference/admin/node/admin.auth.UserMetadata) - have been renamed to `creationTime` and `lastSignInTime`. Also these - properties now provide UTC formatted strings instead of `Date` values. - -# v4.2.1 - -- [changed] Updated the SDK to periodically refresh the OAuth access token - internally used by `FirebaseApp`. This reduces the number of authentication - failures encountered at runtime by SDK components like Realtime Database. - -# v4.2.0 - -### Cloud Messaging - -- [added] Added the methods - [`subscribeToTopic()`](https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#subscribeToTopic) - and - [`unsubscribeFromTopic()`](https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#unsubscribeFromTopic) - to the [`admin.messaging()`](https://firebase.google.com/docs/reference/admin/node/admin.messaging) - service. The new methods allow subscribing to and unsubscribing from {{messaging}} - topics via registration tokens. - -# v4.1.4 - -### Authentication - -- [changed] Cleaned up a number of types to improve the log output, thereby - making debugging easier. - -### Realtime Database - -- [changed] Fixed an issue which could cause infinite loops when using `push()` - with no arguments. - -# v4.1.3 - -- [changed] Fixed incorrect usage of `undefined` - as opposed to `void` - in - several places in the TypeScript typings. -- [changed] Added missing properties to the TypeScript typings for - [`DecodedIdToken`](https://firebase.google.com/docs/reference/admin/node/admin.auth.DecodedIdToken). -- [changed] Fixed issues when using some types with the TypeScript - `strictNullChecks` option enabled. -- [changed] Removed incorrect `admin.Promise` type from the TypeScript typings - in favor of the Node.js built-in `Promise` type, which the SDK actually uses. -- [changed] Added error codes to all app-level errors. All errors in the SDK - now properly implement the - [`FirebaseError`](https://firebase.google.com/docs/reference/admin/node/admin.FirebaseError) interface. -- [changed] Improved error handling when initializing the SDK with a credential - that cannot generate valid access tokens. -- [added] Added new `admin.database.EventType` to the TypeScript typings. - -### Realtime Database - -- [changed] Improved how the Realtime Database reports errors when provided with - various types of invalid credentials. - -# v4.1.2 - -### Authentication - -- [changed] Improved input validation and error messages for all user - management methods. -- [changed] - [`verifyIdToken()`](https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#verifyIdToken) - now works with non-cert credentials, assuming the `GCLOUD_PROJECT` environment - variable is set to your project ID, which is the case when running on Google - infrastructure such as Google App Engine and Google Compute Engine. - -### Realtime Database - -- [changed] Added `toJSON()` methods to the `DataSnapshot` and `Query` objects - to make them properly JSON-serializable. - -### Cloud Messaging - -- [changed] Improved response parsing when - [`sendToDevice()`](https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#sendToDevice) - and - [`sendToDeviceGroup()`](https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#sendToDeviceGroup) - are provided with unexpected inputs. - - -# v4.1.1 - -- [changed] Added in missing TypeScript typings for the `FirebaseError.toJSON()` - method. - -### Authentication - -- [changed] Fixed issue with - [`createUser()`](https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#createUser) - which sometimes caused multiple users to share the same email. - - -# v4.1.0 - -- [changed] Added in missing TypeScript typings for the `toJSON()` method off - of several objects. - -### Cloud Messaging - -- [added] A new - [`admin.messaging()`](https://firebase.google.com/docs/reference/admin/node/admin.messaging) service - allows you to send messages through - [Firebase Cloud Messaging](https://firebase.google.com/docs/cloud-messaging/admin/). The new service - includes the - [`sendToDevice()`](https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#sendToDevice), - [`sendToDeviceGroup()`](https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#sendToDeviceGroup), - [`sendToTopic()`](https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#sendToTopic), - and - [`sendToCondition()`](https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#sendToCondition) - methods. - - -# v4.0.6 - -### Initialization - -- [changed] Fixed an issue which caused importing the library via the ES2015 - import syntax (`import * as admin from "firebase-admin"`) to not work - properly. - - -# v4.0.5 - -- [changed] TypeScript support has been greatly improved. Typings for the - Realtime Database are now available and all other known issues with incorrect or - incomplete type information have been resolved. - -### Initialization - -- [changed] Fixed an issue which caused the SDK to appear to hang when provided - with a credential that generated invalid access tokens. The most common cause - of this was using a credential whose access had been revoked. Now, an error - will be logged to the console in this scenario. - -### Authentication - -- [added] The error message for an `auth/internal-error` error now includes - the raw server response to more easily debug and track down unhandled errors. -- [changed] Fixed an issue that caused an `auth/internal-error` error to be - thrown when calling - [`getUser()`](https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#getUser) or - [`getUserByEmail()`](https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#getUserByEmail) - for a user without a creation date. -- [changed] Fixed an issue which caused an `auth/internal-error` error to be - thrown when calling - [`createUser()`](https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#createUser) with - an email that corresponds to an existing user. -- [changed] Fixed an issue which caused an `auth/internal-error` error to be - thrown when calling Authentication methods with a credential with insufficient - permission. Now, an `auth/insufficient-permission` error will be thrown - instead. - - -# v4.0.4 - -### Authentication - -- [changed] Fixed an issue that caused several Authentication methods to throw - an error when provided with inputs containing Unicode characters. - - -# v4.0.3 - -### Initialization - -- [changed] Fixed an issue that caused a `null` value for the - `databaseAuthVariableOverride` property to be ignored when passed as part - of the first argument to - [`initializeApp()`](https://firebase.google.com/docs/reference/admin/node/admin#.initializeApp), which - caused the app to still have full admin access. Now, passing this value has - the expected behavior: the app has unauthenticated access to the - Realtime Database, and behaves as if no user is logged into the app. - -### Authentication - -- [changed] Fixed an issue that caused an `auth/invalid-uid` error to - be thrown for valid `uid` values passed to several Authentication methods. - - -# v4.0.2 - -- [added] Improved error messages throughout the Admin Node.js SDK. -- [changed] Upgraded dependencies so that the Admin Node.js SDK no longer - throws warnings for using deprecated `Buffer` APIs in Node.js `7.x.x`. - - -# v4.0.1 - -- [changed] Fixed issue which caused the 4.0.0 release to not - include the `README.md` and `npm-shrinkwrap.json` files. - - -# v4.0.0 - -- [added] The Admin Node.js SDK (available on npm as `firebase-admin`) is a - new SDK which replaces and expands the admin capabilities of the standard - `firebase` npm module. See - [Add the Firebase Admin SDK to your Server](https://firebase.google.com/docs/admin/setup/) to get - started. -- [issue] This version does not include the `README.md` and - `npm-shrinkwrap.json` files. This was fixed in version 4.0.1. - -### Initialization - -- [deprecated] The `serviceAccount` property of the options passed as the - first argument to - [`initializeApp()`](https://firebase.google.com/docs/reference/admin/node/admin#.initializeApp) has been - deprecated in favor of a new `credential` property. See - [Initialize the SDK](https://firebase.google.com/docs/admin/setup/#initialize_the_sdk) for more details. -- [added] The new - [`admin.credential.cert()`](https://firebase.google.com/docs/reference/admin/node/admin.credential#.cert) - method allows you to authenticate the SDK with a service account key file. -- [added] The new - [`admin.credential.refreshToken()`](https://firebase.google.com/docs/reference/admin/node/admin.credential#.refreshToken) - method allows you to authenticate the SDK with a Google OAuth2 refresh token. -- [added] The new - [`admin.credential.applicationDefault()`](https://firebase.google.com/docs/reference/admin/node/admin.credential#.applicationDefault) - method allows you to authenticate the SDK with Google Application Default - Credentials. - -### Authentication - -- [added] A new Admin API for managing your Firebase Authentication users is now - available. This API lets you manage your users without using their existing - credentials, and without worrying about client-side rate limiting. The new - methods included in this API are - [`getUser()`](https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#getUser), - [`getUserByEmail()`](https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#getUserByEmail), - [`createUser()`](https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#createUser), - [`updateUser()`](https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#updateUser), and - [`deleteUser()`](https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#deleteUser). See - [Manage Users](https://firebase.google.com/docs/auth/admin/manage-users) for more details. -- [changed] The - [`createCustomToken()`](https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#createCustomToken) - method is now asynchronous, returning a `Promise` instead of a - `string`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 42c8942215..28d15bf290 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -85,6 +85,12 @@ information on using pull requests. ## Need to get set up locally? +### Prerequisites + +1. Node.js 16 or higher. +2. `npm` 6 or higher. +3. Google Cloud SDK ([`gcloud`](https://cloud.google.com/sdk/downloads) utility). + ### Initial Setup Run the following commands from the command line to get your local environment set up: @@ -92,13 +98,11 @@ Run the following commands from the command line to get your local environment s ```bash $ git clone https://github.com/firebase/firebase-admin-node.git $ cd firebase-admin-node # go to the firebase-admin-node directory -$ npm install -g gulp # globally install gulp task runner $ npm install # install local npm build / test dependencies ``` -In order to run the tests, you also need to -[download the `gcloud` CLI](https://cloud.google.com/sdk/downloads), run the following command, and -follow the prompts: +In order to run the tests, you also need to authorize the `gcloud` utility with +Google application default credentials: ```bash $ gcloud beta auth application-default login @@ -119,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 @@ -131,27 +137,142 @@ If you wish to skip the linter, and only run the unit tests: $ npm run test:unit ``` -The integration test suite requires a service account JSON key file, and an API key for a Firebase -project. Create a new project in the [Firebase console](https://console.firebase.google.com) if -you do not already have one. Use a separate, dedicated project for integration tests since the -test suite makes a large number of writes to the Firebase realtime database. Download the service -account key file from the "Settings > Service Accounts" page of the project, and copy it to -`test/resources/key.json`. Also obtain the API key for the same project from "Settings > General", -and save it to `test/resources/apikey.txt`. Finally, to run the integration test suite: +#### 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 + +Integration tests are executed against a real life Firebase project. If you do not already +have one suitable for running the tests against, you can create a new project in the +[Firebase Console](https://console.firebase.google.com) following the setup guide below. +If you already have a Firebase project, you'll need to obtain credentials to communicate and +authorize access to your Firebase project: + +1. Service account certificate: This allows access to your Firebase project through a service account +which is required for all integration tests. This can be downloaded as a JSON file from the +**Settings > Service Accounts** tab of the Firebase console when you click the +**Generate new private key** button. Copy the file into the repo so it's available at +`test/resources/key.json`. + > **Note:** Service accounts should be carefully managed and their keys should never be stored in publicly accessible source code or repositories. + + +2. Web API key: This allows for Auth sign-in needed for some Authentication and Tenant Management +integration tests. This is displayed in the **Settings > General** tab of the Firebase console +after enabling Authentication as described in the steps below. Copy it and save to a new text +file at `test/resources/apikey.txt`. + + +Set up your Firebase project as follows: + + +1. Enable Authentication: + 1. Go to the Firebase Console, and select **Authentication** from the **Build** menu. + 2. Click on **Get Started**. + 3. Select **Sign-in method > Add new provider > Email/Password** then enable both the + **Email/Password** and **Email link (passwordless sign-in)** options. + + +2. Enable Firestore: + 1. Go to the Firebase Console, and select **Firestore Database** from the **Build** menu. + 2. Click on the **Create database** button. You can choose to set up Firestore either in + the production mode or in the test mode. + + +3. Enable Realtime Database: + 1. Go to the Firebase Console, and select **Realtime Database** from the **Build** menu. + 2. Click on the **Create Database** button. You can choose to set up the Realtime Database + either in the locked mode or in the test mode. + + > **Note:** Integration tests are not run against the default Realtime Database reference and are + instead run against a database created at `https://{PROJECT_ID}.firebaseio.com`. + This second Realtime Database reference is created in the following steps. + + 3. In the **Data** tab click on the kebab menu (3 dots) and select **Create Database**. + 4. Enter your Project ID (Found in the **General** tab in **Account Settings**) as the + **Realtime Database reference**. Again, you can choose to set up the Realtime Database + either in the locked mode or in the test mode. + + +4. Enable Storage: + 1. Go to the Firebase Console, and select **Storage** from the **Build** menu. + 2. Click on the **Get started** button. You can choose to set up Cloud Storage + either in the production mode or in the test mode. + + +5. Enable the Firebase ML API: + 1. Go to the + [Google Cloud console | Firebase ML API](https://console.cloud.google.com/apis/api/firebaseml.googleapis.com/overview) + and make sure your project is selected. + 2. If the API is not already enabled, click **Enable**. + + +6. Enable the IAM API: + 1. Go to the [Google Cloud console](https://console.cloud.google.com) + and make sure your Firebase project is selected. + 2. Select **APIs & Services** from the main menu, and click the + **ENABLE APIS AND SERVICES** button. + 3. Search for and enable **Identity and Access Management (IAM) API** by Google Enterprise API. + + +7. Enable Tenant Management: + 1. Go to + [Google Cloud console | Identity Platform](https://console.cloud.google.com/customer-identity/) + and if it is not already enabled, click **Enable**. + 2. Then + [enable multi-tenancy](https://cloud.google.com/identity-platform/docs/multi-tenancy-quickstart#enabling_multi-tenancy) + for your project. + + +8. Ensure your service account has 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 console | IAM & admin](https://console.cloud.google.com/iam-admin). + 2. Find your service account in the list. If not added click the pencil icon to edit its + permissions. + 3. Click **ADD ANOTHER ROLE** and choose **Firebase Authentication Admin**. + 4. Click **SAVE**. + + +Finally, to run the integration test suite: ```bash $ npm run integration # Build and run integration test suite ``` -The integration test suite overwrites the security rules present in your Firebase project. You -will be prompted before the overwrite takes place: +By default the integration test suite does not modify the Firebase security rules for the +Realtime Database. If you want to force update the rules, so that the relevant Database +integration tests can pass, launch the tests as follows: +```bash +$ npm run test:integration -- --updateRules ``` -Warning: This test will overwrite your project's existing Database rules. -Overwrite Database rules for tests? -* 'yes' to agree -* 'skip' to continue without the overwrite -* 'no' to cancel + +The integration test suite skips the multi-tenancy Auth tests by default. +If you want to run these tests, an +[Identity Platform](https://cloud.google.com/identity-platform/) project with multi-tenancy +[enabled](https://cloud.google.com/identity-platform/docs/multi-tenancy-quickstart#enabling_multi-tenancy) +will be required. +An existing Firebase project can be upgraded to an Identity Platform project without +losing any functionality via the +[Identity Platform Marketplace Page](https://console.cloud.google.com/customer-identity). +Note that charges may be incurred for active users beyond the Identity Platform free tier. +The integration tests can be launched with these tests enabled as follows: + +```bash +$ npm run test:integration -- --testMultiTenancy ``` ### Repo Organization diff --git a/README.md b/README.md index d84b429e9d..2d466926a7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Build Status](https://travis-ci.org/firebase/firebase-admin-node.svg?branch=master)](https://travis-ci.org/firebase/firebase-admin-node) +[![Build Status](https://github.com/firebase/firebase-admin-node/workflows/Continuous%20Integration/badge.svg)](https://github.com/firebase/firebase-admin-node/actions) # Firebase Admin Node.js SDK @@ -9,6 +9,7 @@ * [Installation](#installation) * [Contributing](#contributing) * [Documentation](#documentation) + * [Supported Environments](#supported-environments) * [Acknowledgments](#acknowledgments) * [License](#license) @@ -35,13 +36,17 @@ $ npm install --save firebase-admin To use the module in your application, `require` it from any JavaScript file: ```js -var admin = require("firebase-admin"); +const { initializeApp } = require("firebase-admin/app"); + +initializeApp(); ``` If you are using ES2015, you can `import` the module instead: ```js -import * as admin from "firebase-admin"; +import { initializeApp } from "firebase-admin/app"; + +initializeApp(); ``` @@ -52,6 +57,18 @@ about how you can contribute to this project. We welcome bug reports, feature requests, code review feedback, and also pull requests. +## Supported Environments + +We support Node.js 14 and higher. However, Node.js 14 support is deprecated. We strongly encourage +you to use Node.js 16 or higher as we will drop support for Node.js 14 in the next major version. + +Please also note that the Admin SDK should only +be used in server-side/back-end environments controlled by the app developer. +This includes most server and serverless platforms (both on-premise and in +the cloud). It is not recommended to use the Admin SDK in client-side +environments. + + ## Documentation * [Setup Guide](https://firebase.google.com/docs/admin/setup/) diff --git a/api-extractor.json b/api-extractor.json new file mode 100644 index 0000000000..fede69a665 --- /dev/null +++ b/api-extractor.json @@ -0,0 +1,46 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "mainEntryPointFilePath": "/lib/default-namespace.d.ts", + "bundledPackages": [], + "compiler": { + + }, + "apiReport": { + "enabled": true, + "reportFileName": "" + }, + "docModel": { + "enabled": true + }, + "dtsRollup": { + "enabled": false + }, + "tsdocMetadata": { + "enabled": false + }, + "messages": { + "compilerMessageReporting": { + "default": { + "logLevel": "warning" + } + }, + "extractorMessageReporting": { + "default": { + "logLevel": "warning" + }, + + "ae-missing-release-tag": { + "logLevel": "none" + }, + + "ae-unresolved-link": { + "logLevel": "error" + } + }, + "tsdocMessageReporting": { + "default": { + "logLevel": "error" + } + } + } +} diff --git a/createReleaseTarball.sh b/createReleaseTarball.sh deleted file mode 100755 index 49faf45081..0000000000 --- a/createReleaseTarball.sh +++ /dev/null @@ -1,193 +0,0 @@ -# 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. - -#!/bin/bash - -# Helper function to print the usage instructions. -printUsage () { - echo "[INFO] Usage: $0 " - echo "[INFO] is the version number of the tarball you want to create." -} - - -############## -# PROLOGUE # -############## - -echo "[INFO] This script only affects your local repo and can be used at any time during development or release." -echo - -# Print usage instructions if the first argument is -h or --help. -if [[ $1 == "-h" || $1 == "--help" ]]; then - printUsage - exit 1 -fi - -VERSION=$1 -VERSION_WITHOUT_RC=${VERSION%-*} - - -############################# -# VALIDATE VERSION NUMBER # -############################# -if [[ -z $VERSION ]]; then - echo "[ERROR] Version number not provided." - echo - printUsage - exit 1 -elif ! [[ $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z-]+)?$ ]]; then - echo "[ERROR] Version number (${VERSION}) must be a valid SemVer number." - echo - printUsage - exit 1 -fi - - -################### -# VALIDATE REPO # -################### -# Ensure the checked out branch is master. -CHECKED_OUT_BRANCH="$(git branch | grep "*" | awk -F ' ' '{print $2}')" -if [[ $CHECKED_OUT_BRANCH != "master" ]]; then - read -p "[WARNING] You are on the '${CHECKED_OUT_BRANCH}' branch, not 'master'. Continue? (y/N) " CONTINUE - echo - - if ! [[ $CONTINUE == "y" || $CONTINUE == "Y" ]]; then - echo "[INFO] You chose not to continue." - exit 1 - fi -fi - -# Make sure the master branch does not have existing changes. -if ! git --git-dir=".git" diff --quiet; then - read -p "[WARNING] You have uncommitted changes on the current branch. Continue? (y/N) " CONTINUE - echo - - if ! [[ $CONTINUE == "y" || $CONTINUE == "Y" ]]; then - echo "[INFO] You chose not to continue." - exit 1 - fi -fi - - -######################### -# UPDATE package.json # -######################### -echo "[INFO] Updating version number in package.json to ${VERSION_WITHOUT_RC}..." -sed -i '' -e s/"\"version\": \".*\""/"\"version\": \"${VERSION_WITHOUT_RC}\""/ package.json -echo - -######################### -# UPDATE CHANGELOG.md # -######################### -echo "[INFO] Updating version number in CHANGELOG.md to ${VERSION_WITHOUT_RC}..." -sed -i '' -e "/^# Unreleased$/d" CHANGELOG.md -echo -e "# Unreleased\n\n-\n\n# v${VERSION_WITHOUT_RC}" | cat - CHANGELOG.md > TEMP_CHANGELOG.md -mv TEMP_CHANGELOG.md CHANGELOG.md - -############################ -# REINSTALL DEPENDENCIES # -############################ -echo "[INFO] Removing lib/, and node_modules/..." -rm -rf lib/ node_modules/ -if [[ $? -ne 0 ]]; then - echo "Error: Failed to remove lib/, and node_modules/." - exit 1 -fi -echo - -echo "[INFO] Installing production node modules..." -npm install --production -if [[ $? -ne 0 ]]; then - echo "Error: Failed to install production node modules." - exit 1 -fi -echo - - -############################ -# CREATE RELEASE TARBALL # -############################ -echo "[INFO] Installing all node modules..." -npm install -if [[ $? -ne 0 ]]; then - echo "Error: Failed to install all node modules." - exit 1 -fi -echo - -echo "[INFO] Running linter..." -npm run lint -if [[ $? -ne 0 ]]; then - echo "Error: Linter failed." - exit 1 -fi -echo - -echo "[INFO] Running unit tests..." -npm run test:unit -if [[ $? -ne 0 ]]; then - echo "Error: Unit tests failed." - exit 1 -fi -echo - -echo "[INFO] Building the release package contents..." -npm run build -if [[ $? -ne 0 ]]; then - echo "Error: Failed to build release package contents." - exit 1 -fi -echo - -echo "[INFO] Running integration tests..." -npm run test:integration -- --updateRules -if [[ $? -ne 0 ]]; then - echo "Error: Integration tests failed." - exit 1 -fi -echo - -echo "[INFO] Packaging up release tarball..." -npm pack -if [[ $? -ne 0 ]]; then - echo "Error: Failed to package up release tarball." - exit 1 -fi -echo - -# Since npm pack uses the version number in the package.json when creating the tarball, -# rename the tarball to include the RC version. -mv firebase-admin-${VERSION_WITHOUT_RC}.tgz firebase-admin-${VERSION}.tgz - - -############################ -# VERIFY RELEASE TARBALL # -############################ -echo "[INFO] Running release tarball verification..." -bash verifyReleaseTarball.sh firebase-admin-${VERSION}.tgz -if [[ $? -ne 0 ]]; then - echo "Error: Release tarball failed verification." - exit 1 -fi -echo - - -############## -# EPILOGUE # -############## - -echo "[INFO] firebase-admin-${VERSION}.tgz successfully created!" -echo "[INFO] Create a CL for the updated package.json if this is an actual release." -echo diff --git a/docgen/extras/firebase-admin.database.md b/docgen/extras/firebase-admin.database.md new file mode 100644 index 0000000000..0cce501db4 --- /dev/null +++ b/docgen/extras/firebase-admin.database.md @@ -0,0 +1,12 @@ +## External API Re-exports + +The following externally defined APIs are re-exported from this module entry point for convenience. + +| Symbol | Description | +| --- | --- | +| [DataSnapshot](https://firebase.google.com/docs/reference/js/v8/firebase.database.DataSnapshot) | `DataSnapshot` type from the `@firebase/database-compat` package. | +| [EventType](https://firebase.google.com/docs/reference/js/v8/firebase.database#eventtype) | `EventType` type from the `@firebase/database-compat` package. | +| [OnDisconnect](https://firebase.google.com/docs/reference/js/v8/firebase.database.OnDisconnect) | `OnDisconnect` type from the `@firebase/database-compat` package. | +| [Query](https://firebase.google.com/docs/reference/js/v8/firebase.database.Query) | `Query` type from the `@firebase/database-compat` package. | +| [Reference](https://firebase.google.com/docs/reference/js/v8/firebase.database.Reference) | `Reference` type from the `@firebase/database-compat` package. | +| [ThenableReference](https://firebase.google.com/docs/reference/js/v8/firebase.database.ThenableReference) | `ThenableReference` type from the `@firebase/database-compat` package. | diff --git a/docgen/extras/firebase-admin.firestore.md b/docgen/extras/firebase-admin.firestore.md new file mode 100644 index 0000000000..f00750727b --- /dev/null +++ b/docgen/extras/firebase-admin.firestore.md @@ -0,0 +1,35 @@ +## External API Re-exports + +The following externally defined APIs are re-exported from this module entry point for convenience. + +| Symbol | Description | +| --- | --- | +| [BulkWriter](https://cloud.google.com/nodejs/docs/reference/firestore/latest/firestore/bulkwriter) | `BulkWriter` type from the `@google-cloud/firestore` package. | +| [AggregateField](https://cloud.google.com/nodejs/docs/reference/firestore/latest/firestore/aggregatefield) | `AggregateField` type from the `@google-cloud/firestore` package. | +| [BulkWriterOptions](https://cloud.google.com/nodejs/docs/reference/firestore/latest/firestore/bulkwriter) | `BulkWriterOptions` type from the `@google-cloud/firestore` package. | +| [BundleBuilder](https://googleapis.dev/nodejs/firestore/latest/BundleBuilder.html) | `BundleBuilder` type from the `@google-cloud/firestore` package. | +| [CollectionGroup](https://cloud.google.com/nodejs/docs/reference/firestore/latest/firestore/collectiongroup) | `CollectionGroup` type from the `@google-cloud/firestore` package. | +| [CollectionReference](https://cloud.google.com/nodejs/docs/reference/firestore/latest/firestore/collectionreference) | `CollectionReference` type from the `@google-cloud/firestore` package. | +| [DocumentChange](https://cloud.google.com/nodejs/docs/reference/firestore/latest/firestore/documentchange) | `DocumentChange` type from the `@google-cloud/firestore` package. | +| [DocumentData](https://googleapis.dev/nodejs/firestore/latest/global.html#DocumentData) | `DocumentData` type from the `@google-cloud/firestore` package. | +| [DocumentReference](https://cloud.google.com/nodejs/docs/reference/firestore/latest/firestore/documentreference) | `DocumentReference` type from the `@google-cloud/firestore` package. | +| [DocumentSnapshot](https://cloud.google.com/nodejs/docs/reference/firestore/latest/firestore/documentsnapshot) | `DocumentSnapshot` type from the `@google-cloud/firestore` package. | +| [FieldPath](https://cloud.google.com/nodejs/docs/reference/firestore/latest/firestore/fieldpath) | `FieldPath` type from the `@google-cloud/firestore` package. | +| [FieldValue](https://cloud.google.com/nodejs/docs/reference/firestore/latest/firestore/fieldvalue) | `FieldValue` type from the `@google-cloud/firestore` package. | +| [Filter](https://cloud.google.com/nodejs/docs/reference/firestore/latest/firestore/filter) | `Filter` type from the `@google-cloud/firestore` package. | +| [Firestore](https://cloud.google.com/nodejs/docs/reference/firestore/latest/firestore/firestore) | `Firestore` type from the `@google-cloud/firestore` package. | +| [FirestoreDataConverter](https://googleapis.dev/nodejs/firestore/latest/global.html#FirestoreDataConverter) | `FirestoreDataConverter` type from the `@google-cloud/firestore` package. | +| [GeoPoint](https://cloud.google.com/nodejs/docs/reference/firestore/latest/firestore/geopoint) | `GeoPoint` type from the `@google-cloud/firestore` package. | +| [GrpcStatus](https://googleapis.dev/nodejs/firestore/latest/global.html#GrpcStatus) | `GrpcStatus` type from the `@google-cloud/firestore` package. | +| [Precondition](https://googleapis.dev/nodejs/firestore/latest/global.html#Precondition) | `Precondition` type from the `@google-cloud/firestore` package. | +| [Query](https://cloud.google.com/nodejs/docs/reference/firestore/latest/firestore/query) | `Query` type from the `@google-cloud/firestore` package. | +| [QueryDocumentSnapshot](https://cloud.google.com/nodejs/docs/reference/firestore/latest/firestore/querydocumentsnapshot) | `QueryDocumentSnapshot` type from the `@google-cloud/firestore` package. | +| [QueryPartition](https://cloud.google.com/nodejs/docs/reference/firestore/latest/firestore/querypartition) | `QueryPartition` type from the `@google-cloud/firestore` package. | +| [QuerySnapshot](https://cloud.google.com/nodejs/docs/reference/firestore/latest/firestore/querysnapshot) | `QuerySnapshot` type from the `@google-cloud/firestore` package. | +| [ReadOptions](https://googleapis.dev/nodejs/firestore/latest/global.html#ReadOptions) | `ReadOptions` type from the `@google-cloud/firestore` package. | +| [SetOptions](https://googleapis.dev/nodejs/firestore/latest/global.html#SetOptions) | `SetOptions` type from the `@google-cloud/firestore` package. | +| [Timestamp](https://cloud.google.com/nodejs/docs/reference/firestore/latest/firestore/timestamp) | `Timestamp` type from the `@google-cloud/firestore` package. | +| [Transaction](https://cloud.google.com/nodejs/docs/reference/firestore/latest/firestore/transaction) | `Transaction` type from the `@google-cloud/firestore` package. | +| [WriteBatch](https://cloud.google.com/nodejs/docs/reference/firestore/latest/firestore/writebatch) | `WriteBatch` type from the `@google-cloud/firestore` package. | +| [WriteResult](https://cloud.google.com/nodejs/docs/reference/firestore/latest/firestore/writeresult) | `WriteResult` type from the `@google-cloud/firestore` package. | +| [setLogFunction](https://googleapis.dev/nodejs/firestore/latest/global.html#setLogFunction) | `setLogFunction` function from the `@google-cloud/firestore` package. | diff --git a/docgen/post-process.js b/docgen/post-process.js new file mode 100644 index 0000000000..9c0a79bdcd --- /dev/null +++ b/docgen/post-process.js @@ -0,0 +1,181 @@ +/** + * @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. + */ + +const fs = require('mz/fs'); +const path = require('path'); +const readline = require('readline'); + +async function main() { + await applyExtras(); + await fixHomePage(); + await fixTitles(); +} + +/** + * Adds extra content to the generated markdown files. Content in each file in the `extras/` + * directory is added/merged to the top of the corresponding markdown file in the `markdown/` + * directory. + */ +async function applyExtras() { + const extras = await getExtraFiles(); + for (const source of extras) { + await applyExtraContentFrom(source); + } +} + +/** + * Replace dotted module names in the home page with the correct slash-separated names. For + * example, `firebase-admin.foo` becomes `firebase-admin/foo`. Also replaces the term "Package" + * with "Module" for accuracy. + */ +async function fixHomePage() { + const homePage = path.join(__dirname, 'markdown', 'index.md'); + const content = await fs.readFile(homePage); + const updatedText = content.toString() + .replace(/\[firebase-admin\./g, '[firebase-admin/') + .replace(/_package/g, '_module') + .replace(/Package/g, 'Module'); + console.log(`Updating module listings in ${homePage}`); + await fs.writeFile(homePage, updatedText); +} + +/** + * Replaces dotted module names and the term "package" in page titles. For example, the title text + * `firebase-admin.foo package` becomes `firebase-admin/foo module`. + */ +async function fixTitles() { + const markdownDir = path.join(__dirname, 'markdown'); + const files = await fs.readdir(markdownDir); + for (const file of files) { + await fixTitleOf(path.join(markdownDir, file)); + } + + const tocFile = path.join(markdownDir, 'toc.yaml'); + await fixTocTitles(tocFile); +} + +async function fixTitleOf(file) { + const reader = readline.createInterface({ + input: fs.createReadStream(file), + }); + + const buffer = []; + let updated = false; + for await (let line of reader) { + if (line.startsWith('{% block title %}')) { + if (line.match(/firebase-admin\./)) { + line = line.replace(/firebase-admin\./, 'firebase-admin/').replace('package', 'module'); + updated = true; + } else { + break; + } + } + + buffer.push(line); + } + + if (updated) { + console.log(`Updating title in ${file}`); + const content = Buffer.from(buffer.join('\n')); + await fs.writeFile(file, content); + } +} + +async function fixTocTitles(file) { + const reader = readline.createInterface({ + input: fs.createReadStream(file), + }); + + const buffer = []; + for await (let line of reader) { + if (line.includes('- title: firebase-admin.')) { + line = line.replace(/firebase-admin\./, 'firebase-admin/'); + } + + buffer.push(line); + } + + console.log(`Updating titles in ${file}`); + const content = Buffer.from(buffer.join('\n')); + await fs.writeFile(file, content); +} + +async function getExtraFiles() { + const extrasPath = path.join(__dirname, 'extras'); + const files = await fs.readdir(extrasPath); + return files + .filter((name) => name.endsWith('.md')) + .map((name) => path.join(__dirname, 'extras', name)); +} + +async function applyExtraContentFrom(source) { + const target = path.join(__dirname, 'markdown', path.basename(source)); + if (!await fs.exists(target)) { + console.log(`Target path not found: ${target}`); + return; + } + + const extra = await readExtraContentFrom(source); + await writeExtraContentTo(target, extra); +} + +async function readExtraContentFrom(source) { + const reader = readline.createInterface({ + input: fs.createReadStream(source), + }); + const content = []; + for await (const line of reader) { + content.push(line); + } + + return content; +} + +async function writeExtraContentTo(target, extra) { + const output = []; + const reader = readline.createInterface({ + input: fs.createReadStream(target), + }); + + let firstHeadingSeen = false; + for await (const line of reader) { + // Insert extra content just before the first markdown heading. + if (line.match(/^\#+ /)) { + if (!firstHeadingSeen) { + output.push(...extra); + output.push(''); + } + + firstHeadingSeen = true; + } + + output.push(line); + } + + const outputBuffer = Buffer.from(output.join('\n')); + console.log(`Writing extra content to ${target}`); + await fs.writeFile(target, outputBuffer); +} + +(async () => { + try { + await main(); + } catch (err) { + console.log(err); + process.exit(1); + } +})(); diff --git a/entrypoints.json b/entrypoints.json new file mode 100644 index 0000000000..dd17bd46fb --- /dev/null +++ b/entrypoints.json @@ -0,0 +1,71 @@ +{ + "firebase-admin": { + "legacy": true, + "typings": "./lib/default-namespace.d.ts", + "dist": "./lib/index.js" + }, + "firebase-admin/app": { + "typings": "./lib/app/index.d.ts", + "dist": "./lib/app/index.js" + }, + "firebase-admin/app-check": { + "typings": "./lib/app-check/index.d.ts", + "dist": "./lib/app-check/index.js" + }, + "firebase-admin/auth": { + "typings": "./lib/auth/index.d.ts", + "dist": "./lib/auth/index.js" + }, + "firebase-admin/database": { + "typings": "./lib/database/index.d.ts", + "dist": "./lib/database/index.js" + }, + "firebase-admin/extensions": { + "typings": "./lib/extensions/index.d.ts", + "dist": "./lib/extensions/index.js" + }, + "firebase-admin/firestore": { + "typings": "./lib/firestore/index.d.ts", + "dist": "./lib/firestore/index.js" + }, + "firebase-admin/functions": { + "typings": "./lib/functions/index.d.ts", + "dist": "./lib/functions/index.js" + }, + "firebase-admin/installations": { + "typings": "./lib/installations/index.d.ts", + "dist": "./lib/installations/index.js" + }, + "firebase-admin/instance-id": { + "typings": "./lib/instance-id/index.d.ts", + "dist": "./lib/instance-id/index.js" + }, + "firebase-admin/messaging": { + "typings": "./lib/messaging/index.d.ts", + "dist": "./lib/messaging/index.js" + }, + "firebase-admin/machine-learning": { + "typings": "./lib/machine-learning/index.d.ts", + "dist": "./lib/machine-learning/index.js" + }, + "firebase-admin/project-management": { + "typings": "./lib/project-management/index.d.ts", + "dist": "./lib/project-management/index.js" + }, + "firebase-admin/security-rules": { + "typings": "./lib/security-rules/index.d.ts", + "dist": "./lib/security-rules/index.js" + }, + "firebase-admin/storage": { + "typings": "./lib/storage/index.d.ts", + "dist": "./lib/storage/index.js" + }, + "firebase-admin/remote-config": { + "typings": "./lib/remote-config/index.d.ts", + "dist": "./lib/remote-config/index.js" + }, + "firebase-admin/eventarc": { + "typings": "./lib/eventarc/index.d.ts", + "dist": "./lib/eventarc/index.js" + } +} diff --git a/etc/firebase-admin.api.md b/etc/firebase-admin.api.md new file mode 100644 index 0000000000..ceb4ff1ab4 --- /dev/null +++ b/etc/firebase-admin.api.md @@ -0,0 +1,518 @@ +## 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 { FirebaseDatabase } from '@firebase/database-types'; +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 { + // Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point default-namespace.d.ts + export interface App extends App { + // (undocumented) + appCheck(): appCheck.AppCheck; + // (undocumented) + auth(): auth.Auth; + // (undocumented) + database(url?: string): database.Database; + delete(): Promise; + // (undocumented) + firestore(): firestore.Firestore; + // (undocumented) + installations(): installations.Installations; + // @deprecated (undocumented) + instanceId(): instanceId.InstanceId; + // (undocumented) + machineLearning(): machineLearning.MachineLearning; + // (undocumented) + messaging(): messaging.Messaging; + // (undocumented) + projectManagement(): projectManagement.ProjectManagement; + // (undocumented) + remoteConfig(): remoteConfig.RemoteConfig; + // (undocumented) + securityRules(): securityRules.SecurityRules; + // (undocumented) + storage(): storage.Storage; + } +} + +// @public +export function appCheck(app?: App): appCheck.AppCheck; + +// @public (undocumented) +export namespace appCheck { + // Warning: (ae-forgotten-export) The symbol "AppCheck" needs to be exported by the entry point default-namespace.d.ts + export type AppCheck = AppCheck; + // Warning: (ae-forgotten-export) The symbol "AppCheckToken" needs to be exported by the entry point default-namespace.d.ts + export type AppCheckToken = AppCheckToken; + // Warning: (ae-forgotten-export) The symbol "AppCheckTokenOptions" needs to be exported by the entry point default-namespace.d.ts + export type AppCheckTokenOptions = AppCheckTokenOptions; + // Warning: (ae-forgotten-export) The symbol "DecodedAppCheckToken" needs to be exported by the entry point default-namespace.d.ts + export type DecodedAppCheckToken = DecodedAppCheckToken; + // Warning: (ae-forgotten-export) The symbol "VerifyAppCheckTokenOptions" needs to be exported by the entry point default-namespace.d.ts + export type VerifyAppCheckTokenOptions = VerifyAppCheckTokenOptions; + // Warning: (ae-forgotten-export) The symbol "VerifyAppCheckTokenResponse" needs to be exported by the entry point default-namespace.d.ts + export type VerifyAppCheckTokenResponse = VerifyAppCheckTokenResponse; +} + +// @public +export interface AppOptions { + // Warning: (ae-forgotten-export) The symbol "Credential" needs to be exported by the entry point default-namespace.d.ts + 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): auth.Auth; + +// @public (undocumented) +export namespace auth { + // Warning: (ae-forgotten-export) The symbol "ActionCodeSettings" needs to be exported by the entry point default-namespace.d.ts + export type ActionCodeSettings = ActionCodeSettings; + // Warning: (ae-forgotten-export) The symbol "Auth" needs to be exported by the entry point default-namespace.d.ts + export type Auth = Auth; + // Warning: (ae-forgotten-export) The symbol "AuthFactorType" needs to be exported by the entry point default-namespace.d.ts + export type AuthFactorType = AuthFactorType; + // Warning: (ae-forgotten-export) The symbol "AuthProviderConfig" needs to be exported by the entry point default-namespace.d.ts + export type AuthProviderConfig = AuthProviderConfig; + // Warning: (ae-forgotten-export) The symbol "AuthProviderConfigFilter" needs to be exported by the entry point default-namespace.d.ts + export type AuthProviderConfigFilter = AuthProviderConfigFilter; + // Warning: (ae-forgotten-export) The symbol "BaseAuth" needs to be exported by the entry point default-namespace.d.ts + export type BaseAuth = BaseAuth; + // Warning: (ae-forgotten-export) The symbol "CreateMultiFactorInfoRequest" needs to be exported by the entry point default-namespace.d.ts + export type CreateMultiFactorInfoRequest = CreateMultiFactorInfoRequest; + // Warning: (ae-forgotten-export) The symbol "CreatePhoneMultiFactorInfoRequest" needs to be exported by the entry point default-namespace.d.ts + export type CreatePhoneMultiFactorInfoRequest = CreatePhoneMultiFactorInfoRequest; + // Warning: (ae-forgotten-export) The symbol "CreateRequest" needs to be exported by the entry point default-namespace.d.ts + export type CreateRequest = CreateRequest; + // Warning: (ae-forgotten-export) The symbol "CreateTenantRequest" needs to be exported by the entry point default-namespace.d.ts + export type CreateTenantRequest = CreateTenantRequest; + // Warning: (ae-forgotten-export) The symbol "DecodedAuthBlockingToken" needs to be exported by the entry point default-namespace.d.ts + // + // @alpha (undocumented) + export type DecodedAuthBlockingToken = DecodedAuthBlockingToken; + // Warning: (ae-forgotten-export) The symbol "DecodedIdToken" needs to be exported by the entry point default-namespace.d.ts + export type DecodedIdToken = DecodedIdToken; + // Warning: (ae-forgotten-export) The symbol "DeleteUsersResult" needs to be exported by the entry point default-namespace.d.ts + export type DeleteUsersResult = DeleteUsersResult; + // Warning: (ae-forgotten-export) The symbol "EmailIdentifier" needs to be exported by the entry point default-namespace.d.ts + export type EmailIdentifier = EmailIdentifier; + // Warning: (ae-forgotten-export) The symbol "EmailSignInProviderConfig" needs to be exported by the entry point default-namespace.d.ts + export type EmailSignInProviderConfig = EmailSignInProviderConfig; + // Warning: (ae-forgotten-export) The symbol "GetUsersResult" needs to be exported by the entry point default-namespace.d.ts + export type GetUsersResult = GetUsersResult; + // Warning: (ae-forgotten-export) The symbol "HashAlgorithmType" needs to be exported by the entry point default-namespace.d.ts + export type HashAlgorithmType = HashAlgorithmType; + // Warning: (ae-forgotten-export) The symbol "ListProviderConfigResults" needs to be exported by the entry point default-namespace.d.ts + export type ListProviderConfigResults = ListProviderConfigResults; + // Warning: (ae-forgotten-export) The symbol "ListTenantsResult" needs to be exported by the entry point default-namespace.d.ts + export type ListTenantsResult = ListTenantsResult; + // Warning: (ae-forgotten-export) The symbol "ListUsersResult" needs to be exported by the entry point default-namespace.d.ts + export type ListUsersResult = ListUsersResult; + // Warning: (ae-forgotten-export) The symbol "MultiFactorConfig" needs to be exported by the entry point default-namespace.d.ts + export type MultiFactorConfig = MultiFactorConfig; + // Warning: (ae-forgotten-export) The symbol "MultiFactorConfigState" needs to be exported by the entry point default-namespace.d.ts + export type MultiFactorConfigState = MultiFactorConfigState; + // Warning: (ae-forgotten-export) The symbol "MultiFactorCreateSettings" needs to be exported by the entry point default-namespace.d.ts + export type MultiFactorCreateSettings = MultiFactorCreateSettings; + // Warning: (ae-forgotten-export) The symbol "MultiFactorInfo" needs to be exported by the entry point default-namespace.d.ts + export type MultiFactorInfo = MultiFactorInfo; + // Warning: (ae-forgotten-export) The symbol "MultiFactorSettings" needs to be exported by the entry point default-namespace.d.ts + export type MultiFactorSettings = MultiFactorSettings; + // Warning: (ae-forgotten-export) The symbol "MultiFactorUpdateSettings" needs to be exported by the entry point default-namespace.d.ts + export type MultiFactorUpdateSettings = MultiFactorUpdateSettings; + // Warning: (ae-forgotten-export) The symbol "OIDCAuthProviderConfig" needs to be exported by the entry point default-namespace.d.ts + export type OIDCAuthProviderConfig = OIDCAuthProviderConfig; + // Warning: (ae-forgotten-export) The symbol "OIDCUpdateAuthProviderRequest" needs to be exported by the entry point default-namespace.d.ts + export type OIDCUpdateAuthProviderRequest = OIDCUpdateAuthProviderRequest; + // Warning: (ae-forgotten-export) The symbol "PhoneIdentifier" needs to be exported by the entry point default-namespace.d.ts + export type PhoneIdentifier = PhoneIdentifier; + // Warning: (ae-forgotten-export) The symbol "PhoneMultiFactorInfo" needs to be exported by the entry point default-namespace.d.ts + export type PhoneMultiFactorInfo = PhoneMultiFactorInfo; + // Warning: (ae-forgotten-export) The symbol "ProviderIdentifier" needs to be exported by the entry point default-namespace.d.ts + export type ProviderIdentifier = ProviderIdentifier; + // Warning: (ae-forgotten-export) The symbol "SAMLAuthProviderConfig" needs to be exported by the entry point default-namespace.d.ts + export type SAMLAuthProviderConfig = SAMLAuthProviderConfig; + // Warning: (ae-forgotten-export) The symbol "SAMLUpdateAuthProviderRequest" needs to be exported by the entry point default-namespace.d.ts + export type SAMLUpdateAuthProviderRequest = SAMLUpdateAuthProviderRequest; + // Warning: (ae-forgotten-export) The symbol "SessionCookieOptions" needs to be exported by the entry point default-namespace.d.ts + export type SessionCookieOptions = SessionCookieOptions; + // Warning: (ae-forgotten-export) The symbol "Tenant" needs to be exported by the entry point default-namespace.d.ts + export type Tenant = Tenant; + // Warning: (ae-forgotten-export) The symbol "TenantAwareAuth" needs to be exported by the entry point default-namespace.d.ts + export type TenantAwareAuth = TenantAwareAuth; + // Warning: (ae-forgotten-export) The symbol "TenantManager" needs to be exported by the entry point default-namespace.d.ts + export type TenantManager = TenantManager; + // Warning: (ae-forgotten-export) The symbol "UidIdentifier" needs to be exported by the entry point default-namespace.d.ts + export type UidIdentifier = UidIdentifier; + // Warning: (ae-forgotten-export) The symbol "UpdateAuthProviderRequest" needs to be exported by the entry point default-namespace.d.ts + export type UpdateAuthProviderRequest = UpdateAuthProviderRequest; + // Warning: (ae-forgotten-export) The symbol "UpdateMultiFactorInfoRequest" needs to be exported by the entry point default-namespace.d.ts + export type UpdateMultiFactorInfoRequest = UpdateMultiFactorInfoRequest; + // Warning: (ae-forgotten-export) The symbol "UpdatePhoneMultiFactorInfoRequest" needs to be exported by the entry point default-namespace.d.ts + export type UpdatePhoneMultiFactorInfoRequest = UpdatePhoneMultiFactorInfoRequest; + // Warning: (ae-forgotten-export) The symbol "UpdateRequest" needs to be exported by the entry point default-namespace.d.ts + export type UpdateRequest = UpdateRequest; + // Warning: (ae-forgotten-export) The symbol "UpdateTenantRequest" needs to be exported by the entry point default-namespace.d.ts + export type UpdateTenantRequest = UpdateTenantRequest; + // Warning: (ae-forgotten-export) The symbol "UserIdentifier" needs to be exported by the entry point default-namespace.d.ts + export type UserIdentifier = UserIdentifier; + // Warning: (ae-forgotten-export) The symbol "UserImportOptions" needs to be exported by the entry point default-namespace.d.ts + export type UserImportOptions = UserImportOptions; + // Warning: (ae-forgotten-export) The symbol "UserImportRecord" needs to be exported by the entry point default-namespace.d.ts + export type UserImportRecord = UserImportRecord; + // Warning: (ae-forgotten-export) The symbol "UserImportResult" needs to be exported by the entry point default-namespace.d.ts + export type UserImportResult = UserImportResult; + // Warning: (ae-forgotten-export) The symbol "UserInfo" needs to be exported by the entry point default-namespace.d.ts + export type UserInfo = UserInfo; + // Warning: (ae-forgotten-export) The symbol "UserMetadata" needs to be exported by the entry point default-namespace.d.ts + export type UserMetadata = UserMetadata; + // Warning: (ae-forgotten-export) The symbol "UserMetadataRequest" needs to be exported by the entry point default-namespace.d.ts + export type UserMetadataRequest = UserMetadataRequest; + // Warning: (ae-forgotten-export) The symbol "UserProviderRequest" needs to be exported by the entry point default-namespace.d.ts + export type UserProviderRequest = UserProviderRequest; + // Warning: (ae-forgotten-export) The symbol "UserRecord" needs to be exported by the entry point default-namespace.d.ts + export type UserRecord = UserRecord; +} + +// @public (undocumented) +export namespace credential { + export type Credential = Credential; + const // Warning: (ae-forgotten-export) The symbol "applicationDefault" needs to be exported by the entry point default-namespace.d.ts + applicationDefault: typeof applicationDefault; + const // Warning: (ae-forgotten-export) The symbol "cert" needs to be exported by the entry point default-namespace.d.ts + cert: typeof cert; + const // Warning: (ae-forgotten-export) The symbol "refreshToken" needs to be exported by the entry point default-namespace.d.ts + refreshToken: typeof refreshToken; +} + +// @public +export function database(app?: App): database.Database; + +// @public (undocumented) +export namespace database { + // Warning: (ae-forgotten-export) The symbol "Database" needs to be exported by the entry point default-namespace.d.ts + export type Database = Database; + export type DataSnapshot = rtdb.DataSnapshot; + export type EventType = rtdb.EventType; + export type OnDisconnect = rtdb.OnDisconnect; + export type Query = rtdb.Query; + export type Reference = rtdb.Reference; + export type ThenableReference = rtdb.ThenableReference; + const enableLogging: typeof 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): _firestore.Firestore; + +// @public (undocumented) +export namespace firestore { + import v1beta1 = _firestore.v1beta1; + import v1 = _firestore.v1; + import AggregateField = _firestore.AggregateField; + import AggregateFieldType = _firestore.AggregateFieldType; + import AggregateQuery = _firestore.AggregateQuery; + import AggregateQuerySnapshot = _firestore.AggregateQuerySnapshot; + import AggregateSpecData = _firestore.AggregateSpecData; + import AggregateSpec = _firestore.AggregateSpec; + import AggregateType = _firestore.AggregateType; + import BulkWriter = _firestore.BulkWriter; + import BulkWriterOptions = _firestore.BulkWriterOptions; + import BundleBuilder = _firestore.BundleBuilder; + import CollectionGroup = _firestore.CollectionGroup; + import CollectionReference = _firestore.CollectionReference; + import DocumentChange = _firestore.DocumentChange; + 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 Filter = _firestore.Filter; + import Firestore = _firestore.Firestore; + import FirestoreDataConverter = _firestore.FirestoreDataConverter; + import GeoPoint = _firestore.GeoPoint; + import GrpcStatus = _firestore.GrpcStatus; + import OrderByDirection = _firestore.OrderByDirection; + 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 SetOptions = _firestore.SetOptions; + import Timestamp = _firestore.Timestamp; + import Transaction = _firestore.Transaction; + import UpdateData = _firestore.UpdateData; + import WhereFilterOp = _firestore.WhereFilterOp; + import WriteBatch = _firestore.WriteBatch; + import WriteResult = _firestore.WriteResult; + import PartialWithFieldValue = _firestore.PartialWithFieldValue; + import WithFieldValue = _firestore.WithFieldValue; + import Primitive = _firestore.Primitive; + import NestedUpdateFields = _firestore.NestedUpdateFields; + import ChildUpdateFields = _firestore.ChildUpdateFields; + import AddPrefixToKeys = _firestore.AddPrefixToKeys; + import UnionToIntersection = _firestore.UnionToIntersection; + import ReadOnlyTransactionOptions = _firestore.ReadOnlyTransactionOptions; + 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 installations(app?: App): installations.Installations; + +// @public (undocumented) +export namespace installations { + // Warning: (ae-forgotten-export) The symbol "Installations" needs to be exported by the entry point default-namespace.d.ts + export type Installations = Installations; +} + +// @public +export function instanceId(app?: App): instanceId.InstanceId; + +// @public (undocumented) +export namespace instanceId { + // Warning: (ae-forgotten-export) The symbol "InstanceId" needs to be exported by the entry point default-namespace.d.ts + export type InstanceId = InstanceId; +} + +// @public +export function machineLearning(app?: App): machineLearning.MachineLearning; + +// @public (undocumented) +export namespace machineLearning { + // Warning: (ae-forgotten-export) The symbol "GcsTfliteModelOptions" needs to be exported by the entry point default-namespace.d.ts + export type GcsTfliteModelOptions = GcsTfliteModelOptions; + // Warning: (ae-forgotten-export) The symbol "ListModelsOptions" needs to be exported by the entry point default-namespace.d.ts + export type ListModelsOptions = ListModelsOptions; + // Warning: (ae-forgotten-export) The symbol "ListModelsResult" needs to be exported by the entry point default-namespace.d.ts + export type ListModelsResult = ListModelsResult; + // Warning: (ae-forgotten-export) The symbol "MachineLearning" needs to be exported by the entry point default-namespace.d.ts + export type MachineLearning = MachineLearning; + // Warning: (ae-forgotten-export) The symbol "Model" needs to be exported by the entry point default-namespace.d.ts + export type Model = Model; + // Warning: (ae-forgotten-export) The symbol "ModelOptions" needs to be exported by the entry point default-namespace.d.ts + export type ModelOptions = ModelOptions; + // Warning: (ae-forgotten-export) The symbol "ModelOptionsBase" needs to be exported by the entry point default-namespace.d.ts + export type ModelOptionsBase = ModelOptionsBase; + // Warning: (ae-forgotten-export) The symbol "TFLiteModel" needs to be exported by the entry point default-namespace.d.ts + export type TFLiteModel = TFLiteModel; +} + +// @public +export function messaging(app?: App): messaging.Messaging; + +// @public (undocumented) +export namespace messaging { + // Warning: (ae-forgotten-export) The symbol "AndroidConfig" needs to be exported by the entry point default-namespace.d.ts + export type AndroidConfig = AndroidConfig; + // Warning: (ae-forgotten-export) The symbol "AndroidFcmOptions" needs to be exported by the entry point default-namespace.d.ts + export type AndroidFcmOptions = AndroidFcmOptions; + // Warning: (ae-forgotten-export) The symbol "AndroidNotification" needs to be exported by the entry point default-namespace.d.ts + export type AndroidNotification = AndroidNotification; + // Warning: (ae-forgotten-export) The symbol "ApnsConfig" needs to be exported by the entry point default-namespace.d.ts + export type ApnsConfig = ApnsConfig; + // Warning: (ae-forgotten-export) The symbol "ApnsFcmOptions" needs to be exported by the entry point default-namespace.d.ts + export type ApnsFcmOptions = ApnsFcmOptions; + // Warning: (ae-forgotten-export) The symbol "ApnsPayload" needs to be exported by the entry point default-namespace.d.ts + export type ApnsPayload = ApnsPayload; + // Warning: (ae-forgotten-export) The symbol "Aps" needs to be exported by the entry point default-namespace.d.ts + export type Aps = Aps; + // Warning: (ae-forgotten-export) The symbol "ApsAlert" needs to be exported by the entry point default-namespace.d.ts + export type ApsAlert = ApsAlert; + // Warning: (ae-forgotten-export) The symbol "BatchResponse" needs to be exported by the entry point default-namespace.d.ts + export type BatchResponse = BatchResponse; + // Warning: (ae-forgotten-export) The symbol "ConditionMessage" needs to be exported by the entry point default-namespace.d.ts + export type ConditionMessage = ConditionMessage; + // Warning: (ae-forgotten-export) The symbol "CriticalSound" needs to be exported by the entry point default-namespace.d.ts + export type CriticalSound = CriticalSound; + // Warning: (ae-forgotten-export) The symbol "DataMessagePayload" needs to be exported by the entry point default-namespace.d.ts + export type DataMessagePayload = DataMessagePayload; + // Warning: (ae-forgotten-export) The symbol "FcmOptions" needs to be exported by the entry point default-namespace.d.ts + export type FcmOptions = FcmOptions; + // Warning: (ae-forgotten-export) The symbol "LightSettings" needs to be exported by the entry point default-namespace.d.ts + export type LightSettings = LightSettings; + // Warning: (ae-forgotten-export) The symbol "Message" needs to be exported by the entry point default-namespace.d.ts + export type Message = Message; + // Warning: (ae-forgotten-export) The symbol "Messaging" needs to be exported by the entry point default-namespace.d.ts + export type Messaging = Messaging; + // Warning: (ae-forgotten-export) The symbol "MessagingConditionResponse" needs to be exported by the entry point default-namespace.d.ts + export type MessagingConditionResponse = MessagingConditionResponse; + // Warning: (ae-forgotten-export) The symbol "MessagingDeviceGroupResponse" needs to be exported by the entry point default-namespace.d.ts + export type MessagingDeviceGroupResponse = MessagingDeviceGroupResponse; + // Warning: (ae-forgotten-export) The symbol "MessagingDeviceResult" needs to be exported by the entry point default-namespace.d.ts + export type MessagingDeviceResult = MessagingDeviceResult; + // Warning: (ae-forgotten-export) The symbol "MessagingDevicesResponse" needs to be exported by the entry point default-namespace.d.ts + export type MessagingDevicesResponse = MessagingDevicesResponse; + // Warning: (ae-forgotten-export) The symbol "MessagingOptions" needs to be exported by the entry point default-namespace.d.ts + export type MessagingOptions = MessagingOptions; + // Warning: (ae-forgotten-export) The symbol "MessagingPayload" needs to be exported by the entry point default-namespace.d.ts + export type MessagingPayload = MessagingPayload; + // Warning: (ae-forgotten-export) The symbol "MessagingTopicManagementResponse" needs to be exported by the entry point default-namespace.d.ts + export type MessagingTopicManagementResponse = MessagingTopicManagementResponse; + // Warning: (ae-forgotten-export) The symbol "MessagingTopicResponse" needs to be exported by the entry point default-namespace.d.ts + export type MessagingTopicResponse = MessagingTopicResponse; + // Warning: (ae-forgotten-export) The symbol "MulticastMessage" needs to be exported by the entry point default-namespace.d.ts + export type MulticastMessage = MulticastMessage; + // Warning: (ae-forgotten-export) The symbol "Notification" needs to be exported by the entry point default-namespace.d.ts + export type Notification = Notification; + // Warning: (ae-forgotten-export) The symbol "NotificationMessagePayload" needs to be exported by the entry point default-namespace.d.ts + export type NotificationMessagePayload = NotificationMessagePayload; + // Warning: (ae-forgotten-export) The symbol "SendResponse" needs to be exported by the entry point default-namespace.d.ts + export type SendResponse = SendResponse; + // Warning: (ae-forgotten-export) The symbol "TokenMessage" needs to be exported by the entry point default-namespace.d.ts + export type TokenMessage = TokenMessage; + // Warning: (ae-forgotten-export) The symbol "TopicMessage" needs to be exported by the entry point default-namespace.d.ts + export type TopicMessage = TopicMessage; + // Warning: (ae-forgotten-export) The symbol "WebpushConfig" needs to be exported by the entry point default-namespace.d.ts + export type WebpushConfig = WebpushConfig; + // Warning: (ae-forgotten-export) The symbol "WebpushFcmOptions" needs to be exported by the entry point default-namespace.d.ts + export type WebpushFcmOptions = WebpushFcmOptions; + // Warning: (ae-forgotten-export) The symbol "WebpushNotification" needs to be exported by the entry point default-namespace.d.ts + export type WebpushNotification = WebpushNotification; +} + +// @public +export function projectManagement(app?: App): projectManagement.ProjectManagement; + +// @public (undocumented) +export namespace projectManagement { + // Warning: (ae-forgotten-export) The symbol "AndroidApp" needs to be exported by the entry point default-namespace.d.ts + export type AndroidApp = AndroidApp; + // Warning: (ae-forgotten-export) The symbol "AndroidAppMetadata" needs to be exported by the entry point default-namespace.d.ts + export type AndroidAppMetadata = AndroidAppMetadata; + // Warning: (ae-forgotten-export) The symbol "AppMetadata" needs to be exported by the entry point default-namespace.d.ts + export type AppMetadata = AppMetadata; + // Warning: (ae-forgotten-export) The symbol "AppPlatform" needs to be exported by the entry point default-namespace.d.ts + export type AppPlatform = AppPlatform; + // Warning: (ae-forgotten-export) The symbol "IosApp" needs to be exported by the entry point default-namespace.d.ts + export type IosApp = IosApp; + // Warning: (ae-forgotten-export) The symbol "IosAppMetadata" needs to be exported by the entry point default-namespace.d.ts + export type IosAppMetadata = IosAppMetadata; + // Warning: (ae-forgotten-export) The symbol "ProjectManagement" needs to be exported by the entry point default-namespace.d.ts + export type ProjectManagement = ProjectManagement; + // Warning: (ae-forgotten-export) The symbol "ShaCertificate" needs to be exported by the entry point default-namespace.d.ts + export type ShaCertificate = ShaCertificate; +} + +// @public +export function remoteConfig(app?: App): remoteConfig.RemoteConfig; + +// @public (undocumented) +export namespace remoteConfig { + // Warning: (ae-forgotten-export) The symbol "ExplicitParameterValue" needs to be exported by the entry point default-namespace.d.ts + export type ExplicitParameterValue = ExplicitParameterValue; + // Warning: (ae-forgotten-export) The symbol "InAppDefaultValue" needs to be exported by the entry point default-namespace.d.ts + export type InAppDefaultValue = InAppDefaultValue; + // Warning: (ae-forgotten-export) The symbol "ListVersionsOptions" needs to be exported by the entry point default-namespace.d.ts + export type ListVersionsOptions = ListVersionsOptions; + // Warning: (ae-forgotten-export) The symbol "ListVersionsResult" needs to be exported by the entry point default-namespace.d.ts + export type ListVersionsResult = ListVersionsResult; + // Warning: (ae-forgotten-export) The symbol "ParameterValueType" needs to be exported by the entry point default-namespace.d.ts + export type ParameterValueType = ParameterValueType; + // Warning: (ae-forgotten-export) The symbol "RemoteConfig" needs to be exported by the entry point default-namespace.d.ts + export type RemoteConfig = RemoteConfig; + // Warning: (ae-forgotten-export) The symbol "RemoteConfigCondition" needs to be exported by the entry point default-namespace.d.ts + export type RemoteConfigCondition = RemoteConfigCondition; + // Warning: (ae-forgotten-export) The symbol "RemoteConfigParameter" needs to be exported by the entry point default-namespace.d.ts + export type RemoteConfigParameter = RemoteConfigParameter; + // Warning: (ae-forgotten-export) The symbol "RemoteConfigParameterGroup" needs to be exported by the entry point default-namespace.d.ts + export type RemoteConfigParameterGroup = RemoteConfigParameterGroup; + // Warning: (ae-forgotten-export) The symbol "RemoteConfigParameterValue" needs to be exported by the entry point default-namespace.d.ts + export type RemoteConfigParameterValue = RemoteConfigParameterValue; + // Warning: (ae-forgotten-export) The symbol "RemoteConfigTemplate" needs to be exported by the entry point default-namespace.d.ts + export type RemoteConfigTemplate = RemoteConfigTemplate; + // Warning: (ae-forgotten-export) The symbol "RemoteConfigUser" needs to be exported by the entry point default-namespace.d.ts + export type RemoteConfigUser = RemoteConfigUser; + // Warning: (ae-forgotten-export) The symbol "TagColor" needs to be exported by the entry point default-namespace.d.ts + export type TagColor = TagColor; + // Warning: (ae-forgotten-export) The symbol "Version" needs to be exported by the entry point default-namespace.d.ts + export type Version = Version; +} + +// @public (undocumented) +export const SDK_VERSION: string; + +// @public +export function securityRules(app?: App): securityRules.SecurityRules; + +// @public (undocumented) +export namespace securityRules { + // Warning: (ae-forgotten-export) The symbol "Ruleset" needs to be exported by the entry point default-namespace.d.ts + export type Ruleset = Ruleset; + // Warning: (ae-forgotten-export) The symbol "RulesetMetadata" needs to be exported by the entry point default-namespace.d.ts + export type RulesetMetadata = RulesetMetadata; + // Warning: (ae-forgotten-export) The symbol "RulesetMetadataList" needs to be exported by the entry point default-namespace.d.ts + export type RulesetMetadataList = RulesetMetadataList; + // Warning: (ae-forgotten-export) The symbol "RulesFile" needs to be exported by the entry point default-namespace.d.ts + export type RulesFile = RulesFile; + // Warning: (ae-forgotten-export) The symbol "SecurityRules" needs to be exported by the entry point default-namespace.d.ts + export type SecurityRules = SecurityRules; +} + +// @public (undocumented) +export interface ServiceAccount { + // (undocumented) + clientEmail?: string; + // (undocumented) + privateKey?: string; + // (undocumented) + projectId?: string; +} + +// @public +export function storage(app?: App): storage.Storage; + +// @public (undocumented) +export namespace storage { + // Warning: (ae-forgotten-export) The symbol "Storage" needs to be exported by the entry point default-namespace.d.ts + export type Storage = Storage; +} + +``` diff --git a/etc/firebase-admin.app-check.api.md b/etc/firebase-admin.app-check.api.md new file mode 100644 index 0000000000..7c883d5f38 --- /dev/null +++ b/etc/firebase-admin.app-check.api.md @@ -0,0 +1,59 @@ +## API Report File for "firebase-admin.app-check" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +/// + +import { Agent } from 'http'; + +// @public +export class AppCheck { + // Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly app: App; + createToken(appId: string, options?: AppCheckTokenOptions): Promise; + verifyToken(appCheckToken: string, options?: VerifyAppCheckTokenOptions): Promise; +} + +// @public +export interface AppCheckToken { + token: string; + ttlMillis: number; +} + +// @public +export interface AppCheckTokenOptions { + ttlMillis?: number; +} + +// @public +export interface DecodedAppCheckToken { + // (undocumented) + [key: string]: any; + app_id: string; + aud: string[]; + exp: number; + iat: number; + iss: string; + sub: string; +} + +// @public +export function getAppCheck(app?: App): AppCheck; + +// @public +export interface VerifyAppCheckTokenOptions { + consume?: boolean; +} + +// @public +export interface VerifyAppCheckTokenResponse { + alreadyConsumed?: boolean; + appId: string; + token: DecodedAppCheckToken; +} + +``` diff --git a/etc/firebase-admin.app.api.md b/etc/firebase-admin.app.api.md new file mode 100644 index 0000000000..4546fe7824 --- /dev/null +++ b/etc/firebase-admin.app.api.md @@ -0,0 +1,121 @@ +## API Report File for "firebase-admin.app" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +/// + +import { Agent } from 'http'; + +// @public +export interface App { + name: string; + options: AppOptions; +} + +// @public +export class AppErrorCodes { + // (undocumented) + static APP_DELETED: string; + // (undocumented) + static DUPLICATE_APP: string; + // (undocumented) + static INTERNAL_ERROR: string; + // (undocumented) + static INVALID_APP_NAME: string; + // (undocumented) + static INVALID_APP_OPTIONS: string; + // (undocumented) + static INVALID_ARGUMENT: string; + // (undocumented) + static INVALID_CREDENTIAL: string; + // (undocumented) + static NETWORK_ERROR: string; + // (undocumented) + static NETWORK_TIMEOUT: string; + // (undocumented) + static NO_APP: string; + // (undocumented) + static UNABLE_TO_PARSE_RESPONSE: string; +} + +// @public +export function applicationDefault(httpAgent?: Agent): Credential; + +// @public +export interface AppOptions { + credential?: Credential; + databaseAuthVariableOverride?: object | null; + databaseURL?: string; + httpAgent?: Agent; + projectId?: string; + serviceAccountId?: string; + storageBucket?: string; +} + +// @public +export function cert(serviceAccountPathOrObject: string | ServiceAccount, httpAgent?: Agent): Credential; + +// @public +export interface Credential { + getAccessToken(): Promise; +} + +// @public +export function deleteApp(app: App): Promise; + +// Warning: (ae-forgotten-export) The symbol "PrefixedFirebaseError" needs to be exported by the entry point index.d.ts +// +// @public +export class FirebaseAppError extends PrefixedFirebaseError { +} + +// @public +export interface FirebaseArrayIndexError { + error: FirebaseError; + index: number; +} + +// @public +export interface FirebaseError { + code: string; + message: string; + stack?: string; + toJSON(): object; +} + +// @public (undocumented) +export function getApp(appName?: string): App; + +// @public (undocumented) +export function getApps(): App[]; + +// @public +export interface GoogleOAuthAccessToken { + // (undocumented) + access_token: string; + // (undocumented) + expires_in: number; +} + +// @public (undocumented) +export function initializeApp(options?: AppOptions, appName?: string): App; + +// @public +export function refreshToken(refreshTokenPathOrObject: string | object, httpAgent?: Agent): Credential; + +// @public (undocumented) +export const SDK_VERSION: string; + +// @public (undocumented) +export interface ServiceAccount { + // (undocumented) + clientEmail?: string; + // (undocumented) + privateKey?: string; + // (undocumented) + projectId?: string; +} + +``` diff --git a/etc/firebase-admin.auth.api.md b/etc/firebase-admin.auth.api.md new file mode 100644 index 0000000000..d921458bec --- /dev/null +++ b/etc/firebase-admin.auth.api.md @@ -0,0 +1,1137 @@ +## API Report File for "firebase-admin.auth" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +/// + +import { Agent } from 'http'; + +// @public +export interface ActionCodeSettings { + android?: { + packageName: string; + installApp?: boolean; + minimumVersion?: string; + }; + dynamicLinkDomain?: string; + handleCodeInApp?: boolean; + iOS?: { + bundleId: string; + }; + url: string; +} + +// @public +export interface AllowByDefault { + disallowedRegions: string[]; +} + +// @public +export interface AllowByDefaultWrap { + allowByDefault: AllowByDefault; + // @alpha (undocumented) + allowlistOnly?: never; +} + +// @public +export interface AllowlistOnly { + allowedRegions: string[]; +} + +// @public +export interface AllowlistOnlyWrap { + // @alpha (undocumented) + allowByDefault?: never; + allowlistOnly: AllowlistOnly; +} + +// @public +export class Auth extends BaseAuth { + // Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts + get app(): App; + projectConfigManager(): ProjectConfigManager; + tenantManager(): TenantManager; +} + +// @public +export class AuthClientErrorCode { + // (undocumented) + static AUTH_BLOCKING_TOKEN_EXPIRED: { + code: string; + message: string; + }; + // (undocumented) + static BILLING_NOT_ENABLED: { + code: string; + message: string; + }; + // (undocumented) + static CLAIMS_TOO_LARGE: { + code: string; + message: string; + }; + // (undocumented) + static CONFIGURATION_EXISTS: { + code: string; + message: string; + }; + // (undocumented) + static CONFIGURATION_NOT_FOUND: { + code: string; + message: string; + }; + // (undocumented) + static EMAIL_ALREADY_EXISTS: { + code: string; + message: string; + }; + // (undocumented) + static EMAIL_NOT_FOUND: { + code: string; + message: string; + }; + // (undocumented) + static FORBIDDEN_CLAIM: { + code: string; + message: string; + }; + // (undocumented) + static ID_TOKEN_EXPIRED: { + code: string; + message: string; + }; + // (undocumented) + static ID_TOKEN_REVOKED: { + code: string; + message: string; + }; + // (undocumented) + static INSUFFICIENT_PERMISSION: { + code: string; + message: string; + }; + // (undocumented) + static INTERNAL_ERROR: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_ARGUMENT: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_CLAIMS: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_CONFIG: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_CONTINUE_URI: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_CREATION_TIME: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_CREDENTIAL: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_DISABLED_FIELD: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_DISPLAY_NAME: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_DYNAMIC_LINK_DOMAIN: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_EMAIL: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_EMAIL_VERIFIED: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_ENROLLED_FACTORS: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_ENROLLMENT_TIME: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_HASH_ALGORITHM: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_HASH_BLOCK_SIZE: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_HASH_DERIVED_KEY_LENGTH: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_HASH_KEY: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_HASH_MEMORY_COST: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_HASH_PARALLELIZATION: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_HASH_ROUNDS: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_HASH_SALT_SEPARATOR: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_ID_TOKEN: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_LAST_SIGN_IN_TIME: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_NAME: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_NEW_EMAIL: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_OAUTH_CLIENT_ID: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_OAUTH_RESPONSETYPE: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_PAGE_TOKEN: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_PASSWORD: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_PASSWORD_HASH: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_PASSWORD_SALT: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_PHONE_NUMBER: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_PHOTO_URL: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_PROJECT_ID: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_PROVIDER_DATA: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_PROVIDER_ID: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_PROVIDER_UID: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_RECAPTCHA_ACTION: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_RECAPTCHA_ENFORCEMENT_STATE: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_SESSION_COOKIE_DURATION: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_TENANT_ID: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_TENANT_TYPE: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_TESTING_PHONE_NUMBER: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_TOKENS_VALID_AFTER_TIME: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_UID: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_USER_IMPORT: { + code: string; + message: string; + }; + // (undocumented) + static MAXIMUM_TEST_PHONE_NUMBER_EXCEEDED: { + code: string; + message: string; + }; + // (undocumented) + static MAXIMUM_USER_COUNT_EXCEEDED: { + code: string; + message: string; + }; + // (undocumented) + static MISMATCHING_TENANT_ID: { + code: string; + message: string; + }; + // (undocumented) + static MISSING_ANDROID_PACKAGE_NAME: { + code: string; + message: string; + }; + // (undocumented) + static MISSING_CONFIG: { + code: string; + message: string; + }; + // (undocumented) + static MISSING_CONTINUE_URI: { + code: string; + message: string; + }; + // (undocumented) + static MISSING_DISPLAY_NAME: { + code: string; + message: string; + }; + // (undocumented) + static MISSING_EMAIL: { + code: string; + message: string; + }; + // (undocumented) + static MISSING_HASH_ALGORITHM: { + code: string; + message: string; + }; + // (undocumented) + static MISSING_IOS_BUNDLE_ID: { + code: string; + message: string; + }; + // (undocumented) + static MISSING_ISSUER: { + code: string; + message: string; + }; + // (undocumented) + static MISSING_OAUTH_CLIENT_ID: { + code: string; + message: string; + }; + // (undocumented) + static MISSING_OAUTH_CLIENT_SECRET: { + code: string; + message: string; + }; + // (undocumented) + static MISSING_PROVIDER_ID: { + code: string; + message: string; + }; + // (undocumented) + static MISSING_SAML_RELYING_PARTY_CONFIG: { + code: string; + message: string; + }; + // (undocumented) + static MISSING_UID: { + code: string; + message: string; + }; + // (undocumented) + static NOT_FOUND: { + code: string; + message: string; + }; + // (undocumented) + static OPERATION_NOT_ALLOWED: { + code: string; + message: string; + }; + // (undocumented) + static PHONE_NUMBER_ALREADY_EXISTS: { + code: string; + message: string; + }; + // (undocumented) + static PROJECT_NOT_FOUND: { + code: string; + message: string; + }; + // (undocumented) + static QUOTA_EXCEEDED: { + code: string; + message: string; + }; + // (undocumented) + static RECAPTCHA_NOT_ENABLED: { + code: string; + message: string; + }; + // (undocumented) + static SECOND_FACTOR_LIMIT_EXCEEDED: { + code: string; + message: string; + }; + // (undocumented) + static SECOND_FACTOR_UID_ALREADY_EXISTS: { + code: string; + message: string; + }; + // (undocumented) + static SESSION_COOKIE_EXPIRED: { + code: string; + message: string; + }; + // (undocumented) + static SESSION_COOKIE_REVOKED: { + code: string; + message: string; + }; + // (undocumented) + static TENANT_NOT_FOUND: { + code: string; + message: string; + }; + // (undocumented) + static UID_ALREADY_EXISTS: { + code: string; + message: string; + }; + // (undocumented) + static UNAUTHORIZED_DOMAIN: { + code: string; + message: string; + }; + // (undocumented) + static UNSUPPORTED_FIRST_FACTOR: { + code: string; + message: string; + }; + // (undocumented) + static UNSUPPORTED_SECOND_FACTOR: { + code: string; + message: string; + }; + // (undocumented) + static UNSUPPORTED_TENANT_OPERATION: { + code: string; + message: string; + }; + // (undocumented) + static UNVERIFIED_EMAIL: { + code: string; + message: string; + }; + // (undocumented) + static USER_DISABLED: { + code: string; + message: string; + }; + // (undocumented) + static USER_NOT_DISABLED: { + code: string; + message: string; + }; + // (undocumented) + static USER_NOT_FOUND: { + code: string; + message: string; + }; +} + +// @public +export type AuthFactorType = 'phone'; + +// @public +export type AuthProviderConfig = SAMLAuthProviderConfig | OIDCAuthProviderConfig; + +// @public +export interface AuthProviderConfigFilter { + maxResults?: number; + pageToken?: string; + type: 'saml' | 'oidc'; +} + +// @public +export abstract class 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; + generateVerifyAndChangeEmailLink(email: string, newEmail: string, actionCodeSettings?: ActionCodeSettings): Promise; + getProviderConfig(providerId: string): Promise; + 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; + 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; + // @alpha (undocumented) + _verifyAuthBlockingToken(token: string, audience?: string): Promise; + verifyIdToken(idToken: string, checkRevoked?: boolean): Promise; + verifySessionCookie(sessionCookie: string, checkRevoked?: boolean): Promise; +} + +// @public +export interface BaseAuthProviderConfig { + displayName?: string; + enabled: boolean; + providerId: string; +} + +// @public +export interface BaseCreateMultiFactorInfoRequest { + displayName?: string; + factorId: string; +} + +// @public +export interface BaseUpdateMultiFactorInfoRequest { + displayName?: string; + enrollmentTime?: string; + factorId: string; + uid?: string; +} + +// @public +export type CreateMultiFactorInfoRequest = CreatePhoneMultiFactorInfoRequest; + +// @public +export interface CreatePhoneMultiFactorInfoRequest extends BaseCreateMultiFactorInfoRequest { + phoneNumber: string; +} + +// @public +export interface CreateRequest extends UpdateRequest { + multiFactor?: MultiFactorCreateSettings; + uid?: string; +} + +// @public +export type CreateTenantRequest = UpdateTenantRequest; + +// @public +export interface CustomStrengthOptionsConfig { + maxLength?: number; + minLength?: number; + requireLowercase?: boolean; + requireNonAlphanumeric?: boolean; + requireNumeric?: boolean; + requireUppercase?: boolean; +} + +// @alpha (undocumented) +export interface DecodedAuthBlockingToken { + // (undocumented) + [key: string]: any; + // (undocumented) + aud: string; + // (undocumented) + event_id: string; + // (undocumented) + event_type: string; + // (undocumented) + exp: number; + // (undocumented) + iat: number; + // (undocumented) + ip_address: string; + // (undocumented) + iss: string; + // (undocumented) + locale?: string; + // (undocumented) + oauth_access_token?: string; + // (undocumented) + oauth_expires_in?: number; + // (undocumented) + oauth_id_token?: string; + // (undocumented) + oauth_refresh_token?: string; + // (undocumented) + oauth_token_secret?: string; + // (undocumented) + raw_user_info?: string; + // (undocumented) + sign_in_attributes?: { + [key: string]: any; + }; + // (undocumented) + sign_in_method?: string; + // (undocumented) + sub: string; + // (undocumented) + tenant_id?: string; + // (undocumented) + user_agent?: string; + // Warning: (ae-forgotten-export) The symbol "DecodedAuthBlockingUserRecord" needs to be exported by the entry point index.d.ts + // + // (undocumented) + user_record?: DecodedAuthBlockingUserRecord; +} + +// @public +export interface DecodedIdToken { + [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; +} + +// @public +export interface DeleteUsersResult { + // Warning: (ae-forgotten-export) The symbol "FirebaseArrayIndexError" needs to be exported by the entry point index.d.ts + errors: FirebaseArrayIndexError[]; + failureCount: number; + successCount: number; +} + +// @public +export interface EmailIdentifier { + // (undocumented) + email: string; +} + +// @public +export interface EmailPrivacyConfig { + enableImprovedEmailPrivacy?: boolean; +} + +// @public +export interface EmailSignInProviderConfig { + enabled: boolean; + passwordRequired?: boolean; +} + +// Warning: (ae-forgotten-export) The symbol "PrefixedFirebaseError" needs to be exported by the entry point index.d.ts +// +// @public +export class FirebaseAuthError extends PrefixedFirebaseError { +} + +// @public +export function getAuth(app?: App): Auth; + +// @public +export interface GetUsersResult { + notFound: UserIdentifier[]; + users: UserRecord[]; +} + +// @public (undocumented) +export type HashAlgorithmType = 'SCRYPT' | 'STANDARD_SCRYPT' | 'HMAC_SHA512' | 'HMAC_SHA256' | 'HMAC_SHA1' | 'HMAC_MD5' | 'MD5' | 'PBKDF_SHA1' | 'BCRYPT' | 'PBKDF2_SHA256' | 'SHA512' | 'SHA256' | 'SHA1'; + +// @public +export interface ListProviderConfigResults { + pageToken?: string; + providerConfigs: AuthProviderConfig[]; +} + +// @public +export interface ListTenantsResult { + pageToken?: string; + tenants: Tenant[]; +} + +// @public +export interface ListUsersResult { + pageToken?: string; + users: UserRecord[]; +} + +// @public +export interface MultiFactorConfig { + factorIds?: AuthFactorType[]; + providerConfigs?: MultiFactorProviderConfig[]; + state: MultiFactorConfigState; +} + +// @public +export type MultiFactorConfigState = 'ENABLED' | 'DISABLED'; + +// @public +export interface MultiFactorCreateSettings { + enrolledFactors: CreateMultiFactorInfoRequest[]; +} + +// @public +export abstract class MultiFactorInfo { + readonly displayName?: string; + readonly enrollmentTime?: string; + readonly factorId: string; + toJSON(): object; + readonly uid: string; +} + +// @public +export interface MultiFactorProviderConfig { + state: MultiFactorConfigState; + totpProviderConfig?: TotpMultiFactorProviderConfig; +} + +// @public +export class MultiFactorSettings { + enrolledFactors: MultiFactorInfo[]; + toJSON(): object; +} + +// @public +export interface MultiFactorUpdateSettings { + enrolledFactors: UpdateMultiFactorInfoRequest[] | null; +} + +// @public +export interface OAuthResponseType { + code?: boolean; + idToken?: boolean; +} + +// @public +export interface OIDCAuthProviderConfig extends BaseAuthProviderConfig { + clientId: string; + clientSecret?: string; + issuer: string; + responseType?: OAuthResponseType; +} + +// @public +export interface OIDCUpdateAuthProviderRequest { + clientId?: string; + clientSecret?: string; + displayName?: string; + enabled?: boolean; + issuer?: string; + responseType?: OAuthResponseType; +} + +// @public +export interface PasswordPolicyConfig { + constraints?: CustomStrengthOptionsConfig; + enforcementState?: PasswordPolicyEnforcementState; + forceUpgradeOnSignin?: boolean; +} + +// @public +export type PasswordPolicyEnforcementState = 'ENFORCE' | 'OFF'; + +// @public +export interface PhoneIdentifier { + // (undocumented) + phoneNumber: string; +} + +// @public +export class PhoneMultiFactorInfo extends MultiFactorInfo { + readonly phoneNumber: string; + toJSON(): object; +} + +// @public +export class ProjectConfig { + readonly emailPrivacyConfig?: EmailPrivacyConfig; + get multiFactorConfig(): MultiFactorConfig | undefined; + readonly passwordPolicyConfig?: PasswordPolicyConfig; + get recaptchaConfig(): RecaptchaConfig | undefined; + readonly smsRegionConfig?: SmsRegionConfig; + toJSON(): object; +} + +// @public +export class ProjectConfigManager { + getProjectConfig(): Promise; + updateProjectConfig(projectConfigOptions: UpdateProjectConfigRequest): Promise; +} + +// @public +export interface ProviderIdentifier { + // (undocumented) + providerId: string; + // (undocumented) + providerUid: string; +} + +// @public +export type RecaptchaAction = 'BLOCK'; + +// @public +export interface RecaptchaConfig { + emailPasswordEnforcementState?: RecaptchaProviderEnforcementState; + managedRules?: RecaptchaManagedRule[]; + recaptchaKeys?: RecaptchaKey[]; + useAccountDefender?: boolean; +} + +// @public +export interface RecaptchaKey { + key: string; + type?: RecaptchaKeyClientType; +} + +// @public +export type RecaptchaKeyClientType = 'WEB' | 'IOS' | 'ANDROID'; + +// @public +export interface RecaptchaManagedRule { + action?: RecaptchaAction; + endScore: number; +} + +// @public +export type RecaptchaProviderEnforcementState = 'OFF' | 'AUDIT' | 'ENFORCE'; + +// @public +export interface SAMLAuthProviderConfig extends BaseAuthProviderConfig { + callbackURL?: string; + idpEntityId: string; + rpEntityId: string; + ssoURL: string; + x509Certificates: string[]; +} + +// @public +export interface SAMLUpdateAuthProviderRequest { + callbackURL?: string; + displayName?: string; + enabled?: boolean; + idpEntityId?: string; + rpEntityId?: string; + ssoURL?: string; + x509Certificates?: string[]; +} + +// @public +export interface SessionCookieOptions { + expiresIn: number; +} + +// @public +export type SmsRegionConfig = AllowByDefaultWrap | AllowlistOnlyWrap; + +// @public +export class Tenant { + // (undocumented) + readonly anonymousSignInEnabled: boolean; + readonly displayName?: string; + readonly emailPrivacyConfig?: EmailPrivacyConfig; + get emailSignInConfig(): EmailSignInProviderConfig | undefined; + get multiFactorConfig(): MultiFactorConfig | undefined; + readonly passwordPolicyConfig?: PasswordPolicyConfig; + get recaptchaConfig(): RecaptchaConfig | undefined; + readonly smsRegionConfig?: SmsRegionConfig; + readonly tenantId: string; + readonly testPhoneNumbers?: { + [phoneNumber: string]: string; + }; + toJSON(): object; +} + +// @public +export class TenantAwareAuth extends BaseAuth { + createSessionCookie(idToken: string, sessionCookieOptions: SessionCookieOptions): Promise; + readonly tenantId: string; + verifyIdToken(idToken: string, checkRevoked?: boolean): Promise; + verifySessionCookie(sessionCookie: string, checkRevoked?: boolean): Promise; +} + +// @public +export class TenantManager { + 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; +} + +// @public +export interface TotpMultiFactorProviderConfig { + adjacentIntervals?: number; +} + +// @public +export interface UidIdentifier { + // (undocumented) + uid: string; +} + +// @public (undocumented) +export type UpdateAuthProviderRequest = SAMLUpdateAuthProviderRequest | OIDCUpdateAuthProviderRequest; + +// @public +export type UpdateMultiFactorInfoRequest = UpdatePhoneMultiFactorInfoRequest; + +// @public +export interface UpdatePhoneMultiFactorInfoRequest extends BaseUpdateMultiFactorInfoRequest { + phoneNumber: string; +} + +// @public +export interface UpdateProjectConfigRequest { + emailPrivacyConfig?: EmailPrivacyConfig; + multiFactorConfig?: MultiFactorConfig; + passwordPolicyConfig?: PasswordPolicyConfig; + recaptchaConfig?: RecaptchaConfig; + smsRegionConfig?: SmsRegionConfig; +} + +// @public +export interface UpdateRequest { + disabled?: boolean; + displayName?: string | null; + email?: string; + emailVerified?: boolean; + multiFactor?: MultiFactorUpdateSettings; + password?: string; + phoneNumber?: string | null; + photoURL?: string | null; + providersToUnlink?: string[]; + providerToLink?: UserProvider; +} + +// @public +export interface UpdateTenantRequest { + anonymousSignInEnabled?: boolean; + displayName?: string; + emailPrivacyConfig?: EmailPrivacyConfig; + emailSignInConfig?: EmailSignInProviderConfig; + multiFactorConfig?: MultiFactorConfig; + passwordPolicyConfig?: PasswordPolicyConfig; + recaptchaConfig?: RecaptchaConfig; + smsRegionConfig?: SmsRegionConfig; + testPhoneNumbers?: { + [phoneNumber: string]: string; + } | null; +} + +// @public +export type UserIdentifier = UidIdentifier | EmailIdentifier | PhoneIdentifier | ProviderIdentifier; + +// @public +export interface UserImportOptions { + hash: { + algorithm: HashAlgorithmType; + key?: Buffer; + saltSeparator?: Buffer; + rounds?: number; + memoryCost?: number; + parallelization?: number; + blockSize?: number; + derivedKeyLength?: number; + }; +} + +// @public +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; +} + +// @public +export interface UserImportResult { + errors: FirebaseArrayIndexError[]; + failureCount: number; + successCount: number; +} + +// @public +export class UserInfo { + readonly displayName: string; + readonly email: string; + readonly phoneNumber: string; + readonly photoURL: string; + readonly providerId: string; + toJSON(): object; + readonly uid: string; +} + +// @public +export class UserMetadata { + readonly creationTime: string; + readonly lastRefreshTime?: string | null; + readonly lastSignInTime: string; + toJSON(): object; +} + +// @public +export interface UserMetadataRequest { + creationTime?: string; + lastSignInTime?: string; +} + +// @public +export interface UserProvider { + displayName?: string; + email?: string; + phoneNumber?: string; + photoURL?: string; + providerId?: string; + uid?: string; +} + +// @public +export interface UserProviderRequest { + displayName?: string; + email?: string; + phoneNumber?: string; + photoURL?: string; + providerId: string; + uid: string; +} + +// @public +export class UserRecord { + readonly customClaims?: { + [key: string]: any; + }; + readonly disabled: boolean; + readonly displayName?: string; + readonly email?: string; + readonly emailVerified: boolean; + readonly metadata: UserMetadata; + readonly multiFactor?: MultiFactorSettings; + readonly passwordHash?: string; + readonly passwordSalt?: string; + readonly phoneNumber?: string; + readonly photoURL?: string; + readonly providerData: UserInfo[]; + readonly tenantId?: string | null; + toJSON(): object; + readonly tokensValidAfterTime?: string; + readonly uid: string; +} + +``` diff --git a/etc/firebase-admin.database.api.md b/etc/firebase-admin.database.api.md new file mode 100644 index 0000000000..1778f75d51 --- /dev/null +++ b/etc/firebase-admin.database.api.md @@ -0,0 +1,58 @@ +## API Report File for "firebase-admin.database" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +/// + +import { Agent } from 'http'; +import { DataSnapshot } from '@firebase/database-types'; +import { EventType } from '@firebase/database-types'; +import { FirebaseDatabase } from '@firebase/database-types'; +import { OnDisconnect } from '@firebase/database-types'; +import { Query } from '@firebase/database-types'; +import { Reference } from '@firebase/database-types'; +import * as rtdb from '@firebase/database-types'; +import { ThenableReference } from '@firebase/database-types'; + +// @public +export interface Database extends FirebaseDatabase { + getRules(): Promise; + getRulesJSON(): Promise; + setRules(source: string | Buffer | object): Promise; +} + +export { DataSnapshot } + +// @public +export const enableLogging: typeof rtdb.enableLogging; + +export { EventType } + +// Warning: (ae-forgotten-export) The symbol "FirebaseError" needs to be exported by the entry point index.d.ts +// +// @public +export class FirebaseDatabaseError extends FirebaseError { +} + +// Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts +// +// @public +export function getDatabase(app?: App): Database; + +// @public +export function getDatabaseWithUrl(url: string, app?: App): Database; + +export { OnDisconnect } + +export { Query } + +export { Reference } + +// @public +export const ServerValue: rtdb.ServerValue; + +export { ThenableReference } + +``` diff --git a/etc/firebase-admin.eventarc.api.md b/etc/firebase-admin.eventarc.api.md new file mode 100644 index 0000000000..a070f8b391 --- /dev/null +++ b/etc/firebase-admin.eventarc.api.md @@ -0,0 +1,51 @@ +## API Report File for "firebase-admin.eventarc" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +/// + +import { Agent } from 'http'; + +// @public +export class Channel { + readonly allowedEventTypes?: string[]; + get eventarc(): Eventarc; + get name(): string; + publish(events: CloudEvent | CloudEvent[]): Promise; +} + +// @public +export interface ChannelOptions { + allowedEventTypes?: string[] | string | undefined; +} + +// @public +export interface CloudEvent { + [key: string]: any; + data?: object | string; + datacontenttype?: string; + id?: string; + source?: string; + specversion?: CloudEventVersion; + subject?: string; + time?: string; + type: string; +} + +// @public +export type CloudEventVersion = '1.0'; + +// @public +export class Eventarc { + // Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts + get app(): App; + channel(name: string, options?: ChannelOptions): Channel; + channel(options?: ChannelOptions): Channel; +} + +// @public +export function getEventarc(app?: App): Eventarc; + +``` diff --git a/etc/firebase-admin.extensions.api.md b/etc/firebase-admin.extensions.api.md new file mode 100644 index 0000000000..127754c9c9 --- /dev/null +++ b/etc/firebase-admin.extensions.api.md @@ -0,0 +1,32 @@ +## API Report File for "firebase-admin.extensions" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +/// + +import { Agent } from 'http'; + +// @public +export class Extensions { + // Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly app: App; + runtime(): Runtime; +} + +// @public +export function getExtensions(app?: App): Extensions; + +// @public +export class Runtime { + setFatalError(errorMessage: string): Promise; + setProcessingState(state: SettableProcessingState, detailMessage: string): Promise; +} + +// @public +export type SettableProcessingState = 'NONE' | 'PROCESSING_COMPLETE' | 'PROCESSING_WARNING' | 'PROCESSING_FAILED'; + +``` diff --git a/etc/firebase-admin.firestore.api.md b/etc/firebase-admin.firestore.api.md new file mode 100644 index 0000000000..8f29baeab3 --- /dev/null +++ b/etc/firebase-admin.firestore.api.md @@ -0,0 +1,192 @@ +## API Report File for "firebase-admin.firestore" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +/// + +import { AddPrefixToKeys } from '@google-cloud/firestore'; +import { Agent } from 'http'; +import { AggregateField } from '@google-cloud/firestore'; +import { AggregateFieldType } from '@google-cloud/firestore'; +import { AggregateQuery } from '@google-cloud/firestore'; +import { AggregateQuerySnapshot } from '@google-cloud/firestore'; +import { AggregateSpec } from '@google-cloud/firestore'; +import { AggregateSpecData } from '@google-cloud/firestore'; +import { AggregateType } from '@google-cloud/firestore'; +import { BulkWriter } from '@google-cloud/firestore'; +import { BulkWriterOptions } from '@google-cloud/firestore'; +import { BundleBuilder } from '@google-cloud/firestore'; +import { ChildUpdateFields } from '@google-cloud/firestore'; +import { CollectionGroup } from '@google-cloud/firestore'; +import { CollectionReference } from '@google-cloud/firestore'; +import { DocumentChange } from '@google-cloud/firestore'; +import { DocumentChangeType } from '@google-cloud/firestore'; +import { DocumentData } from '@google-cloud/firestore'; +import { DocumentReference } from '@google-cloud/firestore'; +import { DocumentSnapshot } from '@google-cloud/firestore'; +import { FieldPath } from '@google-cloud/firestore'; +import { FieldValue } from '@google-cloud/firestore'; +import { Filter } from '@google-cloud/firestore'; +import { Firestore } from '@google-cloud/firestore'; +import { FirestoreDataConverter } from '@google-cloud/firestore'; +import { GeoPoint } from '@google-cloud/firestore'; +import { GrpcStatus } from '@google-cloud/firestore'; +import { NestedUpdateFields } from '@google-cloud/firestore'; +import { OrderByDirection } from '@google-cloud/firestore'; +import { PartialWithFieldValue } from '@google-cloud/firestore'; +import { Precondition } from '@google-cloud/firestore'; +import { Primitive } from '@google-cloud/firestore'; +import { Query } from '@google-cloud/firestore'; +import { QueryDocumentSnapshot } from '@google-cloud/firestore'; +import { QueryPartition } from '@google-cloud/firestore'; +import { QuerySnapshot } from '@google-cloud/firestore'; +import { ReadOnlyTransactionOptions } from '@google-cloud/firestore'; +import { ReadOptions } from '@google-cloud/firestore'; +import { ReadWriteTransactionOptions } from '@google-cloud/firestore'; +import { setLogFunction } from '@google-cloud/firestore'; +import { SetOptions } from '@google-cloud/firestore'; +import { Settings } from '@google-cloud/firestore'; +import { Timestamp } from '@google-cloud/firestore'; +import { Transaction } from '@google-cloud/firestore'; +import { UnionToIntersection } from '@google-cloud/firestore'; +import { UpdateData } from '@google-cloud/firestore'; +import { v1 } from '@google-cloud/firestore'; +import { WhereFilterOp } from '@google-cloud/firestore'; +import { WithFieldValue } from '@google-cloud/firestore'; +import { WriteBatch } from '@google-cloud/firestore'; +import { WriteResult } from '@google-cloud/firestore'; + +export { AddPrefixToKeys } + +export { AggregateField } + +export { AggregateFieldType } + +export { AggregateQuery } + +export { AggregateQuerySnapshot } + +export { AggregateSpec } + +export { AggregateSpecData } + +export { AggregateType } + +export { BulkWriter } + +export { BulkWriterOptions } + +export { BundleBuilder } + +export { ChildUpdateFields } + +export { CollectionGroup } + +export { CollectionReference } + +export { DocumentChange } + +export { DocumentChangeType } + +export { DocumentData } + +export { DocumentReference } + +export { DocumentSnapshot } + +export { FieldPath } + +export { FieldValue } + +export { Filter } + +// Warning: (ae-forgotten-export) The symbol "FirebaseError" needs to be exported by the entry point index.d.ts +// +// @public +export class FirebaseFirestoreError extends FirebaseError { +} + +export { Firestore } + +export { FirestoreDataConverter } + +// @public +export interface FirestoreSettings { + preferRest?: boolean; +} + +export { GeoPoint } + +// @public +export function getFirestore(): Firestore; + +// Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts +// +// @public +export function getFirestore(app: App): Firestore; + +// @beta +export function getFirestore(databaseId: string): Firestore; + +// @beta +export function getFirestore(app: App, databaseId: string): Firestore; + +export { GrpcStatus } + +// @public +export function initializeFirestore(app: App, settings?: FirestoreSettings): Firestore; + +// @beta +export function initializeFirestore(app: App, settings: FirestoreSettings, databaseId: string): Firestore; + +export { NestedUpdateFields } + +export { OrderByDirection } + +export { PartialWithFieldValue } + +export { Precondition } + +export { Primitive } + +export { Query } + +export { QueryDocumentSnapshot } + +export { QueryPartition } + +export { QuerySnapshot } + +export { ReadOnlyTransactionOptions } + +export { ReadOptions } + +export { ReadWriteTransactionOptions } + +export { setLogFunction } + +export { SetOptions } + +export { Settings } + +export { Timestamp } + +export { Transaction } + +export { UnionToIntersection } + +export { UpdateData } + +export { v1 } + +export { WhereFilterOp } + +export { WithFieldValue } + +export { WriteBatch } + +export { WriteResult } + +``` diff --git a/etc/firebase-admin.functions.api.md b/etc/firebase-admin.functions.api.md new file mode 100644 index 0000000000..87f8656b4a --- /dev/null +++ b/etc/firebase-admin.functions.api.md @@ -0,0 +1,59 @@ +## API Report File for "firebase-admin.functions" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +/// + +import { Agent } from 'http'; + +// @public +export interface AbsoluteDelivery { + // @alpha (undocumented) + scheduleDelaySeconds?: never; + scheduleTime?: Date; +} + +// @public +export interface DelayDelivery { + scheduleDelaySeconds?: number; + // @alpha (undocumented) + scheduleTime?: never; +} + +// @public +export type DeliverySchedule = DelayDelivery | AbsoluteDelivery; + +// @public +export class Functions { + // Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly app: App; + taskQueue>(functionName: string, extensionId?: string): TaskQueue; +} + +// @public +export function getFunctions(app?: App): Functions; + +// @public +export type TaskOptions = DeliverySchedule & TaskOptionsExperimental & { + dispatchDeadlineSeconds?: number; + id?: string; + headers?: Record; +}; + +// @public +export interface TaskOptionsExperimental { + // @beta + uri?: string; +} + +// @public +export class TaskQueue> { + delete(id: string): Promise; + enqueue(data: Args, opts?: TaskOptions): Promise; +} + +``` diff --git a/etc/firebase-admin.installations.api.md b/etc/firebase-admin.installations.api.md new file mode 100644 index 0000000000..56496ef135 --- /dev/null +++ b/etc/firebase-admin.installations.api.md @@ -0,0 +1,52 @@ +## API Report File for "firebase-admin.installations" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +/// + +import { Agent } from 'http'; + +// Warning: (ae-forgotten-export) The symbol "FirebaseError" needs to be exported by the entry point index.d.ts +// +// @public +export class FirebaseInstallationsError extends FirebaseError { +} + +// Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts +// +// @public +export function getInstallations(app?: App): Installations; + +// @public +export class Installations { + get app(): App; + deleteInstallation(fid: string): Promise; +} + +// @public (undocumented) +export class InstallationsClientErrorCode { + // (undocumented) + static API_ERROR: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_ARGUMENT: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_INSTALLATION_ID: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_PROJECT_ID: { + code: string; + message: string; + }; +} + +``` diff --git a/etc/firebase-admin.instance-id.api.md b/etc/firebase-admin.instance-id.api.md new file mode 100644 index 0000000000..9c28852a2c --- /dev/null +++ b/etc/firebase-admin.instance-id.api.md @@ -0,0 +1,39 @@ +## API Report File for "firebase-admin.instance-id" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +/// + +import { Agent } from 'http'; + +// Warning: (ae-forgotten-export) The symbol "FirebaseError" needs to be exported by the entry point index.d.ts +// +// @public +export class FirebaseInstanceIdError extends FirebaseError { +} + +// Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts +// +// @public @deprecated +export function getInstanceId(app?: App): InstanceId; + +// @public @deprecated +export class InstanceId { + get app(): App; + deleteInstanceId(instanceId: string): Promise; +} + +// Warning: (ae-forgotten-export) The symbol "InstallationsClientErrorCode" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export class InstanceIdClientErrorCode extends InstallationsClientErrorCode { + // (undocumented) + static INVALID_INSTANCE_ID: { + code: string; + message: string; + }; +} + +``` diff --git a/etc/firebase-admin.machine-learning.api.md b/etc/firebase-admin.machine-learning.api.md new file mode 100644 index 0000000000..b6c3569de9 --- /dev/null +++ b/etc/firebase-admin.machine-learning.api.md @@ -0,0 +1,85 @@ +## API Report File for "firebase-admin.machine-learning" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +/// + +import { Agent } from 'http'; + +// @public (undocumented) +export interface GcsTfliteModelOptions extends ModelOptionsBase { + // (undocumented) + tfliteModel: { + gcsTfliteUri: string; + }; +} + +// Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts +// +// @public +export function getMachineLearning(app?: App): MachineLearning; + +// @public +export interface ListModelsOptions { + filter?: string; + pageSize?: number; + pageToken?: string; +} + +// @public +export interface ListModelsResult { + readonly models: Model[]; + readonly pageToken?: string; +} + +// @public +export class MachineLearning { + get 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; +} + +// @public +export class Model { + get createTime(): string; + get displayName(): string; + get etag(): string; + get locked(): boolean; + get modelHash(): string | undefined; + get modelId(): string; + get published(): boolean; + get tags(): string[]; + get tfliteModel(): TFLiteModel | undefined; + toJSON(): { + [key: string]: any; + }; + get updateTime(): string; + get validationError(): string | undefined; + waitForUnlocked(maxTimeMillis?: number): Promise; +} + +// @public (undocumented) +export type ModelOptions = ModelOptionsBase | GcsTfliteModelOptions; + +// @public +export interface ModelOptionsBase { + // (undocumented) + displayName?: string; + // (undocumented) + tags?: string[]; +} + +// @public +export interface TFLiteModel { + readonly gcsTfliteUri?: string; + readonly sizeBytes: number; +} + +``` diff --git a/etc/firebase-admin.messaging.api.md b/etc/firebase-admin.messaging.api.md new file mode 100644 index 0000000000..4b8e6c1ed3 --- /dev/null +++ b/etc/firebase-admin.messaging.api.md @@ -0,0 +1,466 @@ +## API Report File for "firebase-admin.messaging" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +/// + +import { Agent } from 'http'; + +// @public +export interface AndroidConfig { + collapseKey?: string; + data?: { + [key: string]: string; + }; + fcmOptions?: AndroidFcmOptions; + notification?: AndroidNotification; + priority?: ('high' | 'normal'); + restrictedPackageName?: string; + ttl?: number; +} + +// @public +export interface AndroidFcmOptions { + analyticsLabel?: string; +} + +// @public +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'); +} + +// @public +export interface ApnsConfig { + fcmOptions?: ApnsFcmOptions; + headers?: { + [key: string]: string; + }; + payload?: ApnsPayload; +} + +// @public +export interface ApnsFcmOptions { + analyticsLabel?: string; + imageUrl?: string; +} + +// @public +export interface ApnsPayload { + // (undocumented) + [customData: string]: any; + aps: Aps; +} + +// @public +export interface Aps { + // (undocumented) + [customData: string]: any; + alert?: string | ApsAlert; + badge?: number; + category?: string; + contentAvailable?: boolean; + mutableContent?: boolean; + sound?: string | CriticalSound; + threadId?: string; +} + +// @public (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; +} + +// @public (undocumented) +export interface BaseMessage { + // (undocumented) + android?: AndroidConfig; + // (undocumented) + apns?: ApnsConfig; + // (undocumented) + data?: { + [key: string]: string; + }; + // (undocumented) + fcmOptions?: FcmOptions; + // (undocumented) + notification?: Notification; + // (undocumented) + webpush?: WebpushConfig; +} + +// @public +export interface BatchResponse { + failureCount: number; + responses: SendResponse[]; + successCount: number; +} + +// @public (undocumented) +export interface ConditionMessage extends BaseMessage { + // (undocumented) + condition: string; +} + +// @public +export interface CriticalSound { + critical?: boolean; + name: string; + volume?: number; +} + +// @public +export interface DataMessagePayload { + // (undocumented) + [key: string]: string; +} + +// @public +export interface FcmOptions { + analyticsLabel?: string; +} + +// Warning: (ae-forgotten-export) The symbol "PrefixedFirebaseError" needs to be exported by the entry point index.d.ts +// +// @public +export class FirebaseMessagingError extends PrefixedFirebaseError { +} + +// Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts +// +// @public +export function getMessaging(app?: App): Messaging; + +// @public +export interface LightSettings { + color: string; + lightOffDurationMillis: number; + lightOnDurationMillis: number; +} + +// @public +export type Message = TokenMessage | TopicMessage | ConditionMessage; + +// @public +export class Messaging { + get app(): App; + send(message: Message, dryRun?: boolean): Promise; + // @deprecated + sendAll(messages: Message[], dryRun?: boolean): Promise; + sendEach(messages: Message[], dryRun?: boolean): Promise; + sendEachForMulticast(message: MulticastMessage, dryRun?: boolean): Promise; + // @deprecated + sendMulticast(message: MulticastMessage, dryRun?: boolean): Promise; + sendToCondition(condition: string, payload: MessagingPayload, options?: MessagingOptions): Promise; + // @deprecated + sendToDevice(registrationTokenOrTokens: string | string[], payload: MessagingPayload, options?: MessagingOptions): Promise; + // @deprecated + sendToDeviceGroup(notificationKey: string, payload: MessagingPayload, options?: MessagingOptions): Promise; + sendToTopic(topic: string, payload: MessagingPayload, options?: MessagingOptions): Promise; + subscribeToTopic(registrationTokenOrTokens: string | string[], topic: string): Promise; + unsubscribeFromTopic(registrationTokenOrTokens: string | string[], topic: string): Promise; +} + +// @public +export class MessagingClientErrorCode { + // (undocumented) + static AUTHENTICATION_ERROR: { + code: string; + message: string; + }; + // (undocumented) + static DEVICE_MESSAGE_RATE_EXCEEDED: { + code: string; + message: string; + }; + // (undocumented) + static INTERNAL_ERROR: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_ARGUMENT: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_DATA_PAYLOAD_KEY: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_OPTIONS: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_PACKAGE_NAME: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_PAYLOAD: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_RECIPIENT: { + code: string; + message: string; + }; + // (undocumented) + static INVALID_REGISTRATION_TOKEN: { + code: string; + message: string; + }; + // (undocumented) + static MESSAGE_RATE_EXCEEDED: { + code: string; + message: string; + }; + // (undocumented) + static MISMATCHED_CREDENTIAL: { + code: string; + message: string; + }; + // (undocumented) + static PAYLOAD_SIZE_LIMIT_EXCEEDED: { + code: string; + message: string; + }; + // (undocumented) + static REGISTRATION_TOKEN_NOT_REGISTERED: { + code: string; + message: string; + }; + // (undocumented) + static SERVER_UNAVAILABLE: { + code: string; + message: string; + }; + // (undocumented) + static THIRD_PARTY_AUTH_ERROR: { + code: string; + message: string; + }; + // (undocumented) + static TOO_MANY_TOPICS: { + code: string; + message: string; + }; + // (undocumented) + static TOPICS_MESSAGE_RATE_EXCEEDED: { + code: string; + message: string; + }; + // (undocumented) + static UNKNOWN_ERROR: { + code: string; + message: string; + }; +} + +// @public +export interface MessagingConditionResponse { + messageId: number; +} + +// @public @deprecated +export interface MessagingDeviceGroupResponse { + failedRegistrationTokens: string[]; + failureCount: number; + successCount: number; +} + +// @public @deprecated +export interface MessagingDeviceResult { + canonicalRegistrationToken?: string; + // Warning: (ae-forgotten-export) The symbol "FirebaseError" needs to be exported by the entry point index.d.ts + error?: FirebaseError; + messageId?: string; +} + +// @public @deprecated +export interface MessagingDevicesResponse { + // (undocumented) + canonicalRegistrationTokenCount: number; + // (undocumented) + failureCount: number; + // (undocumented) + multicastId: number; + // (undocumented) + results: MessagingDeviceResult[]; + // (undocumented) + successCount: number; +} + +// @public +export interface MessagingOptions { + // (undocumented) + [key: string]: any | undefined; + collapseKey?: string; + contentAvailable?: boolean; + dryRun?: boolean; + mutableContent?: boolean; + priority?: string; + restrictedPackageName?: string; + timeToLive?: number; +} + +// @public +export interface MessagingPayload { + data?: DataMessagePayload; + notification?: NotificationMessagePayload; +} + +// @public +export interface MessagingTopicManagementResponse { + // Warning: (ae-forgotten-export) The symbol "FirebaseArrayIndexError" needs to be exported by the entry point index.d.ts + errors: FirebaseArrayIndexError[]; + failureCount: number; + successCount: number; +} + +// @public +export interface MessagingTopicResponse { + messageId: number; +} + +// @public +export interface MulticastMessage extends BaseMessage { + // (undocumented) + tokens: string[]; +} + +// @public +export interface Notification { + body?: string; + imageUrl?: string; + title?: string; +} + +// @public +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; +} + +// @public +export interface SendResponse { + error?: FirebaseError; + messageId?: string; + success: boolean; +} + +// @public (undocumented) +export interface TokenMessage extends BaseMessage { + // (undocumented) + token: string; +} + +// @public (undocumented) +export interface TopicMessage extends BaseMessage { + // (undocumented) + topic: string; +} + +// @public +export interface WebpushConfig { + data?: { + [key: string]: string; + }; + fcmOptions?: WebpushFcmOptions; + headers?: { + [key: string]: string; + }; + notification?: WebpushNotification; +} + +// @public +export interface WebpushFcmOptions { + link?: string; +} + +// @public +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[]; +} + +``` diff --git a/etc/firebase-admin.project-management.api.md b/etc/firebase-admin.project-management.api.md new file mode 100644 index 0000000000..8f59f54642 --- /dev/null +++ b/etc/firebase-admin.project-management.api.md @@ -0,0 +1,100 @@ +## API Report File for "firebase-admin.project-management" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +/// + +import { Agent } from 'http'; + +// @public +export class AndroidApp { + addShaCertificate(certificateToAdd: ShaCertificate): Promise; + // (undocumented) + readonly appId: string; + deleteShaCertificate(certificateToDelete: ShaCertificate): Promise; + getConfig(): Promise; + getMetadata(): Promise; + getShaCertificates(): Promise; + setDisplayName(newDisplayName: string): Promise; +} + +// @public +export interface AndroidAppMetadata extends AppMetadata { + packageName: string; + // (undocumented) + platform: AppPlatform.ANDROID; +} + +// @public +export interface AppMetadata { + appId: string; + displayName?: string; + platform: AppPlatform; + projectId: string; + resourceName: string; +} + +// @public +export enum AppPlatform { + ANDROID = "ANDROID", + IOS = "IOS", + PLATFORM_UNKNOWN = "PLATFORM_UNKNOWN" +} + +// Warning: (ae-forgotten-export) The symbol "PrefixedFirebaseError" needs to be exported by the entry point index.d.ts +// +// @public +export class FirebaseProjectManagementError extends PrefixedFirebaseError { +} + +// Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts +// +// @public +export function getProjectManagement(app?: App): ProjectManagement; + +// @public +export class IosApp { + // (undocumented) + readonly appId: string; + getConfig(): Promise; + getMetadata(): Promise; + setDisplayName(newDisplayName: string): Promise; +} + +// @public +export interface IosAppMetadata extends AppMetadata { + bundleId: string; + // (undocumented) + platform: AppPlatform.IOS; +} + +// @public +export class ProjectManagement { + androidApp(appId: string): AndroidApp; + // (undocumented) + readonly 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; +} + +// @public (undocumented) +export type ProjectManagementErrorCode = 'already-exists' | 'authentication-error' | 'internal-error' | 'invalid-argument' | 'invalid-project-id' | 'invalid-server-response' | 'not-found' | 'service-unavailable' | 'unknown-error'; + +// @public +export class ShaCertificate { + readonly certType: ('sha1' | 'sha256'); + // (undocumented) + readonly resourceName?: string | undefined; + // (undocumented) + readonly shaHash: string; +} + +``` diff --git a/etc/firebase-admin.remote-config.api.md b/etc/firebase-admin.remote-config.api.md new file mode 100644 index 0000000000..aeadcbf779 --- /dev/null +++ b/etc/firebase-admin.remote-config.api.md @@ -0,0 +1,231 @@ +## API Report File for "firebase-admin.remote-config" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +/// + +import { Agent } from 'http'; + +// @public +export interface AndCondition { + conditions?: Array; +} + +// @public +export type DefaultConfig = { + [key: string]: string | number | boolean; +}; + +// @public +export type EvaluationContext = { + randomizationId?: string; +}; + +// @public +export interface ExplicitParameterValue { + value: string; +} + +// Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts +// +// @public +export function getRemoteConfig(app?: App): RemoteConfig; + +// @public +export interface GetServerTemplateOptions { + defaultConfig?: DefaultConfig; +} + +// @public +export interface InAppDefaultValue { + useInAppDefault: boolean; +} + +// @public +export interface InitServerTemplateOptions extends GetServerTemplateOptions { + template?: ServerTemplateDataType; +} + +// @public +export interface ListVersionsOptions { + endTime?: Date | string; + endVersionNumber?: string | number; + pageSize?: number; + pageToken?: string; + startTime?: Date | string; +} + +// @public +export interface ListVersionsResult { + nextPageToken?: string; + versions: Version[]; +} + +// @public +export interface MicroPercentRange { + microPercentLowerBound?: number; + microPercentUpperBound?: number; +} + +// @public +export interface NamedCondition { + condition: OneOfCondition; + name: string; +} + +// @public +export interface OneOfCondition { + andCondition?: AndCondition; + false?: Record; + orCondition?: OrCondition; + percent?: PercentCondition; + true?: Record; +} + +// @public +export interface OrCondition { + conditions?: Array; +} + +// @public +export type ParameterValueType = 'STRING' | 'BOOLEAN' | 'NUMBER' | 'JSON'; + +// @public +export interface PercentCondition { + microPercent?: number; + microPercentRange?: MicroPercentRange; + percentOperator?: PercentConditionOperator; + seed?: string; +} + +// @public +export enum PercentConditionOperator { + BETWEEN = "BETWEEN", + GREATER_THAN = "GREATER_THAN", + LESS_OR_EQUAL = "LESS_OR_EQUAL", + UNKNOWN = "UNKNOWN" +} + +// @public +export class RemoteConfig { + // (undocumented) + readonly app: App; + createTemplateFromJSON(json: string): RemoteConfigTemplate; + getServerTemplate(options?: GetServerTemplateOptions): Promise; + getTemplate(): Promise; + getTemplateAtVersion(versionNumber: number | string): Promise; + initServerTemplate(options?: InitServerTemplateOptions): ServerTemplate; + listVersions(options?: ListVersionsOptions): Promise; + publishTemplate(template: RemoteConfigTemplate, options?: { + force: boolean; + }): Promise; + rollback(versionNumber: number | string): Promise; + validateTemplate(template: RemoteConfigTemplate): Promise; +} + +// @public +export interface RemoteConfigCondition { + expression: string; + name: string; + tagColor?: TagColor; +} + +// @public +export interface RemoteConfigParameter { + conditionalValues?: { + [key: string]: RemoteConfigParameterValue; + }; + defaultValue?: RemoteConfigParameterValue; + description?: string; + valueType?: ParameterValueType; +} + +// @public +export interface RemoteConfigParameterGroup { + description?: string; + parameters: { + [key: string]: RemoteConfigParameter; + }; +} + +// @public +export type RemoteConfigParameterValue = ExplicitParameterValue | InAppDefaultValue; + +// @public +export interface RemoteConfigTemplate { + conditions: RemoteConfigCondition[]; + readonly etag: string; + parameterGroups: { + [key: string]: RemoteConfigParameterGroup; + }; + parameters: { + [key: string]: RemoteConfigParameter; + }; + version?: Version; +} + +// @public +export interface RemoteConfigUser { + email: string; + imageUrl?: string; + name?: string; +} + +// @public +export interface ServerConfig { + getBoolean(key: string): boolean; + getNumber(key: string): number; + getString(key: string): string; + getValue(key: string): Value; +} + +// @public +export interface ServerTemplate { + evaluate(context?: EvaluationContext): ServerConfig; + load(): Promise; + set(template: ServerTemplateDataType): void; + toJSON(): ServerTemplateData; +} + +// @public +export interface ServerTemplateData { + conditions: NamedCondition[]; + readonly etag: string; + parameters: { + [key: string]: RemoteConfigParameter; + }; + version?: Version; +} + +// @public +export type ServerTemplateDataType = ServerTemplateData | string; + +// @public +export type TagColor = 'BLUE' | 'BROWN' | 'CYAN' | 'DEEP_ORANGE' | 'GREEN' | 'INDIGO' | 'LIME' | 'ORANGE' | 'PINK' | 'PURPLE' | 'TEAL'; + +// @public +export interface Value { + asBoolean(): boolean; + asNumber(): number; + asString(): string; + getSource(): ValueSource; +} + +// @public +export type ValueSource = 'static' | 'default' | 'remote'; + +// @public +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; +} + +``` diff --git a/etc/firebase-admin.security-rules.api.md b/etc/firebase-admin.security-rules.api.md new file mode 100644 index 0000000000..890da538d1 --- /dev/null +++ b/etc/firebase-admin.security-rules.api.md @@ -0,0 +1,61 @@ +## API Report File for "firebase-admin.security-rules" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +/// + +import { Agent } from 'http'; + +// Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts +// +// @public +export function getSecurityRules(app?: App): SecurityRules; + +// @public +export class Ruleset implements RulesetMetadata { + readonly createTime: string; + readonly name: string; + // (undocumented) + readonly source: RulesFile[]; +} + +// @public +export interface RulesetMetadata { + readonly createTime: string; + readonly name: string; +} + +// @public +export class RulesetMetadataList { + readonly nextPageToken?: string; + readonly rulesets: RulesetMetadata[]; +} + +// @public +export interface RulesFile { + // (undocumented) + readonly content: string; + // (undocumented) + readonly name: string; +} + +// @public +export class SecurityRules { + // (undocumented) + readonly 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; +} + +``` diff --git a/etc/firebase-admin.storage.api.md b/etc/firebase-admin.storage.api.md new file mode 100644 index 0000000000..ab798e8fca --- /dev/null +++ b/etc/firebase-admin.storage.api.md @@ -0,0 +1,27 @@ +## API Report File for "firebase-admin.storage" + +> 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 { File as File_2 } from '@google-cloud/storage'; + +// @public +export function getDownloadURL(file: File_2): Promise; + +// Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts +// +// @public +export function getStorage(app?: App): Storage; + +// @public +export class Storage { + get app(): App; + bucket(name?: string): Bucket; +} + +``` diff --git a/generate-esm-wrapper.js b/generate-esm-wrapper.js new file mode 100644 index 0000000000..3d47c709d1 --- /dev/null +++ b/generate-esm-wrapper.js @@ -0,0 +1,82 @@ +/** + * @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. + */ + +const path = require('path'); +const fs = require('mz/fs'); + +async function main() { + const entryPoints = require('./entrypoints.json'); + for (const entryPoint in entryPoints) { + const info = entryPoints[entryPoint]; + if (info.legacy) { + continue; + } + + await generateEsmWrapper(entryPoint, info.dist); + } +} + +async function generateEsmWrapper(entryPoint, source) { + console.log(`Generating ESM wrapper for ${entryPoint}`); + const target = getTarget(entryPoint); + const output = getEsmOutput(source, target); + await fs.mkdir(path.dirname(target), { recursive: true }); + await fs.writeFile(target, output); + await fs.writeFile('./lib/esm/package.json', JSON.stringify({type: 'module'})); +} + +function getTarget(entryPoint) { + const child = entryPoint.replace('firebase-admin/', ''); + return `./lib/esm/${child}/index.js`; +} + +function getEsmOutput(source, target) { + const sourcePath = path.resolve(source); + const cjsSource = require.resolve(sourcePath); + const keys = getExports(cjsSource); + const targetPath = path.resolve(target); + const importPath = getImportPath(targetPath, cjsSource); + + let output = `import mod from ${JSON.stringify(importPath)};`; + output += '\n\n'; + for (const key of keys) { + output += `export const ${key} = mod.${key};\n`; + } + + return output; +} + +function getImportPath(from, to) { + const fromDir = path.dirname(from); + return path.relative(fromDir, to).replace(/\\/g, '/'); +} + +function getExports(cjsSource) { + const mod = require(cjsSource); + const keys = new Set(Object.getOwnPropertyNames(mod)); + keys.delete('__esModule'); + return [...keys].sort(); +} + +(async () => { + try { + await main(); + } catch (err) { + console.log(err); + process.exit(1); + } +})(); diff --git a/generate-reports.js b/generate-reports.js new file mode 100644 index 0000000000..129c06e542 --- /dev/null +++ b/generate-reports.js @@ -0,0 +1,88 @@ +/** + * @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. + */ + +const path = require('path'); +const fs = require('mz/fs'); +const yargs = require('yargs'); +const { Extractor, ExtractorConfig } = require('@microsoft/api-extractor'); + +const { local: localMode } = yargs + .option('local', { + boolean: true, + description: 'Run API Extractor with --local flag', + }) + .version(false) + .help().argv; + +// API Extractor configuration file. +const config = require('./api-extractor.json'); + +const tempConfigFile = 'api-extractor.tmp'; + +async function generateReports() { + const entryPoints = require('./entrypoints.json'); + for (const entryPoint in entryPoints) { + const filePath = entryPoints[entryPoint].typings; + await generateReportForEntryPoint(entryPoint, filePath); + } +} + +async function generateReportForEntryPoint(entryPoint, filePath) { + console.log(`\nGenerating API report for ${entryPoint}`) + console.log('========================================================\n'); + + const safeName = entryPoint.replace('/', '.'); + console.log('Updating configuration for entry point...'); + config.apiReport.reportFileName = `${safeName}.api.md`; + config.mainEntryPointFilePath = filePath; + console.log(`Report file name: ${config.apiReport.reportFileName}`); + console.log(`Entry point declaration: ${config.mainEntryPointFilePath}`); + await fs.writeFile(tempConfigFile, JSON.stringify(config)); + + try { + const configFile = ExtractorConfig.loadFile(tempConfigFile); + const extractorConfig = ExtractorConfig.prepare({ + configObject: configFile, + configObjectFullPath: path.resolve(tempConfigFile), + packageJson: { + name: safeName, + }, + packageJsonFullPath: path.resolve('package.json'), + }); + const extractorResult = Extractor.invoke(extractorConfig, { + localBuild: localMode, + showVerboseMessages: true + }); + if (!extractorResult.succeeded) { + throw new Error(`API Extractor completed with ${extractorResult.errorCount} errors` + + ` and ${extractorResult.warningCount} warnings`); + } + + console.error(`API Extractor completed successfully`); + } finally { + await fs.unlink(tempConfigFile); + } +} + +(async () => { + try { + await generateReports(); + } catch (err) { + console.log(err); + process.exit(1); + } +})(); diff --git a/gulpfile.js b/gulpfile.js index 34370409f8..749b6ff517 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"); @@ -19,20 +20,15 @@ /**************/ /* REQUIRES */ /**************/ -var fs = require('fs'); var _ = require('lodash'); var gulp = require('gulp'); var pkg = require('./package.json'); -var runSequence = require('run-sequence'); // File I/O -var fs = require('fs'); -var exit = require('gulp-exit'); var ts = require('gulp-typescript'); var del = require('del'); -var merge = require('merge2'); var header = require('gulp-header'); -var replace = require('gulp-replace'); +var filter = require('gulp-filter'); /****************/ @@ -40,20 +36,27 @@ var replace = require('gulp-replace'); /****************/ var paths = { src: [ - 'src/**/*.ts' + 'src/**/*.ts', ], - databaseSrc: [ - 'src/**/*.js' + test: [ + 'test/**/*.ts', + '!test/integration/postcheck/typescript/*.ts', ], build: 'lib/', }; -// Create a separate project for buildProject that overrides the rootDir +// 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. +var buildProject = ts.createProject('tsconfig.json', { rootDir: 'src', declarationMap: true }); + +// Include dom libraries during test compilation since we use some web SDK +// libraries in our tests. +var buildTest = ts.createProject('tsconfig.json', { lib: ['es2018', 'dom'] }); var banner = `/*! firebase-admin v${pkg.version} */\n`; @@ -67,55 +70,57 @@ 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()) - // Replace SDK version - .pipe(replace(/\/g, pkg.version)) - // Add header - .pipe(header(banner)) + .pipe(header(banner)); - // Write to build directory - .pipe(gulp.dest(paths.build)) -}); + const configuration = [ + 'lib/**/*.js', + 'lib/**/*.d.ts', + ]; -gulp.task('copyDatabase', function() { - return gulp.src(paths.databaseSrc) - // Add headers - .pipe(header(fs.readFileSync('third_party/database-license.txt', 'utf8'))) - .pipe(header(banner)) + workflow = workflow.pipe(filter(configuration)); - // Write to build directory - .pipe(gulp.dest(paths.build)) + // Write to build directory + return workflow.pipe(gulp.dest(paths.build)) +}); + +/** + * Task only used to capture typescript compilation errors in the test suite. + * Output is discarded. + */ +gulp.task('compile_test', function() { + return gulp.src(paths.test) + // Compile Typescript into .js and .d.ts files + .pipe(buildTest()) }); gulp.task('copyTypings', function() { - return gulp.src('src/index.d.ts') + return gulp.src(['src/index.d.ts', 'src/default-namespace.d.ts']) // Add header .pipe(header(banner)) - - // Write to build directory .pipe(gulp.dest(paths.build)) }); +gulp.task('compile_all', gulp.series('compile', 'copyTypings', 'compile_test')); + // Regenerates js every time a source file changes gulp.task('watch', function() { - gulp.watch(paths.src, ['compile']); + gulp.watch(paths.src.concat(paths.test), { ignoreInitial: false }, gulp.series('compile_all')); }); // Build task -gulp.task('build', function(done) { - runSequence('cleanup', 'compile', 'copyDatabase', 'copyTypings', function(error) { - done(error && error.err); - }); -}); +gulp.task('build', gulp.series('cleanup', 'compile', 'copyTypings')); // Default task -gulp.task('default', function(done) { - runSequence('build', function(error) { - done(error && error.err); - }); -}); +gulp.task('default', gulp.series('build')); diff --git a/package-lock.json b/package-lock.json index 34c358294b..82ab704853 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,9294 +1,8681 @@ { "name": "firebase-admin", - "version": "5.10.0", + "version": "12.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { - "@firebase/app": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.1.10.tgz", - "integrity": "sha512-2GTXt3b2QZXkmx6/5nNJq+pEN/VTjAG55MFJS1WMoLVZkwKuNpWNk65QVyPaoL88x1iHtuLqAMFgJUOnhOg+Pw==", + "@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true + }, + "@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, "requires": { - "@firebase/app-types": "0.1.2", - "@firebase/util": "0.1.10", - "tslib": "1.9.0" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "@firebase/app-types": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.1.2.tgz", - "integrity": "sha512-bCIZGeMtP0ibrXNNaU214/1tRNw0jHnir/cfiAao1gjUyIS7RzOTQoH+zbwPJNEwUqJ0T3ykw/Tv4/khGqbVBg==" - }, - "@firebase/auth": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.3.4.tgz", - "integrity": "sha512-lpKpPGVyNEuQasukVgxrti/GptEZDE24B/UnRmvjiwpVlOpVPLsaNJkklLiODlH7DS3yIyGHWYqojNl3iaTEmA==", + "@babel/code-frame": { + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", + "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", "dev": true, "requires": { - "@firebase/auth-types": "0.1.2" + "@babel/highlight": "^7.24.2", + "picocolors": "^1.0.0" } }, - "@firebase/auth-types": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.1.2.tgz", - "integrity": "sha512-pofZTXrz/urWmH+5opF4jpuv6GEaWOQtX9dl4AKAjOYoLceRyJn4OEeZodsDYdp6kLyARH1mcYtFMyZ9jvUtYg==", + "@babel/compat-data": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.4.tgz", + "integrity": "sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==", "dev": true }, - "@firebase/database": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.2.0.tgz", - "integrity": "sha512-3IYqpxFv3ZIOc+JmGKqn+Yyg36lsxmw/bzZrmsm2YG0Rk9uJkPCGMXxRI+K5+8RYZmQxotL+USRxGYzk0wKvjw==", - "requires": { - "@firebase/database-types": "0.2.0", - "@firebase/logger": "0.1.0", - "@firebase/util": "0.1.10", - "faye-websocket": "0.11.1", - "tslib": "1.9.0" + "@babel/core": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.4.tgz", + "integrity": "sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.2", + "@babel/generator": "^7.24.4", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.24.4", + "@babel/parser": "^7.24.4", + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.1", + "@babel/types": "^7.24.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" }, "dependencies": { - "faye-websocket": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.1.tgz", - "integrity": "sha1-8O/hjE9W5PQK/H4Gxxn9XuYYjzg=", - "requires": { - "websocket-driver": "0.7.0" - } + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true } } }, - "@firebase/database-types": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.2.0.tgz", - "integrity": "sha512-QFrxlLABVbZAVJqw1XNkSYZK22qPjpE3U5eM1SO7Htx69TrIgX7tb1/+BJnFkb3AKUD33tAr22Z4XVth5Ys46A==" + "@babel/generator": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.4.tgz", + "integrity": "sha512-Xd6+v6SnjWVx/nus+y0l1sxMOTOMBkyL4+BIdbALyatQnAe/SRVjANeDPSCYaX+i1iJmuGSKf3Z+E+V/va1Hvw==", + "dev": true, + "requires": { + "@babel/types": "^7.24.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + } }, - "@firebase/firestore": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-0.3.5.tgz", - "integrity": "sha512-lJXLIZ1gVEg6SyB64C40qXcdfLfIVTglpwN67AXHUvfjEKjxc6dvgzJkecF8ennwMeWBwMI86toLApB37t+Xbw==", + "@babel/helper-compilation-targets": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", "dev": true, "requires": { - "@firebase/firestore-types": "0.2.2", - "@firebase/logger": "0.1.0", - "@firebase/webchannel-wrapper": "0.2.6", - "grpc": "1.9.1", - "tslib": "1.9.0" + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "dependencies": { + "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==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } } }, - "@firebase/firestore-types": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-0.2.2.tgz", - "integrity": "sha512-yuC07Zi8p0myCQoU62O0fnGcNEcWZnKEGcQ1tj71Qh3E3Dw7qPJ75kXeeL95Bh1PHI0+TqAcDTEb9yVG9xIUVw==", + "@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true }, - "@firebase/logger": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.1.0.tgz", - "integrity": "sha512-/abxM9/l0V9WzNXvSonI2imVqORVhyCVS8yJ1O2rsRmNzw3FIPPIt0BuTvmCBH1oh1uDtZIn2Aar1p7zF69KWg==" - }, - "@firebase/messaging": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.2.2.tgz", - "integrity": "sha512-Xl0ZVF+OszdV1p0FM0haqkxXtSOoQyN7cMeoByN85qU2OCZ/+oe9KHkvvfpjV8yjysaZyhPr5GouX7x6iQW3Jg==", + "@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "requires": { - "@firebase/messaging-types": "0.1.2", - "@firebase/util": "0.1.10", - "tslib": "1.9.0" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" } }, - "@firebase/messaging-types": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@firebase/messaging-types/-/messaging-types-0.1.2.tgz", - "integrity": "sha512-4Oycm2JiDaLp9jUy4O25gD/B9Hqdy11hGjSNE0rzhVox5d0e1RF08QCwVt9xpjtBLRgEpPLyD9dPeSu4YK0Y4Q==", - "dev": true - }, - "@firebase/polyfill": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@firebase/polyfill/-/polyfill-0.2.0.tgz", - "integrity": "sha512-ylgKsY017TvDH9P01ewccwLBF/yhoj4W1lEad47cuhlOr1EOmSB3fUOVLiFUfElndPJdcpyEUMPK6Tm4hc7Ycw==", + "@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, "requires": { - "promise-polyfill": "7.1.0", - "tslib": "1.9.0" + "@babel/types": "^7.22.5" } }, - "@firebase/storage": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.1.8.tgz", - "integrity": "sha512-g0xYwJbgOuAaAJy5iAoEymS77m3oVqFh9IAF3A4LvqOC9q3v3ubSSYjpNHRPZstO68pMDKsNrqb2TcJgx92kSA==", + "@babel/helper-module-imports": { + "version": "7.24.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz", + "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==", "dev": true, "requires": { - "@firebase/storage-types": "0.1.2", - "tslib": "1.9.0" + "@babel/types": "^7.24.0" } }, - "@firebase/storage-types": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.1.2.tgz", - "integrity": "sha512-/nL93m2lIqzx4FajVnskn2YTDEj0ym53LCZegZpAPxm4GIkOQ8UhzzfHFfHJJCygb58xRszDkDuRlpJlakO4pA==", - "dev": true + "@babel/helper-module-transforms": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + } }, - "@firebase/util": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.1.10.tgz", - "integrity": "sha512-XEogRfUQBZ4T37TMq/3ZbuiTdRAKX8hF3TgJglUZNCJf/6QnQ+jlupCuMAXBqCGfw2Mw0m2matoCUBWpsyevOA==", + "@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, "requires": { - "tslib": "1.9.0" + "@babel/types": "^7.22.5" } }, - "@firebase/webchannel-wrapper": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.2.6.tgz", - "integrity": "sha512-Uv9ieuHVogIOOzpGmdjV3/0asMJPdssq2vrOYJ/UTlvekT6aGdv+sx2WWvIrGRWfFxWIkOxCqpqaGMYbhc88Pg==", - "dev": true - }, - "@google-cloud/common": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-0.16.1.tgz", - "integrity": "sha512-1sufDsSfgJ7fuBLq+ux8t3TlydMlyWl9kPZx2WdLINkGtf5RjvXX6EWYZiCMKe8flJ3oC0l95j5atN2uX5n3rg==", - "requires": { - "array-uniq": "1.0.3", - "arrify": "1.0.1", - "concat-stream": "1.6.0", - "create-error-class": "3.0.2", - "duplexify": "3.5.1", - "ent": "2.2.0", - "extend": "3.0.1", - "google-auto-auth": "0.9.7", - "is": "3.2.1", - "log-driver": "1.2.5", - "methmeth": "1.1.0", - "modelo": "4.2.3", - "request": "2.83.0", - "retry-request": "3.3.1", - "split-array-stream": "1.0.3", - "stream-events": "1.0.2", - "string-format-obj": "1.1.1", - "through2": "2.0.3" - } - }, - "@google-cloud/common-grpc": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@google-cloud/common-grpc/-/common-grpc-0.6.0.tgz", - "integrity": "sha512-b5i2auMeP+kPPPpWtZVgjbbbIB+3uDGw+Vww1QjG0SEQlahcGrwkCEaNLQit1R77m8ibxs+sTVa+AH/FNILAdQ==", + "@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, "requires": { - "@google-cloud/common": "0.16.1", - "dot-prop": "4.2.0", - "duplexify": "3.5.1", - "extend": "3.0.1", - "grpc": "1.9.1", - "is": "3.2.1", - "modelo": "4.2.3", - "retry-request": "3.3.1", - "through2": "2.0.3" + "@babel/types": "^7.22.5" } }, - "@google-cloud/firestore": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-0.13.0.tgz", - "integrity": "sha512-9Aak9O/NBwdhAJWn2ooaHJT0uyU6IN6oHegW4GcAzLwJKwx8nw+c/GwFufSS6PRMLTiXdpV0I/rvdz4nSgO1HA==", - "requires": { - "@google-cloud/common": "0.16.1", - "@google-cloud/common-grpc": "0.6.0", - "bun": "0.0.12", - "deep-equal": "1.0.1", - "extend": "3.0.1", - "functional-red-black-tree": "1.0.1", - "google-gax": "0.15.0", - "is": "3.2.1", - "safe-buffer": "5.1.1", - "through2": "2.0.3" + "@babel/helper-string-parser": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", + "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "dev": true + }, + "@babel/helpers": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.4.tgz", + "integrity": "sha512-FewdlZbSiwaVGlgT1DPANDuCHaDMiOo+D/IDYRFYjHOuv66xMSJ7fQwwODwRNAPkADIO/z1EoF/l2BCWlWABDw==", + "dev": true, + "requires": { + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.1", + "@babel/types": "^7.24.0" } }, - "@google-cloud/storage": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-1.6.0.tgz", - "integrity": "sha512-yQ63bJYoiwY220gn/KdTLPoHppAPwFHfG7VFLPwJ+1R5U1eqUN5XV2a7uPj1szGF8/gxlKm2UbE8DgoJJ76DFw==", - "requires": { - "@google-cloud/common": "0.16.1", - "arrify": "1.0.1", - "async": "2.6.0", - "compressible": "2.0.13", - "concat-stream": "1.6.0", - "create-error-class": "3.0.2", - "duplexify": "3.5.1", - "extend": "3.0.1", - "gcs-resumable-upload": "0.9.0", - "hash-stream-validation": "0.2.1", - "is": "3.2.1", - "mime": "2.2.0", - "mime-types": "2.1.17", - "once": "1.4.0", - "pumpify": "1.4.0", - "request": "2.83.0", - "safe-buffer": "5.1.1", - "snakeize": "0.1.0", - "stream-events": "1.0.2", - "string-format-obj": "1.1.1", - "through2": "2.0.3" + "@babel/highlight": { + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz", + "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "dependencies": { - "@google-cloud/common": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-0.16.1.tgz", - "integrity": "sha512-1sufDsSfgJ7fuBLq+ux8t3TlydMlyWl9kPZx2WdLINkGtf5RjvXX6EWYZiCMKe8flJ3oC0l95j5atN2uX5n3rg==", - "requires": { - "array-uniq": "1.0.3", - "arrify": "1.0.1", - "concat-stream": "1.6.0", - "create-error-class": "3.0.2", - "duplexify": "3.5.1", - "ent": "2.2.0", - "extend": "3.0.1", - "google-auto-auth": "0.9.3", - "is": "3.2.1", - "log-driver": "1.2.5", - "methmeth": "1.1.0", - "modelo": "4.2.3", - "request": "2.83.0", - "retry-request": "3.3.1", - "split-array-stream": "1.0.3", - "stream-events": "1.0.2", - "string-format-obj": "1.1.1", - "through2": "2.0.3" - } - }, - "gcp-metadata": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-0.4.1.tgz", - "integrity": "sha512-yFE7v+NyoMiTOi2L6r8q87eVbiZCKooJNPKXTHhBStga8pwwgWofK9iHl00qd0XevZxcpk7ORaEL/ALuTvlaGQ==", - "requires": { - "extend": "3.0.1", - "retry-request": "3.3.1" - } - }, - "google-auth-library": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-0.12.0.tgz", - "integrity": "sha512-79qCXtJ1VweBmmLr4yLq9S4clZB2p5Y+iACvuKk9gu4JitEnPc+bQFmYvtCYehVR44MQzD1J8DVmYW2w677IEw==", + "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": { - "gtoken": "1.2.3", - "jws": "3.1.4", - "lodash.isstring": "4.0.1", - "lodash.merge": "4.6.1", - "request": "2.83.0" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" } }, - "google-auto-auth": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/google-auto-auth/-/google-auto-auth-0.9.3.tgz", - "integrity": "sha512-TbOZZs0WJOolrRmdQLK5qmWdOJQFG1oPnxcIBbAwL7XCWcv3XgZ9gHJ6W4byrdEZT8TahNFgMfkHd73mqxM9dw==", + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, "requires": { - "async": "2.6.0", - "gcp-metadata": "0.4.1", - "google-auth-library": "0.12.0", - "request": "2.83.0" + "color-name": "1.1.3" } }, - "mime": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.2.0.tgz", - "integrity": "sha512-0Qz9uF1ATtl8RKJG4VRfOymh7PyEor6NbrI/61lRfuRe4vx9SNATrvAeTj2EWVRKjEQGskrzWkJBBY5NbaVHIA==" + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "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" + } } } }, - "@mrmlnc/readdir-enhanced": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", - "integrity": "sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==", - "requires": { - "call-me-maybe": "1.0.1", - "glob-to-regexp": "0.3.0" - } - }, - "@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "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==" - }, - "@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "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=" - }, - "@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", - "requires": { - "@protobufjs/aspromise": "1.1.2", - "@protobufjs/inquire": "1.1.0" - } - }, - "@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "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=" - }, - "@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "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=" - }, - "@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" - }, - "@types/bluebird": { - "version": "3.5.17", - "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.17.tgz", - "integrity": "sha512-1v3/dB01fkW5qTpZH5zjjBLgj97Jhpu/tjJt/g0LHMPTuj2D2a6sJhj9RcwqmXl+TtokCZQyaynxL4buN5G2WA==", - "dev": true - }, - "@types/chai": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-3.5.2.tgz", - "integrity": "sha1-wRzSgX06QBt7oPWkIPNcVhObHB4=", + "@babel/parser": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz", + "integrity": "sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==", "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=", + "@babel/template": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", "dev": true, "requires": { - "@types/chai": "3.5.2", - "@types/promises-a-plus": "0.0.27" + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" } }, - "@types/firebase-token-generator": { - "version": "2.0.28", - "resolved": "https://registry.npmjs.org/@types/firebase-token-generator/-/firebase-token-generator-2.0.28.tgz", - "integrity": "sha1-Z1VIHZMk4mt6XItFXWgUg3aCw5Y=", - "dev": true - }, - "@types/form-data": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.2.0.tgz", - "integrity": "sha512-vm5OGsKc61Sx/GTRMQ9d0H0PYCDebT78/bdIBPCoPEHdgp0etaH1RzMmkDygymUmyXTj3rdWQn0sRUpYKZzljA==", + "@babel/traverse": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.1.tgz", + "integrity": "sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==", "dev": true, "requires": { - "@types/node": "8.0.53" - } - }, - "@types/google-cloud__storage": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@types/google-cloud__storage/-/google-cloud__storage-1.1.7.tgz", - "integrity": "sha512-010Llp+5ze+XWWmZuLDxs0pZgFjOgtJQVt9icJ0Ed67ZFLq7PnXkYx8x/k9nwDojR5/X4XoLPNqB1F627TScdQ==", - "requires": { - "@types/node": "8.0.53" + "@babel/code-frame": "^7.24.1", + "@babel/generator": "^7.24.1", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.24.1", + "@babel/types": "^7.24.0", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "dependencies": { + "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 + } } }, - "@types/lodash": { - "version": "4.14.85", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.85.tgz", - "integrity": "sha512-HrZiwDl62if0z31+rB99CLlg7WzS7b+KmyW75XAHEl/ZG0De2ACo6skZ89Zh3jOWkjKObN0Apq3MUezg7u9NKQ==", - "dev": true - }, - "@types/long": { - "version": "3.0.32", - "resolved": "https://registry.npmjs.org/@types/long/-/long-3.0.32.tgz", - "integrity": "sha512-ZXyOOm83p7X8p3s0IYM3VeueNmHpkk/yMlP8CLeOnEcu6hIwPH7YjZBvhQkR0ZFS2DqZAxKtJ/M5fcuv3OU5BA==" - }, - "@types/mocha": { - "version": "2.2.44", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-2.2.44.tgz", - "integrity": "sha512-k2tWTQU8G4+iSMvqKi0Q9IIsWAp/n8xzdZS4Q4YVIltApoMA00wFBFdlJnmoaK1/z7B0Cy0yPe6GgXteSmdUNw==", - "dev": true - }, - "@types/nock": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@types/nock/-/nock-9.1.2.tgz", - "integrity": "sha512-Vdd1dRTUT5S1ONTcAMmQ2PCzIQccKMOpgu9T+knvJeGRCt29j3tcz9oRC1AM6OXD81+8U4mVuWzHklTlQW7W+w==", + "@babel/types": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", "dev": true, "requires": { - "@types/node": "8.0.53" + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" } }, - "@types/node": { - "version": "8.0.53", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.53.tgz", - "integrity": "sha512-54Dm6NwYeiSQmRB1BLXKr5GELi0wFapR1npi8bnZhEcu84d/yQKqnwwXQ56hZ0RUbTG6L5nqDZaN3dgByQXQRQ==" - }, - "@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.0.6", - "resolved": "https://registry.npmjs.org/@types/request/-/request-2.0.6.tgz", - "integrity": "sha512-8/VAk8kgeWuNQTOMmhaLyYmzX7Foshcdh0f1KwkyWQPmoPCy4AIMDx4KqI/n/uO5pAw2FOCgW+76iJTyjYsIRw==", + "@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", "dev": true, "requires": { - "@types/form-data": "2.2.0", - "@types/node": "8.0.53" + "@jridgewell/trace-mapping": "0.3.9" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + } } }, - "@types/request-promise": { - "version": "4.1.39", - "resolved": "https://registry.npmjs.org/@types/request-promise/-/request-promise-4.1.39.tgz", - "integrity": "sha512-q9/VlE0osQ+EJ/UCF/MzH/xiF+wahQ4LG2i7lKkJuuWeRaM0GlhG29d11HUEFTKSL0gh3T1sJIpbYE7bwku9aQ==", + "@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", "dev": true, "requires": { - "@types/bluebird": "3.5.17", - "@types/request": "2.0.6" + "eslint-visitor-keys": "^3.3.0" } }, - "@types/sinon": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-4.1.2.tgz", - "integrity": "sha512-fL6bJHYRzbw/7ofbKiJ65SOAasoe5mZhHNSYKxWsF3sGl/arhRwDPwXJqM1xofKNTQD14HNX9VruicM7pm++mQ==", + "@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", "dev": true }, - "@types/sinon-chai": { - "version": "2.7.29", - "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-2.7.29.tgz", - "integrity": "sha512-EkI/ZvJT4hglWo7Ipf9SX+J+R9htNOMjW8xiOhce7+0csqvgoF5IXqY5Ae1GqRgNtWCuaywR5HjVa1snkTqpOw==", + "@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "requires": { - "@types/chai": "3.5.2", - "@types/sinon": "4.1.2" + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "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 + } } }, - "abbrev": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", - "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", + "@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", "dev": true }, - "acorn": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", - "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" - }, - "acorn-es7-plugin": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/acorn-es7-plugin/-/acorn-es7-plugin-1.1.7.tgz", - "integrity": "sha1-8u4fMiipDurRJF+asZIusucdM2s=" + "@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==" }, - "ajv": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.3.0.tgz", - "integrity": "sha1-RBT/dKUIecII7l/cgm4ywwNUnto=", + "@firebase/api-documenter": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/api-documenter/-/api-documenter-0.4.0.tgz", + "integrity": "sha512-UUPxxj1wIAGkXBCF9UL1dhfyzM4Lcd/gMSCoJqy8o75rLqQVGB2GSBaU7I4JmQsQhIIk/nKKLiy4HM/70kEfZA==", + "dev": true, "requires": { - "co": "4.6.0", - "fast-deep-equal": "1.0.0", - "fast-json-stable-stringify": "2.0.0", - "json-schema-traverse": "0.3.1" + "@microsoft/tsdoc": "0.12.24", + "@rushstack/node-core-library": "3.59.7", + "@rushstack/ts-command-line": "4.15.2", + "api-extractor-model-me": "0.1.1", + "colors": "~1.4.0", + "js-yaml": "4.1.0", + "resolve": "~1.22.0", + "tslib": "^2.1.0" } }, - "align-text": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", - "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "@firebase/app": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.10.4.tgz", + "integrity": "sha512-oKd5cT+fDbQ22X8Am3tBOrSFdDp8n4NJDqld4uo+H/PL9F+D3ogtTeiPyDWw1lZK7FsMbmtRrPRozlmJFzSKAQ==", "dev": true, "requires": { - "kind-of": "3.2.2", - "longest": "1.0.1", - "repeat-string": "1.6.1" + "@firebase/component": "0.6.7", + "@firebase/logger": "0.4.2", + "@firebase/util": "1.9.6", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "dependencies": { + "@firebase/component": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.7.tgz", + "integrity": "sha512-baH1AA5zxfaz4O8w0vDwETByrKTQqB5CDjRls79Sa4eAGAoERw4Tnung7XbMl3jbJ4B/dmmtsMrdki0KikwDYA==", + "dev": true, + "requires": { + "@firebase/util": "1.9.6", + "tslib": "^2.1.0" + } + }, + "@firebase/logger": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.2.tgz", + "integrity": "sha512-Q1VuA5M1Gjqrwom6I6NUU4lQXdo9IAQieXlujeHZWvRt1b7qQ0KwBaNAjgxG27jgF9/mUwsNmO8ptBCGVYhB0A==", + "dev": true, + "requires": { + "tslib": "^2.1.0" + } + }, + "@firebase/util": { + "version": "1.9.6", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.6.tgz", + "integrity": "sha512-IBr1MZbp4d5MjBCXL3TW1dK/PDXX4yOGbiwRNh1oAbE/+ci5Uuvy9KIrsFYY80as1I0iOaD5oOMA9Q8j4TJWcw==", + "dev": true, + "requires": { + "tslib": "^2.1.0" + } + } } }, - "amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", - "dev": true - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "archy": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", - "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", - "dev": true + "@firebase/app-check-interop-types": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.1.tgz", + "integrity": "sha512-NILZbe6RH3X1pZmJnfOfY2gLIrlKmrkUMMrrK6VSXHcSE0eQv28xFEcw16D198i9JYZpy5Kwq394My62qCMaIw==" }, - "argparse": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", - "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", + "@firebase/app-compat": { + "version": "0.2.34", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.34.tgz", + "integrity": "sha512-enteBla1gBYObauvsC9bRRoqHZnOW48ahYABZ+l+FEiWil1rw0gVihl8D4eLqtQp/ci8+fbOBf3ZL19uFq/OCw==", "dev": true, "requires": { - "sprintf-js": "1.0.3" + "@firebase/app": "0.10.4", + "@firebase/component": "0.6.7", + "@firebase/logger": "0.4.2", + "@firebase/util": "1.9.6", + "tslib": "^2.1.0" + }, + "dependencies": { + "@firebase/component": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.7.tgz", + "integrity": "sha512-baH1AA5zxfaz4O8w0vDwETByrKTQqB5CDjRls79Sa4eAGAoERw4Tnung7XbMl3jbJ4B/dmmtsMrdki0KikwDYA==", + "dev": true, + "requires": { + "@firebase/util": "1.9.6", + "tslib": "^2.1.0" + } + }, + "@firebase/logger": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.2.tgz", + "integrity": "sha512-Q1VuA5M1Gjqrwom6I6NUU4lQXdo9IAQieXlujeHZWvRt1b7qQ0KwBaNAjgxG27jgF9/mUwsNmO8ptBCGVYhB0A==", + "dev": true, + "requires": { + "tslib": "^2.1.0" + } + }, + "@firebase/util": { + "version": "1.9.6", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.6.tgz", + "integrity": "sha512-IBr1MZbp4d5MjBCXL3TW1dK/PDXX4yOGbiwRNh1oAbE/+ci5Uuvy9KIrsFYY80as1I0iOaD5oOMA9Q8j4TJWcw==", + "dev": true, + "requires": { + "tslib": "^2.1.0" + } + } } }, - "arr-diff": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", - "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", - "dev": true, - "requires": { - "arr-flatten": "1.1.0" - } - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" - }, - "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", - "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=", - "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-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.0.0", - "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.0.0.tgz", - "integrity": "sha1-5zA08A3MH0CHYAj9IP6ud71LfC8=", - "dev": true + "@firebase/app-types": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.1.tgz", + "integrity": "sha512-nFGqTYsnDFn1oXf1tCwPAc+hQPxyvBT/QB7qDjwK+IDYThOn63nGhzdUTXxVD9Ca8gUY/e5PQMngeo0ZW/E3uQ==" }, - "array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "@firebase/auth": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.4.0.tgz", + "integrity": "sha512-SfFXZCHDbY+7oSR52NSwx0U7LjYiA+N8imloxphCf3/F+MFty/+mhdjSXGtrJYd0Gbud/qcyedfn2XnWJeIB/g==", + "dev": true, "requires": { - "array-uniq": "1.0.3" + "@firebase/component": "0.6.4", + "@firebase/logger": "0.4.0", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "dependencies": { + "@firebase/component": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.4.tgz", + "integrity": "sha512-rLMyrXuO9jcAUCaQXCMjCMUsWrba5fzHlNK24xz5j2W6A/SRmK8mZJ/hn7V0fViLbxC0lPMtrK1eYzk6Fg03jA==", + "dev": true, + "requires": { + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "@firebase/logger": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.0.tgz", + "integrity": "sha512-eRKSeykumZ5+cJPdxxJRgAC3G5NknY2GwEbKfymdnXtnT0Ucm4pspfR6GT4MUQEDuJwRVbVcSx85kgJulMoFFA==", + "dev": true, + "requires": { + "tslib": "^2.1.0" + } + }, + "@firebase/util": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.3.tgz", + "integrity": "sha512-DY02CRhOZwpzO36fHpuVysz6JZrscPiBXD0fXp6qSrL9oNOx5KWICKdR95C0lSITzxp0TZosVyHqzatE8JbcjA==", + "dev": true, + "requires": { + "tslib": "^2.1.0" + } + }, + "node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dev": true, + "requires": { + "whatwg-url": "^5.0.0" + } + } } }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=" - }, - "array-unique": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", - "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", - "dev": true - }, - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=" - }, - "ascli": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ascli/-/ascli-1.0.1.tgz", - "integrity": "sha1-vPpZdKYvGOgcq660lzKrSoj5Brw=", + "@firebase/auth-compat": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.4.9.tgz", + "integrity": "sha512-Fw03i7vduIciEBG4imLtA1duJbljgkfbxiBo/EuekcB+BnPxHp+e8OGMUfemPYeO7Munj6kUC9gr5DelsQkiNA==", + "dev": true, "requires": { - "colour": "0.7.1", - "optjs": "3.2.2" + "@firebase/auth": "1.4.0", + "@firebase/auth-types": "0.12.0", + "@firebase/component": "0.6.4", + "@firebase/util": "1.9.3", + "node-fetch": "2.6.7", + "tslib": "^2.1.0" + }, + "dependencies": { + "@firebase/auth-types": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.12.0.tgz", + "integrity": "sha512-pPwaZt+SPOshK8xNoiQlK5XIrS97kFYc3Rc7xmy373QsOJ9MmqXxLaYssP5Kcds4wd2qK//amx/c+A8O2fVeZA==", + "dev": true + }, + "@firebase/component": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.4.tgz", + "integrity": "sha512-rLMyrXuO9jcAUCaQXCMjCMUsWrba5fzHlNK24xz5j2W6A/SRmK8mZJ/hn7V0fViLbxC0lPMtrK1eYzk6Fg03jA==", + "dev": true, + "requires": { + "@firebase/util": "1.9.3", + "tslib": "^2.1.0" + } + }, + "@firebase/util": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.3.tgz", + "integrity": "sha512-DY02CRhOZwpzO36fHpuVysz6JZrscPiBXD0fXp6qSrL9oNOx5KWICKdR95C0lSITzxp0TZosVyHqzatE8JbcjA==", + "dev": true, + "requires": { + "tslib": "^2.1.0" + } + }, + "node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dev": true, + "requires": { + "whatwg-url": "^5.0.0" + } + } } }, - "asn1": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + "@firebase/auth-interop-types": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.2.tgz", + "integrity": "sha512-k3NA28Jfoo0+o391bFjoV9X5QLnUL1WbLhZZRbTQhZdmdGYJfX8ixtNNlHsYQ94bwG0QRbsmvkzDnzuhHrV11w==" }, - "assertion-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.2.tgz", - "integrity": "sha1-E8pRXYYgbaC6xm6DTdOX2HWBCUw=", + "@firebase/auth-types": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.12.2.tgz", + "integrity": "sha512-qsEBaRMoGvHO10unlDJhaKSuPn4pyoTtlQuP1ghZfzB6rNQPuhp/N/DcFZxm9i4v0SogjCbf9reWupwIvfmH6w==", "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=" - }, - "async": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz", - "integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==", + "@firebase/component": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.6.tgz", + "integrity": "sha512-pp7sWqHmAAlA3os6ERgoM3k5Cxff510M9RLXZ9Mc8KFKMBc2ct3RkZTWUF7ixJNvMiK/iNgRLPDrLR2gtRJ9iQ==", "requires": { - "lodash": "4.17.4" + "@firebase/util": "1.9.5", + "tslib": "^2.1.0" } }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "atob": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.0.3.tgz", - "integrity": "sha1-GcenYEc3dEaPILLS0DNyrX1Mv10=" - }, - "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=" - }, - "aws4": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", - "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" - }, - "axios": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.0.tgz", - "integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=", + "@firebase/database": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.4.tgz", + "integrity": "sha512-k84cXh+dtpzvY6yOhfyr1B+I1vjvSMtmlqotE0lTNVylc8m5nmOohjzpTLEQDrBWvwACX/VP5fEyajAdmnOKqA==", "requires": { - "follow-redirects": "1.4.1", - "is-buffer": "1.1.6" + "@firebase/app-check-interop-types": "0.3.1", + "@firebase/auth-interop-types": "0.2.2", + "@firebase/component": "0.6.6", + "@firebase/logger": "0.4.1", + "@firebase/util": "1.9.5", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" } }, - "babel-code-frame": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", - "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", - "dev": true, + "@firebase/database-compat": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-1.0.4.tgz", + "integrity": "sha512-GEEDAvsSMAkqy0BIFSVtFzoOIIcKHFfDM4aXHtWL/JCaNn4OOjH7td73jDfN3ALvpIN4hQki0FcxQ89XjqaTjQ==", "requires": { - "chalk": "1.1.3", - "esutils": "2.0.2", - "js-tokens": "3.0.2" + "@firebase/component": "0.6.6", + "@firebase/database": "1.0.4", + "@firebase/database-types": "1.0.2", + "@firebase/logger": "0.4.1", + "@firebase/util": "1.9.5", + "tslib": "^2.1.0" } }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "requires": { - "cache-base": "1.0.1", - "class-utils": "0.3.6", - "component-emitter": "1.2.1", - "define-property": "1.0.0", - "isobject": "3.0.1", - "mixin-deep": "1.3.1", - "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=", - "requires": { - "is-descriptor": "1.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } + "@firebase/database-types": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.2.tgz", + "integrity": "sha512-JRigr5JNLEHqOkI99tAGHDZF47469/cJz1tRAgGs8Feh+3ZmQy/vVChSqwMp2DuVUGp9PlmGsNSlpINJ/hDuIA==", + "requires": { + "@firebase/app-types": "0.9.1", + "@firebase/util": "1.9.5" } }, - "base64url": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/base64url/-/base64url-2.0.0.tgz", - "integrity": "sha1-6sFuA+oUOO/5Qj1puqNiYu0fcLs=" - }, - "bcrypt-pbkdf": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", - "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", - "optional": true, + "@firebase/logger": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.1.tgz", + "integrity": "sha512-tTIixB5UJbG9ZHSGZSZdX7THr3KWOLrejZ9B7jYsm6fpwgRNngKznQKA2wgYVyvBc1ta7dGFh9NtJ8n7qfiYIw==", "requires": { - "tweetnacl": "0.14.5" + "tslib": "^2.1.0" } }, - "beeper": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/beeper/-/beeper-1.1.1.tgz", - "integrity": "sha1-5tXqjF2tABMEpwsiY4RH9pyy+Ak=", - "dev": true - }, - "binaryextensions": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-1.0.1.tgz", - "integrity": "sha1-HmN0iLNbWL2l9HdL+WpSEqjJB1U=", - "dev": true - }, - "bluebird": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", - "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==", - "dev": true - }, - "boom": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", - "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", + "@firebase/util": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.5.tgz", + "integrity": "sha512-PP4pAFISDxsf70l3pEy34Mf3GkkUcVQ3MdKp6aSVb7tcpfUQxnsdV7twDd8EkfB6zZylH6wpUAoangQDmCUMqw==", "requires": { - "hoek": "4.2.0" + "tslib": "^2.1.0" } }, - "brace-expansion": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", - "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", + "@google-cloud/firestore": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.7.0.tgz", + "integrity": "sha512-41/vBFXOeSYjFI/2mJuJrDwg2umGk+FDrI/SCGzBRUe+UZWDN4GoahIbGZ19YQsY0ANNl6DRiAy4wD6JezK02g==", + "optional": true, "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" + "fast-deep-equal": "^3.1.1", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^4.3.3", + "protobufjs": "^7.2.6" } }, - "braces": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", - "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", - "dev": true, + "@google-cloud/paginator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.0.tgz", + "integrity": "sha512-87aeg6QQcEPxGCOthnpUjvw4xAZ57G7pL8FS0C4e/81fr3FjkpUpibf1s2v5XGyGhUVGF4Jfg7yEcxqn2iUw1w==", + "optional": true, "requires": { - "expand-range": "1.8.2", - "preserve": "0.2.0", - "repeat-element": "1.1.2" + "arrify": "^2.0.0", + "extend": "^3.0.2" } }, - "browser-stdout": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", - "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", - "dev": true - }, - "buffer-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz", - "integrity": "sha1-WWFrSYME1Var1GaWayLu2j7KX74=" - }, - "buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + "@google-cloud/projectify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz", + "integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==", + "optional": true }, - "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 + "@google-cloud/promisify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz", + "integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==", + "optional": true }, - "bun": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/bun/-/bun-0.0.12.tgz", - "integrity": "sha512-Toms18J9DqnT+IfWkwxVTB2EaBprHvjlMWrTIsfX4xbu3ZBqVBwrERU0em1IgtRe04wT+wJxMlKHZok24hrcSQ==", + "@google-cloud/storage": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.11.0.tgz", + "integrity": "sha512-W+OPOCgq7a3aAMANALbJAlEnpMV9fy681JWIm7dYe5W/+nRhq/UvA477TJT5/oPNA5DgiAdMEdiitdoLpZqhJg==", + "optional": true, "requires": { - "readable-stream": "1.0.34" + "@google-cloud/paginator": "^5.0.0", + "@google-cloud/projectify": "^4.0.0", + "@google-cloud/promisify": "^4.0.0", + "abort-controller": "^3.0.0", + "async-retry": "^1.3.3", + "duplexify": "^4.1.3", + "fast-xml-parser": "^4.3.0", + "gaxios": "^6.0.2", + "google-auth-library": "^9.6.3", + "html-entities": "^2.5.2", + "mime": "^3.0.0", + "p-limit": "^3.0.1", + "retry-request": "^7.0.0", + "teeny-request": "^9.0.0", + "uuid": "^8.0.0" }, "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "0.0.1", - "string_decoder": "0.10.31" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "optional": true } } }, - "bytebuffer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/bytebuffer/-/bytebuffer-5.0.1.tgz", - "integrity": "sha1-WC7qSxqHO20CCkjVjfhfC7ps/d0=", + "@grpc/grpc-js": { + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.10.7.tgz", + "integrity": "sha512-ZMBVjSeDAz3tFSehyO6Pd08xZT1HfIwq3opbeM4cDlBh52gmwp0wVIPcQur53NN0ac68HMZ/7SF2rGRD5KmVmg==", + "optional": true, "requires": { - "long": "3.2.0" - } - }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "requires": { - "collection-visit": "1.0.0", - "component-emitter": "1.2.1", - "get-value": "2.0.6", - "has-value": "1.0.0", - "isobject": "3.0.1", - "set-value": "2.0.0", - "to-object-path": "0.3.0", - "union-value": "1.0.0", - "unset-value": "1.0.0" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } + "@grpc/proto-loader": "^0.7.13", + "@js-sdsl/ordered-map": "^4.4.2" } }, - "call-me-maybe": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", - "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=" - }, - "call-signature": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/call-signature/-/call-signature-0.0.2.tgz", - "integrity": "sha1-qEq8glpV70yysCi9dOIFpluaSZY=" - }, - "camelcase": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=" - }, - "capture-stack-trace": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz", - "integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=" - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" - }, - "center-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", - "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", - "dev": true, + "@grpc/proto-loader": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", + "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", "optional": true, "requires": { - "align-text": "0.1.4", - "lazy-cache": "1.0.4" + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" } }, - "chai": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-3.5.0.tgz", - "integrity": "sha1-TQJjewZ/6Vi9v906QOxW/vc3Mkc=", - "dev": true, - "requires": { - "assertion-error": "1.0.2", - "deep-eql": "0.1.3", - "type-detect": "1.0.0" - } + "@gulpjs/messages": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@gulpjs/messages/-/messages-1.1.0.tgz", + "integrity": "sha512-Ys9sazDatyTgZVb4xPlDufLweJ/Os2uHWOv+Caxvy2O85JcnT4M3vc73bi8pdLWlv3fdWQz3pdI9tVwo8rQQSg==", + "dev": true }, - "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=", + "@gulpjs/to-absolute-glob": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@gulpjs/to-absolute-glob/-/to-absolute-glob-4.0.0.tgz", + "integrity": "sha512-kjotm7XJrJ6v+7knhPaRgaT6q8F8K2jiafwYdNHLzmV0uGLuZY43FK6smNSHUPrhq5kX2slCUy+RGG/xGqmIKA==", "dev": true, "requires": { - "check-error": "1.0.2" + "is-negated-glob": "^1.0.0" } }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", "dev": true, "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" } }, - "check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "dev": true + }, + "@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": { - "arr-union": "3.1.0", - "define-property": "0.2.5", - "isobject": "3.0.1", - "static-extend": "0.1.2" + "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": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "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": { - "is-descriptor": "0.1.6" + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" } }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } - } + "argparse": "^1.0.7", + "esprima": "^4.0.0" } }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } - } + "p-locate": "^4.1.0" } }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "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": { - "is-accessor-descriptor": "0.1.6", - "is-data-descriptor": "0.1.4", - "kind-of": "5.1.0" + "p-try": "^2.0.0" } }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + "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==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + "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 } } }, - "cliui": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", - "requires": { - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wrap-ansi": "2.1.0" - } - }, - "clone": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.2.tgz", - "integrity": "sha1-Jgt6meux7f4kdTgXX3gyQ8sZ0Uk=", - "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=", + "@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 }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" - }, - "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=" - }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "requires": { - "map-visit": "1.0.0", - "object-visit": "1.0.1" - } - }, - "color-convert": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.0.tgz", - "integrity": "sha1-Gsz5fdc5uYO/mU1W/sj5WFNkG3o=", + "@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, "requires": { - "color-name": "1.1.3" + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true }, - "colour": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/colour/-/colour-0.7.1.tgz", - "integrity": "sha1-nLFpkX7F0SwHNtPoaFdG3xyt93g=" + "@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true }, - "combined-stream": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", - "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", - "requires": { - "delayed-stream": "1.0.0" - } + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true }, - "commander": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", - "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", + "@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, "requires": { - "graceful-readlink": "1.0.1" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + "@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "optional": true }, - "compressible": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.13.tgz", - "integrity": "sha1-DRAgq5JLL9tNYnmHXH1tq6a6p6k=", - "requires": { - "mime-db": "1.33.0" + "@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "dev": true, + "requires": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" }, "dependencies": { - "mime-db": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", - "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "requires": { + "debug": "4" + } + }, + "https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "requires": { + "agent-base": "6", + "debug": "4" + } } } }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "concat-stream": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", - "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=", - "requires": { - "inherits": "2.0.3", - "readable-stream": "2.3.3", - "typedarray": "0.0.6" + "@microsoft/api-extractor": { + "version": "7.43.7", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.43.7.tgz", + "integrity": "sha512-t5M8BdnS+TmroUA/Z9HJXExS9iL4pK9I3yGu9PsXVTXPmcVXlBlA1CVI7TjRa1jwm+vusG/+sbX1/t5UkJhQMg==", + "dev": true, + "requires": { + "@microsoft/api-extractor-model": "7.28.17", + "@microsoft/tsdoc": "0.14.2", + "@microsoft/tsdoc-config": "~0.16.1", + "@rushstack/node-core-library": "4.3.0", + "@rushstack/rig-package": "0.5.2", + "@rushstack/terminal": "0.11.0", + "@rushstack/ts-command-line": "4.21.0", + "lodash": "~4.17.15", + "minimatch": "~3.0.3", + "resolve": "~1.22.1", + "semver": "~7.5.4", + "source-map": "~0.6.1", + "typescript": "5.4.2" + }, + "dependencies": { + "@microsoft/tsdoc": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz", + "integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==", + "dev": true + }, + "@rushstack/node-core-library": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-4.3.0.tgz", + "integrity": "sha512-JuNZ7lwaYQ4R1TugpryyWBn4lIxK+L7fF+muibFp0by5WklG22nsvH868fuBoZMLo5FqAs6WFOifNos4PJjWSA==", + "dev": true, + "requires": { + "fs-extra": "~7.0.1", + "import-lazy": "~4.0.0", + "jju": "~1.4.0", + "resolve": "~1.22.1", + "semver": "~7.5.4", + "z-schema": "~5.0.2" + } + }, + "@rushstack/ts-command-line": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.21.0.tgz", + "integrity": "sha512-z38FLUCn8M9FQf19gJ9eltdwkvc47PxvJmVZS6aKwbBAa3Pis3r3A+ZcBCVPNb9h/Tbga+i0tHdzoSGUoji9GQ==", + "dev": true, + "requires": { + "@rushstack/terminal": "0.11.0", + "@types/argparse": "1.0.38", + "argparse": "~1.0.9", + "string-argv": "~0.3.1" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "typescript": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", + "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", + "dev": true + } } }, - "concat-with-sourcemaps": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/concat-with-sourcemaps/-/concat-with-sourcemaps-1.0.4.tgz", - "integrity": "sha1-9Vs74q60dgGxCi1SWcz7cP0vHdY=", + "@microsoft/api-extractor-model": { + "version": "7.28.17", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.28.17.tgz", + "integrity": "sha512-b2AfLP33oEVtWLeNavSBRdyDa8sKlXjN4pdhBnC4HLontOtjILhL1ERAmZObF4PWSyChnnC2vjb47C9WKCFRGg==", "dev": true, "requires": { - "source-map": "0.5.7" - } - }, - "configstore": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.1.tgz", - "integrity": "sha512-5oNkD/L++l0O6xGXxb1EWS7SivtjfGQlRyxJsYgE0Z495/L81e2h4/d3r969hoPXuFItzNOKMtsXgYG4c7dYvw==", - "requires": { - "dot-prop": "4.2.0", - "graceful-fs": "4.1.11", - "make-dir": "1.2.0", - "unique-string": "1.0.0", - "write-file-atomic": "2.3.0", - "xdg-basedir": "3.0.0" + "@microsoft/tsdoc": "0.14.2", + "@microsoft/tsdoc-config": "~0.16.1", + "@rushstack/node-core-library": "4.3.0" }, "dependencies": { - "dot-prop": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", - "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", + "@microsoft/tsdoc": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz", + "integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==", + "dev": true + }, + "@rushstack/node-core-library": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-4.3.0.tgz", + "integrity": "sha512-JuNZ7lwaYQ4R1TugpryyWBn4lIxK+L7fF+muibFp0by5WklG22nsvH868fuBoZMLo5FqAs6WFOifNos4PJjWSA==", + "dev": true, + "requires": { + "fs-extra": "~7.0.1", + "import-lazy": "~4.0.0", + "jju": "~1.4.0", + "resolve": "~1.22.1", + "semver": "~7.5.4", + "z-schema": "~5.0.2" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, "requires": { - "is-obj": "1.0.1" + "lru-cache": "^6.0.0" } } } }, - "convert-source-map": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.0.tgz", - "integrity": "sha1-ms1whRxtXf3ZPZKC5e35SgP/RrU=", + "@microsoft/tsdoc": { + "version": "0.12.24", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.12.24.tgz", + "integrity": "sha512-Mfmij13RUTmHEMi9vRUhMXD7rnGR2VvxeNYtaGtaJ4redwwjT4UXYJ+nzmVJF7hhd4pn/Fx5sncDKxMVFJSWPg==", "dev": true }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" - }, - "core-js": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.3.tgz", - "integrity": "sha1-isw4NFgk8W2DZbfJtCWRaOjtYD4=" - }, - "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=" - }, - "create-error-class": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", - "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", - "requires": { - "capture-stack-trace": "1.0.0" - } - }, - "cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "@microsoft/tsdoc-config": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.16.2.tgz", + "integrity": "sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw==", "dev": true, "requires": { - "lru-cache": "4.1.1", - "shebang-command": "1.2.0", - "which": "1.3.0" + "@microsoft/tsdoc": "0.14.2", + "ajv": "~6.12.6", + "jju": "~1.4.0", + "resolve": "~1.19.0" }, "dependencies": { - "lru-cache": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz", - "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==", + "@microsoft/tsdoc": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz", + "integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==", + "dev": true + }, + "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": { - "pseudomap": "1.0.2", - "yallist": "2.1.2" + "is-core-module": "^2.1.0", + "path-parse": "^1.0.6" } } } }, - "cryptiles": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", - "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, "requires": { - "boom": "5.2.0" - }, - "dependencies": { - "boom": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", - "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", - "requires": { - "hoek": "4.2.0" - } - } + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" } }, - "crypto-random-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", - "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=" + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, "requires": { - "assert-plus": "1.0.0" + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" } }, - "dateformat": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-2.2.0.tgz", - "integrity": "sha1-QGXiATz5+5Ft39gu+1Bq1MZ2kGI=", - "dev": true + "@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "optional": true }, - "debug": { - "version": "2.6.8", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", - "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", + "@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 + }, + "@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "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": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "optional": true + }, + "@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "optional": true, "requires": { - "ms": "2.0.0" + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" } }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + "@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "optional": true }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" + "@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "optional": true }, - "deep-eql": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", - "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=", + "@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "optional": true + }, + "@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "optional": true + }, + "@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "optional": true + }, + "@rushstack/node-core-library": { + "version": "3.59.7", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.59.7.tgz", + "integrity": "sha512-ln1Drq0h+Hwa1JVA65x5mlSgUrBa1uHL+V89FqVWQgXd1vVIMhrtqtWGQrhTnFHxru5ppX+FY39VWELF/FjQCw==", "dev": true, "requires": { - "type-detect": "0.1.1" + "colors": "~1.2.1", + "fs-extra": "~7.0.1", + "import-lazy": "~4.0.0", + "jju": "~1.4.0", + "resolve": "~1.22.1", + "semver": "~7.5.4", + "z-schema": "~5.0.2" }, "dependencies": { - "type-detect": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", - "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=", + "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 + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } } } }, - "deep-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", - "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true - }, - "defaults": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", - "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "@rushstack/rig-package": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.5.2.tgz", + "integrity": "sha512-mUDecIJeH3yYGZs2a48k+pbhM6JYwWlgjs2Ca5f2n1G2/kgdgP9D/07oglEGf6mRyXEnazhEENeYTSNDRCwdqA==", "dev": true, "requires": { - "clone": "1.0.2" + "resolve": "~1.22.1", + "strip-json-comments": "~3.1.1" + }, + "dependencies": { + "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 + } } }, - "define-properties": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", - "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", + "@rushstack/terminal": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.11.0.tgz", + "integrity": "sha512-LKz7pv0G9Py5uULahNSixK1pTqIIKd103pAGhDW51YfzPojvmO5wfITe0PEUNAJZjuufN/KgeRW83dJo1gL2rQ==", + "dev": true, "requires": { - "foreach": "2.0.5", - "object-keys": "1.0.11" + "@rushstack/node-core-library": "4.3.0", + "supports-color": "~8.1.1" + }, + "dependencies": { + "@rushstack/node-core-library": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-4.3.0.tgz", + "integrity": "sha512-JuNZ7lwaYQ4R1TugpryyWBn4lIxK+L7fF+muibFp0by5WklG22nsvH868fuBoZMLo5FqAs6WFOifNos4PJjWSA==", + "dev": true, + "requires": { + "fs-extra": "~7.0.1", + "import-lazy": "~4.0.0", + "jju": "~1.4.0", + "resolve": "~1.22.1", + "semver": "~7.5.4", + "z-schema": "~5.0.2" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } } }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "@rushstack/ts-command-line": { + "version": "4.15.2", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.15.2.tgz", + "integrity": "sha512-5+C2uoJY8b+odcZD6coEe2XNC4ZjGB4vCMESbqW/8DHRWC/qIHfANdmN9F1wz/lAgxz72i7xRoVtPY2j7e4gpQ==", + "dev": true, "requires": { - "is-descriptor": "1.0.2", - "isobject": "3.0.1" + "@types/argparse": "1.0.38", + "argparse": "~1.0.9", + "colors": "~1.2.1", + "string-argv": "~0.3.1" }, "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + "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 } } }, - "del": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", - "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", + "@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0" + } + }, + "@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", "dev": true, "requires": { - "globby": "5.0.0", - "is-path-cwd": "1.0.0", - "is-path-in-cwd": "1.0.0", - "object-assign": "4.1.1", - "pify": "2.3.0", - "pinkie-promise": "2.0.1", - "rimraf": "2.6.2" + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" }, "dependencies": { - "globby": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", - "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", + "@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", "dev": true, "requires": { - "array-union": "1.0.2", - "arrify": "1.0.1", - "glob": "7.1.2", - "object-assign": "4.1.1", - "pify": "2.3.0", - "pinkie-promise": "2.0.1" + "type-detect": "4.0.8" } } } }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + "@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true }, - "deprecated": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/deprecated/-/deprecated-0.0.1.tgz", - "integrity": "sha1-+cmvVGSvoeepcUWKi97yqpTVuxk=", + "@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "optional": true + }, + "@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", "dev": true }, - "detect-file": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-0.1.0.tgz", - "integrity": "sha1-STXe39lIhkjgBrASlWbpOGcR6mM=", - "dev": true, - "requires": { - "fs-exists-sync": "0.1.0" - } + "@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true }, - "diff": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.2.0.tgz", - "integrity": "sha1-yc45Okt8vQsFinJck98pkCeGj/k=", + "@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", "dev": true }, - "diff-match-patch": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.0.tgz", - "integrity": "sha1-HMPIOkkNZ/ldkeOfatHy4Ia2MEg=" + "@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true }, - "dir-glob": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.0.0.tgz", - "integrity": "sha512-37qirFDz8cA5fimp9feo43fSuRo2gHwaIn6dXL8Ber1dGwUosDrGZeCCXq57WnIqE4aQ+u3eQZzsk1yOzhdwag==", + "@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": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", + "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", + "dev": true, "requires": { - "arrify": "1.0.1", - "path-type": "3.0.0" + "@types/node": "*" } }, - "dom-storage": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/dom-storage/-/dom-storage-2.0.2.tgz", - "integrity": "sha1-7RfL9oq9EOCu+BgnE+KXxeS1ALA=", + "@types/bluebird": { + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.42.tgz", + "integrity": "sha512-Jhy+MWRlro6UjVi578V/4ZGNfeCOcNCp0YaFNIUGFKlImowqwb1O/22wDVk3FDGMLqxdpOV3qQHD5fPEH4hK6A==", "dev": true }, - "dot-prop": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", - "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", + "@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", "requires": { - "is-obj": "1.0.1" + "@types/connect": "*", + "@types/node": "*" } }, - "duplexer": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", - "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", + "@types/caseless": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", + "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==" + }, + "@types/chai": { + "version": "4.3.14", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.14.tgz", + "integrity": "sha512-Wj71sXE4Q4AkGdG9Tvq1u/fquNz9EdG4LIJMwVVII7ashjD/8cf8fyIfJAjRr6YcsXnSE8cOGQPq1gqeR8z+3w==", "dev": true }, - "duplexer2": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz", - "integrity": "sha1-xhTc9n4vsUmVqRcR5aYX6KYKMds=", + "@types/chai-as-promised": { + "version": "7.1.8", + "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.8.tgz", + "integrity": "sha512-ThlRVIJhr69FLlh6IctTXFkmhtP3NpMZ2QGq69StYLyKZFp/HOp1VdKZj7RvfNWYYcJ1xlbLGLLWj1UvP5u/Gw==", "dev": true, "requires": { - "readable-stream": "1.1.14" - }, - "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.2", - "inherits": "2.0.3", - "isarray": "0.0.1", - "string_decoder": "0.10.31" - } - }, - "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 - } + "@types/chai": "*" } }, - "duplexify": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.5.1.tgz", - "integrity": "sha512-j5goxHTwVED1Fpe5hh3q9R93Kip0Bg2KVAt4f8CEYM3UEwYcPSvWbXaUQOzdX/HtiNomipv+gU7ASQPDbV7pGQ==", + "@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", "requires": { - "end-of-stream": "1.4.0", - "inherits": "2.0.3", - "readable-stream": "2.3.3", - "stream-shift": "1.0.0" + "@types/node": "*" } }, - "eastasianwidth": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.1.1.tgz", - "integrity": "sha1-RNZW3p2kFWlEZzNTZfsxR7hXK3w=" - }, - "ecc-jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", - "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", - "optional": true, + "@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", "requires": { - "jsbn": "0.1.1" + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" } }, - "ecdsa-sig-formatter": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz", - "integrity": "sha1-S8kmJ07Dtau1AW5+HWCSGsJisqE=", + "@types/express-serve-static-core": { + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.0.tgz", + "integrity": "sha512-bGyep3JqPCRry1wq+O5n7oiBgGWmeIJXPjXXCo8EK0u8duZGSYar7cGqd3ML2JUsLGeB7fmc06KYo9fLGWqPvQ==", "requires": { - "base64url": "2.0.0", - "safe-buffer": "5.1.1" + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" } }, - "empower": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/empower/-/empower-1.2.3.tgz", - "integrity": "sha1-bw2nNEf07dg4/sXGAxOoi6XLhSs=", + "@types/firebase-token-generator": { + "version": "2.0.33", + "resolved": "https://registry.npmjs.org/@types/firebase-token-generator/-/firebase-token-generator-2.0.33.tgz", + "integrity": "sha512-FNZyTEHIE5Kq55QpVuyK7ZUJE/yoegRXalGVhbUh8+xC9pa1BUP7zspY+P5ci5SYKgoHjYQruC9gulFCn576Zw==", + "dev": true + }, + "@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" + }, + "@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "@types/jsonwebtoken": { + "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": { - "core-js": "2.5.3", - "empower-core": "0.6.2" + "@types/node": "*" } }, - "empower-core": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/empower-core/-/empower-core-0.6.2.tgz", - "integrity": "sha1-Wt71ZgiOMfuoC6CjbfR9cJQWkUQ=", + "@types/lodash": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha512-wYCP26ZLxaT3R39kiN2+HcJ4kTd3U1waI/cY7ivWYqFP6pW3ZNpvi6Wd6PHZx7T/t8z0vlkXMg3QYLa7DZ/IJQ==", + "dev": true + }, + "@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "optional": true + }, + "@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" + }, + "@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true + }, + "@types/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", + "dev": true + }, + "@types/mocha": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.6.tgz", + "integrity": "sha512-dJvrYWxP/UcXm36Qn36fxhUKu8A/xMRXVT2cliFF1Z7UA9liG5Psj3ezNSZw+5puH2czDXRLcXQxf8JbJt0ejg==", + "dev": true + }, + "@types/nock": { + "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": { - "call-signature": "0.0.2", - "core-js": "2.5.3" + "nock": "*" } }, - "end-of-stream": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.0.tgz", - "integrity": "sha1-epDYM+/abPpurA9JSduw+tOmMgY=", + "@types/node": { + "version": "20.12.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz", + "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==", "requires": { - "once": "1.4.0" + "undici-types": "~5.26.4" } }, - "ent": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", - "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=" + "@types/qs": { + "version": "6.9.15", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", + "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==" }, - "error-ex": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", - "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", - "dev": true, + "@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" + }, + "@types/request": { + "version": "2.48.12", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.12.tgz", + "integrity": "sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==", "requires": { - "is-arrayish": "0.2.1" + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" } }, - "es-abstract": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.9.0.tgz", - "integrity": "sha512-kk3IJoKo7A3pWJc0OV8yZ/VEX2oSUytfekrJiqoxBlKJMFAJVJVpGdHClCCTdv+Fn2zHfpDHHIelMFhZVfef3Q==", + "@types/request-promise": { + "version": "4.1.51", + "resolved": "https://registry.npmjs.org/@types/request-promise/-/request-promise-4.1.51.tgz", + "integrity": "sha512-qVcP9Fuzh9oaAh8oPxiSoWMFGnWKkJDknnij66vi09Yiy62bsSDqtd+fG5kIM9wLLgZsRP3Y6acqj9O/v2ZtRw==", "dev": true, "requires": { - "es-to-primitive": "1.1.1", - "function-bind": "1.1.1", - "has": "1.0.1", - "is-callable": "1.1.3", - "is-regex": "1.0.4" + "@types/bluebird": "*", + "@types/request": "*" } }, - "es-to-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", - "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", - "dev": true, + "@types/semver": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "dev": true + }, + "@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", "requires": { - "is-callable": "1.1.3", - "is-date-object": "1.0.1", - "is-symbol": "1.0.1" + "@types/mime": "^1", + "@types/node": "*" } }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "requires": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } }, - "escodegen": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", - "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", + "@types/sinon": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.3.tgz", + "integrity": "sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw==", "dev": true, "requires": { - "esprima": "2.7.3", - "estraverse": "1.9.3", - "esutils": "2.0.2", - "optionator": "0.8.2", - "source-map": "0.2.0" - }, - "dependencies": { - "source-map": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", - "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", - "dev": true, - "optional": true, - "requires": { - "amdefine": "1.0.1" - } - } + "@types/sinonjs__fake-timers": "*" } }, - "esprima": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", - "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", - "dev": true - }, - "espurify": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/espurify/-/espurify-1.7.0.tgz", - "integrity": "sha1-HFz2y8zDLm9jk4C9T5kfq5up0iY=", + "@types/sinon-chai": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-3.2.12.tgz", + "integrity": "sha512-9y0Gflk3b0+NhQZ/oxGtaAJDvRywCa5sIyaVnounqLvmf93yBF4EgIRspePtkMs3Tr844nCclYMlcCNmLCvjuQ==", + "dev": true, "requires": { - "core-js": "2.5.3" + "@types/chai": "*", + "@types/sinon": "*" } }, - "estraverse": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", - "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=", + "@types/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", "dev": true }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==" + }, + "@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", "dev": true }, - "event-stream": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", - "integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=", + "@typescript-eslint/eslint-plugin": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", + "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", "dev": true, "requires": { - "duplexer": "0.1.1", - "from": "0.1.7", - "map-stream": "0.1.0", - "pause-stream": "0.0.11", - "split": "0.3.3", - "stream-combiner": "0.0.4", - "through": "2.3.8" + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" } }, - "expand-brackets": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", - "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "@typescript-eslint/parser": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", + "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "dev": true, "requires": { - "is-posix-bracket": "0.1.1" + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "debug": "^4.3.4" } }, - "expand-range": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", - "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", "dev": true, "requires": { - "fill-range": "2.2.3" + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" } }, - "expand-tilde": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-1.2.2.tgz", - "integrity": "sha1-C4HrqJflo9MdHD0QL48BRB5VlEk=", + "@typescript-eslint/type-utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", + "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", "dev": true, "requires": { - "os-homedir": "1.0.2" + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" } }, - "extend": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" + "@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, "requires": { - "is-extendable": "0.1.1" + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" } }, - "extglob": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", - "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", "dev": true, "requires": { - "is-extglob": "1.0.0" + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" } }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" - }, - "fancy-log": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.0.tgz", - "integrity": "sha1-Rb4X0Cu5kX1gzP/UmVyZnmyMmUg=", + "@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", "dev": true, "requires": { - "chalk": "1.1.3", - "time-stamp": "1.1.0" + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" } }, - "fast-deep-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", - "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=" + "@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true }, - "fast-glob": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.1.0.tgz", - "integrity": "sha512-QSSKZwDHLznUXdVtWvsfdbojmYI5igtVwfVbKW/LwNsy0JdM1cZ5yyP1kl5npg2ddugdnOk66QlNhbJ1c1hErg==", - "requires": { - "@mrmlnc/readdir-enhanced": "2.2.1", - "glob-parent": "3.1.0", - "is-glob": "4.0.0", - "merge2": "1.2.1", - "micromatch": "3.1.9" - }, - "dependencies": { - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" - }, - "braces": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.1.tgz", - "integrity": "sha512-SO5lYHA3vO6gz66erVvedSCkp7AKWdv6VcQ2N4ysXfPxdAlxAMMAdwegGGcv1Bqwm7naF1hNdk5d6AAIEHV2nQ==", - "requires": { - "arr-flatten": "1.1.0", - "array-unique": "0.3.2", - "define-property": "1.0.0", - "extend-shallow": "2.0.1", - "fill-range": "4.0.0", - "isobject": "3.0.1", - "kind-of": "6.0.2", - "repeat-element": "1.1.2", - "snapdragon": "0.8.1", - "snapdragon-node": "2.1.1", - "split-string": "3.1.0", - "to-regex": "3.0.2" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "1.0.2" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "0.1.1" - } - } - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "requires": { - "debug": "2.6.8", - "define-property": "0.2.5", - "extend-shallow": "2.0.1", - "posix-character-classes": "0.1.1", - "regex-not": "1.0.2", - "snapdragon": "0.8.1", - "to-regex": "3.0.2" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "0.1.6" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "0.1.1" - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "requires": { - "is-accessor-descriptor": "0.1.6", - "is-data-descriptor": "0.1.4", - "kind-of": "5.1.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" - } - } - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "requires": { - "assign-symbols": "1.0.0", - "is-extendable": "1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "2.0.4" - } - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "requires": { - "array-unique": "0.3.2", - "define-property": "1.0.0", - "expand-brackets": "2.1.4", - "extend-shallow": "2.0.1", - "fragment-cache": "0.2.1", - "regex-not": "1.0.2", - "snapdragon": "0.8.1", - "to-regex": "3.0.2" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "1.0.2" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "0.1.1" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "requires": { - "extend-shallow": "2.0.1", - "is-number": "3.0.0", - "repeat-string": "1.6.1", - "to-regex-range": "2.1.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "0.1.1" - } - } - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "requires": { - "is-glob": "3.1.0", - "path-dirname": "1.0.2" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "requires": { - "is-extglob": "2.1.1" - } - } - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" - }, - "is-glob": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", - "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", - "requires": { - "is-extglob": "2.1.1" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - }, - "merge2": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.2.1.tgz", - "integrity": "sha512-wUqcG5pxrAcaFI1lkqkMnk3Q7nUxV/NWfpAFSeWUwG9TRODnBDCUHa75mi3o3vLWQ5N4CQERWCauSlP0I3ZqUg==" - }, - "micromatch": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.9.tgz", - "integrity": "sha512-SlIz6sv5UPaAVVFRKodKjCg48EbNoIhgetzfK/Cy0v5U52Z6zB136M8tp0UC9jM53LYbmIRihJszvvqpKkfm9g==", - "requires": { - "arr-diff": "4.0.0", - "array-unique": "0.3.2", - "braces": "2.3.1", - "define-property": "2.0.2", - "extend-shallow": "3.0.2", - "extglob": "2.0.4", - "fragment-cache": "0.2.1", - "kind-of": "6.0.2", - "nanomatch": "1.2.9", - "object.pick": "1.3.0", - "regex-not": "1.0.2", - "snapdragon": "0.8.1", - "to-regex": "3.0.2" - } - } + "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" } }, - "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=" + "acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true }, - "faye-websocket": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.9.3.tgz", - "integrity": "sha1-SCpQWw3wrmJrlphm0710DNuWLoM=", + "acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true + }, + "agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "optional": true, "requires": { - "websocket-driver": "0.7.0" + "debug": "^4.3.4" } }, - "filename-regex": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", - "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", - "dev": true - }, - "fill-range": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz", - "integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=", + "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": { - "is-number": "2.1.0", - "isobject": "2.1.0", - "randomatic": "1.1.7", - "repeat-element": "1.1.2", - "repeat-string": "1.6.1" + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" } }, - "find-index": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/find-index/-/find-index-0.1.1.tgz", - "integrity": "sha1-Z101iyyjiS15Whq0cjL4tuLg3eQ=", - "dev": true - }, - "findup-sync": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.4.3.tgz", - "integrity": "sha1-QAQ5Kee8YK3wt/SCfExudaDeyhI=", + "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": { - "detect-file": "0.1.0", - "is-glob": "2.0.1", - "micromatch": "2.3.11", - "resolve-dir": "0.1.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" } }, - "fined": { + "ansi-colors": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fined/-/fined-1.1.0.tgz", - "integrity": "sha1-s33IRLdqL15wgeiE98CuNE8VNHY=", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", "dev": true, "requires": { - "expand-tilde": "2.0.2", - "is-plain-object": "2.0.4", - "object.defaults": "1.1.0", - "object.pick": "1.3.0", - "parse-filepath": "1.0.1" - }, - "dependencies": { - "expand-tilde": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", - "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", - "dev": true, - "requires": { - "homedir-polyfill": "1.0.1" - } - } + "ansi-wrap": "^0.1.0" } }, - "firebase": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/firebase/-/firebase-4.11.0.tgz", - "integrity": "sha512-G1fX9+q3YXjdz3Wdmq7TM3EmLnxSmhsZtOyyhKg5hYfHi0XoOyQrva/JJmROQ+Dz2Ku+hWpi8tUwOCt7uxKfng==", + "ansi-cyan": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-cyan/-/ansi-cyan-0.1.1.tgz", + "integrity": "sha512-eCjan3AVo/SxZ0/MyIYRtkpxIu/H3xZN7URr1vXVrISxeyz8fUFz0FJziamK4sS8I+t35y4rHg1b2PklyBe/7A==", "dev": true, "requires": { - "@firebase/app": "0.1.10", - "@firebase/auth": "0.3.4", - "@firebase/database": "0.2.0", - "@firebase/firestore": "0.3.5", - "@firebase/messaging": "0.2.2", - "@firebase/polyfill": "0.2.0", - "@firebase/storage": "0.1.8", - "dom-storage": "2.0.2", - "xmlhttprequest": "1.8.0" + "ansi-wrap": "0.1.0" } }, - "firebase-token-generator": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/firebase-token-generator/-/firebase-token-generator-2.0.0.tgz", - "integrity": "sha1-l2fXWewTq9yZuhFf1eqZ2Lk9EgY=", - "dev": true + "ansi-gray": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", + "integrity": "sha512-HrgGIZUl8h2EHuZaU9hTR/cU5nhKxpVE1V6kdGsQ8e4zirElJ5fvtfc8N7Q1oq1aatO275i8pUFUCpNWCAnVWw==", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } }, - "first-chunk-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/first-chunk-stream/-/first-chunk-stream-1.0.0.tgz", - "integrity": "sha1-Wb+1DNkF9g18OUzT2ayqtOatk04=", + "ansi-red": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-red/-/ansi-red-0.1.1.tgz", + "integrity": "sha512-ewaIr5y+9CUTGFwZfpECUbFlGcC0GCw1oqR9RI6h1gQCd9Aj2GxSckCnPsVJnmfMZbwFYE+leZGASgkWl06Jow==", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "ansi-wrap": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", + "integrity": "sha512-ZyznvL8k/FZeQHr2T6LzcJ/+vBApDnMNZvfVFy3At0knswWd6rJ3/0Hhmpu8oqa6C92npmozs890sX9Dl6q+Qw==", "dev": true }, - "flagged-respawn": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-0.3.2.tgz", - "integrity": "sha1-/xke3c1wiKZ1smEP/8l2vpuAdLU=", + "any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", "dev": true }, - "follow-redirects": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.4.1.tgz", - "integrity": "sha512-uxYePVPogtya1ktGnAAXOacnbIuRMB4dkvqeNz2qTtTQsuzSfbDolV+wMMKxAmCx0bLgAKLbBOkjItMbbkR1vg==", + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "api-extractor-model-me": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/api-extractor-model-me/-/api-extractor-model-me-0.1.1.tgz", + "integrity": "sha512-Ez801ZMADfkseOWNRFquvyQYDm3D9McpxfkKMWL6JFCGcpub0miJ+TFNphIR1nSZbrsxz3kIeOovNMY4VlL6Bw==", + "dev": true, "requires": { - "debug": "3.1.0" + "@microsoft/tsdoc": "0.12.24", + "@rushstack/node-core-library": "3.36.0" }, "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "@rushstack/node-core-library": { + "version": "3.36.0", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.36.0.tgz", + "integrity": "sha512-bID2vzXpg8zweXdXgQkKToEdZwVrVCN9vE9viTRk58gqzYaTlz4fMId6V3ZfpXN6H0d319uGi2KDlm+lUEeqCg==", + "dev": true, "requires": { - "ms": "2.0.0" + "@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" + } + }, + "@types/node": { + "version": "10.17.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.13.tgz", + "integrity": "sha512-pMCcqU2zT4TjqYFrWtYHKal7Sl30Ims6ulZ4UFXxI4xbtQqK/qqKwkDoBFCfooRqqmRu9vY3xaJRwxSh673aYg==", + "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 + }, + "commander": { + "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 + }, + "resolve": { + "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.6" + } + }, + "semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dev": true, + "requires": { + "lru-cache": "^6.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 + }, + "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" } } } }, - "for-in": { + "append-buffer": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" + "resolved": "https://registry.npmjs.org/append-buffer/-/append-buffer-1.0.2.tgz", + "integrity": "sha512-WLbYiXzD3y/ATLZFufV/rZvWdZOs+Z/+5v1rBZ463Jn398pa6kcde27cvozYnBoxXblGZTFfoPpsaEw0orU5BA==", + "dev": true, + "requires": { + "buffer-equal": "^1.0.0" + } }, - "for-own": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", - "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "append-transform": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", "dev": true, "requires": { - "for-in": "1.0.2" + "default-require-extensions": "^3.0.0" } }, - "foreach": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", - "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=" + "aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "dev": true }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + "archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", + "dev": true }, - "form-data": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz", - "integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=", + "are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "dev": true, "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.5", - "mime-types": "2.1.17" + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" } }, - "formatio": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/formatio/-/formatio-1.2.0.tgz", - "integrity": "sha1-87IWfZBoxGmKjVH092CjmlTYGOs=", + "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", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, "requires": { - "samsam": "1.3.0" + "sprintf-js": "~1.0.2" } }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "requires": { - "map-cache": "0.2.2" - } + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", + "dev": true }, - "from": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", - "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=", + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", "dev": true }, - "fs-exists-sync": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz", - "integrity": "sha1-mC1ok6+RjnLQjeyehnP/K1qNat0=", + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", "dev": true }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + "array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "dev": true, + "requires": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + } }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "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 }, - "functional-red-black-tree": { + "array-each": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=" + "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", + "integrity": "sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA==", + "dev": true }, - "gaze": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/gaze/-/gaze-0.5.2.tgz", - "integrity": "sha1-QLcJU30k0dRXZ9takIaJ3+aaxE8=", + "array-slice": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", + "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", + "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 + }, + "arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", "dev": true, "requires": { - "globule": "0.1.0" + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", + "is-shared-array-buffer": "^1.0.2" } }, - "gcp-metadata": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-0.6.2.tgz", - "integrity": "sha512-TlOa8HhM5klcBxBNazZUMeI9UZJoKJ4ceiBLPQoJIWNFeC/CqxjlaFE+/YnFQudqG5inhaaNtvoFVm/ZCbBFQQ==", + "arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==" + }, + "asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, "requires": { - "axios": "0.18.0", - "extend": "3.0.1", - "retry-axios": "0.3.2" + "safer-buffer": "~2.1.0" } }, - "gcs-resumable-upload": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/gcs-resumable-upload/-/gcs-resumable-upload-0.9.0.tgz", - "integrity": "sha512-+Zrmr0JKO2y/2mg953TW6JLu+NAMHqQsKzqCm7CIT24gMQakolPJCMzDleVpVjXAqB7ZCD276tcUq2ebOfqTug==", - "requires": { - "buffer-equal": "1.0.0", - "configstore": "3.1.1", - "google-auto-auth": "0.9.3", - "pumpify": "1.4.0", - "request": "2.83.0", - "stream-events": "1.0.2", - "through2": "2.0.3" - }, - "dependencies": { - "gcp-metadata": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-0.4.1.tgz", - "integrity": "sha512-yFE7v+NyoMiTOi2L6r8q87eVbiZCKooJNPKXTHhBStga8pwwgWofK9iHl00qd0XevZxcpk7ORaEL/ALuTvlaGQ==", - "requires": { - "extend": "3.0.1", - "retry-request": "3.3.1" - } - }, - "google-auth-library": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-0.12.0.tgz", - "integrity": "sha512-79qCXtJ1VweBmmLr4yLq9S4clZB2p5Y+iACvuKk9gu4JitEnPc+bQFmYvtCYehVR44MQzD1J8DVmYW2w677IEw==", - "requires": { - "gtoken": "1.2.3", - "jws": "3.1.4", - "lodash.isstring": "4.0.1", - "lodash.merge": "4.6.1", - "request": "2.83.0" - } - }, - "google-auto-auth": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/google-auto-auth/-/google-auto-auth-0.9.3.tgz", - "integrity": "sha512-TbOZZs0WJOolrRmdQLK5qmWdOJQFG1oPnxcIBbAwL7XCWcv3XgZ9gHJ6W4byrdEZT8TahNFgMfkHd73mqxM9dw==", - "requires": { - "async": "2.6.0", - "gcp-metadata": "0.4.1", - "google-auth-library": "0.12.0", - "request": "2.83.0" - } - } - } + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" + "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 }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", + "dev": true + }, + "async-done": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/async-done/-/async-done-2.0.0.tgz", + "integrity": "sha512-j0s3bzYq9yKIVLKGE/tWlCpa3PfFLcrDZLTSVdnnCTGagXuXBJO4SsY9Xdk/fQBirCkH4evW5xOeJXqlAQFdsw==", + "dev": true, "requires": { - "assert-plus": "1.0.0" + "end-of-stream": "^1.4.4", + "once": "^1.4.0", + "stream-exhaust": "^1.0.2" } }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "optional": true, "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" + "retry": "0.13.1" } }, - "glob-base": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", - "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "async-settle": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-2.0.0.tgz", + "integrity": "sha512-Obu/KE8FurfQRN6ODdHN9LuXqwC+JFIM9NRyZqJJ4ZfLJmIYN9Rg0/kb+wF70VV5+fJusTMQlJ1t5rF7J/ETdg==", "dev": true, "requires": { - "glob-parent": "2.0.0", - "is-glob": "2.0.1" + "async-done": "^2.0.0" } }, - "glob-parent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", - "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, "requires": { - "is-glob": "2.0.1" + "possible-typed-array-names": "^1.0.0" } }, - "glob-stream": { - "version": "3.1.18", - "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-3.1.18.tgz", - "integrity": "sha1-kXCl8St5Awb9/lmPMT+PeVT9FDs=", + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "dev": true + }, + "aws4": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", + "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==", + "dev": true + }, + "bach": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bach/-/bach-2.0.1.tgz", + "integrity": "sha512-A7bvGMGiTOxGMpNupYl9HQTf0FFDNF4VCmks4PJpFyN1AX2pdKuxuwdvUz2Hu388wcgp+OvGFNsumBfFNkR7eg==", "dev": true, "requires": { - "glob": "4.5.3", - "glob2base": "0.0.12", - "minimatch": "2.0.10", - "ordered-read-streams": "0.1.0", - "through2": "0.6.5", - "unique-stream": "1.0.0" - }, - "dependencies": { - "glob": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-4.5.3.tgz", - "integrity": "sha1-xstz0yJsHv7wTePFbQEvAzd+4V8=", - "dev": true, - "requires": { - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "2.0.10", - "once": "1.4.0" - } - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "minimatch": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-2.0.10.tgz", - "integrity": "sha1-jQh8OcazjAAbl/ynzm0OHoCvusc=", - "dev": true, - "requires": { - "brace-expansion": "1.1.8" - } - }, - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "dev": true, - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "0.0.1", - "string_decoder": "0.10.31" - } - }, - "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 - }, - "through2": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", - "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", - "dev": true, - "requires": { - "readable-stream": "1.0.34", - "xtend": "4.0.1" - } - } + "async-done": "^2.0.0", + "async-settle": "^2.0.0", + "now-and-later": "^3.0.0" } }, - "glob-to-regexp": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz", - "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=" + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, - "glob-watcher": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-0.0.6.tgz", - "integrity": "sha1-uVtKjfdLOcgymLDAXJeLTZo7cQs=", + "bare-events": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.2.2.tgz", + "integrity": "sha512-h7z00dWdG0PYOQEvChhOSWvOfkIKsdZGkWr083FgN/HyoQuebSew/cgirYqh9SCuy/hRvxc5Vy6Fw8xAmYHLkQ==", + "dev": true, + "optional": true + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", "dev": true, "requires": { - "gaze": "0.5.2" + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" } }, - "glob2base": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/glob2base/-/glob2base-0.0.12.tgz", - "integrity": "sha1-nUGbPijxLoOjYhZKJ3BVkiycDVY=", + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", "dev": true, "requires": { - "find-index": "0.1.1" + "tweetnacl": "^0.14.3" } }, - "global-modules": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-0.2.3.tgz", - "integrity": "sha1-6lo77ULG1s6ZWk+KEmm12uIjgo0=", - "dev": true, + "bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "optional": true + }, + "binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true + }, + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "requires": { - "global-prefix": "0.1.5", - "is-windows": "0.2.0" + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" } }, - "global-prefix": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-0.1.5.tgz", - "integrity": "sha1-jTvGuNo8qBEqFg2NSW/wRiv+948=", + "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==", "dev": true, "requires": { - "homedir-polyfill": "1.0.1", - "ini": "1.3.4", - "is-windows": "0.2.0", - "which": "1.3.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "globby": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/globby/-/globby-8.0.1.tgz", - "integrity": "sha512-oMrYrJERnKBLXNLVTqhm3vPEdJ/b2ZE28xN4YARiix1NOIOBPEpOUnm844K1iu/BkphCaf2WNFwMszv8Soi1pw==", - "requires": { - "array-union": "1.0.2", - "dir-glob": "2.0.0", - "fast-glob": "2.1.0", - "glob": "7.1.2", - "ignore": "3.3.7", - "pify": "3.0.0", - "slash": "1.0.0" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" - } + "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" } }, - "globule": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/globule/-/globule-0.1.0.tgz", - "integrity": "sha1-2cjt3h2nnRJaFRt5UzuXhnY0auU=", + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "browserslist": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", "dev": true, "requires": { - "glob": "3.1.21", - "lodash": "1.0.2", - "minimatch": "0.2.14" - }, + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "buffer-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.1.tgz", + "integrity": "sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==", + "dev": true + }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "caching-transform": { + "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": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" + } + }, + "call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001610", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001610.tgz", + "integrity": "sha512-QFutAY4NgaelojVMjY63o6XlZyORPaLfyMnsl3HgnWdJUcX6K0oaJymHjH8PT5Gk7sTm8rvC/c5COUQKXqmOMA==", + "dev": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true + }, + "chai": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", + "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.0.8" + } + }, + "chai-as-promised": { + "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" + } + }, + "chai-exclude": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/chai-exclude/-/chai-exclude-2.1.0.tgz", + "integrity": "sha512-IBnm50Mvl3O1YhPpTgbU8MK0Gw7NHcb18WT2TxGdPKOMtdtZVKLHmQwdvOF7mTlHVQStbXuZKFwkevFtbHjpVg==", + "dev": true, + "requires": { + "fclone": "^1.0.11" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, "dependencies": { - "glob": { - "version": "3.1.21", - "resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz", - "integrity": "sha1-0p4KBV3qUTj00H7UDomC6DwgZs0=", - "dev": true, - "requires": { - "graceful-fs": "1.2.3", - "inherits": "1.0.2", - "minimatch": "0.2.14" - } - }, - "graceful-fs": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", - "integrity": "sha1-FaSAaldUfLLS2/J/QuiajDRRs2Q=", - "dev": true - }, - "inherits": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz", - "integrity": "sha1-ykMJ2t7mtUzAuNJH6NfHoJdb3Js=", - "dev": true - }, - "lodash": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-1.0.2.tgz", - "integrity": "sha1-j1dWDIO1n8JwvT1WG2kAQ0MOJVE=", - "dev": true - }, - "minimatch": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", - "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", + "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": { - "lru-cache": "2.7.3", - "sigmund": "1.0.1" + "has-flag": "^4.0.0" } } } }, - "glogg": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.0.tgz", - "integrity": "sha1-f+DxmfV6yQbPUS/urY+Q7kooT8U=", + "check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", "dev": true, "requires": { - "sparkles": "1.0.0" + "get-func-name": "^2.0.2" } }, - "google-auth-library": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-1.3.1.tgz", - "integrity": "sha512-NcAzFY+ScalfjmFTHnCXInuivtbIfib9irJ5H8AHONy3eA56YW1Tu5X1dtbjw5TNBgP+BMu+nIrglJsAlfZ/Hg==", - "requires": { - "axios": "0.18.0", - "gcp-metadata": "0.6.2", - "gtoken": "2.1.1", - "jws": "3.1.4", - "lodash.isstring": "4.0.1", - "lru-cache": "4.1.2", - "retry-axios": "0.3.2" + "child-process-promise": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/child-process-promise/-/child-process-promise-2.2.1.tgz", + "integrity": "sha512-Fi4aNdqBsr0mv+jgWxcZ/7rAIC2mgihrptyVI4foh/rrjY/3BNjfP9+oaiFx/fzim+1ZyCNBae0DlyfQhSugog==", + "dev": true, + "requires": { + "cross-spawn": "^4.0.2", + "node-version": "^1.0.0", + "promise-polyfill": "^6.0.1" + } + }, + "chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "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 + }, + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "dev": true + }, + "clone-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz", + "integrity": "sha512-KLLTJWrvwIP+OPfMn0x2PheDEP20RPUcGXj/ERegTgdmPEZylALQldygiqrPPu8P45uNuPs7ckmReLY6v/iA5g==", + "dev": true + }, + "clone-stats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha512-au6ydSpg6nsrigcZ4m8Bc9hxjeW+GJ8xh5G3BJCMt4WXe1H10UNaVOamqQTmrx1kjVuxAHIQSNU6hY4Nsn9/ag==", + "dev": true + }, + "cloneable-readable": { + "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": { - "google-p12-pem": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-1.0.1.tgz", - "integrity": "sha512-6Gb+R8wKs0uGWHYH8US1q4IGYEMKPzg/ty2A/AevGaVDMzPIqNOKFmDxZHsHwda2438u99CkU0HdatsKXOUtcg==", + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, "requires": { - "node-forge": "0.7.4", - "pify": "3.0.0" + "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" } }, - "gtoken": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-2.1.1.tgz", - "integrity": "sha512-9wUP0Gb06lEJxX0w/w+n5Ghxh+/To0rbZSRCOu4Pih2sSDYXJwV4T7q6MPLW31cuKz0wqFQ60mW9nIKc8IgoyA==", - "requires": { - "axios": "0.18.0", - "google-p12-pem": "1.0.1", - "jws": "3.1.4", - "mime": "2.2.0", - "pify": "3.0.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==", + "dev": true }, - "lru-cache": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.2.tgz", - "integrity": "sha512-wgeVXhrDwAWnIF/yZARsFnMBtdFXOg1b8RIrhilp+0iDYN4mdQcNZElDZ0e4B64BhaxeQ5zN7PMyvu7we1kPeQ==", + "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": { - "pseudomap": "1.0.2", - "yallist": "2.1.2" + "safe-buffer": "~5.1.0" } - }, - "mime": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.2.0.tgz", - "integrity": "sha512-0Qz9uF1ATtl8RKJG4VRfOymh7PyEor6NbrI/61lRfuRe4vx9SNATrvAeTj2EWVRKjEQGskrzWkJBBY5NbaVHIA==" - }, - "node-forge": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.4.tgz", - "integrity": "sha512-8Df0906+tq/omxuCZD6PqhPaQDYuyJ1d+VITgxoIA8zvQd1ru+nMJcDChHH324MWitIgbVkAkQoGEEVJNpn/PA==" - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" } } }, - "google-auto-auth": { - "version": "0.9.7", - "resolved": "https://registry.npmjs.org/google-auto-auth/-/google-auto-auth-0.9.7.tgz", - "integrity": "sha512-Nro7aIFrL2NP0G7PoGrJqXGMZj8AjdBOcbZXRRm/8T3w08NUHIiNN3dxpuUYzDsZizslH+c8e+7HXL8vh3JXTQ==", + "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==", "requires": { - "async": "2.6.0", - "gcp-metadata": "0.6.2", - "google-auth-library": "1.3.1", - "request": "2.83.0" + "color-name": "~1.1.4" } }, - "google-gax": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-0.15.0.tgz", - "integrity": "sha512-a+WBi3oiV3jQ0eLCIM0GAFe8vYQ10yYuXRnjhEEXFKSNd8nW6XSQ7YWqMLIod2Xnyu6JiSSymMBwCr5YSwQyRQ==", - "requires": { - "extend": "3.0.1", - "globby": "8.0.1", - "google-auto-auth": "0.9.7", - "google-proto-files": "0.15.1", - "grpc": "1.9.1", - "is-stream-ended": "0.1.3", - "lodash": "4.17.4", - "protobufjs": "6.8.6", - "readable-stream": "2.3.3", - "through2": "2.0.3" + "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==" + }, + "color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true + }, + "colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "optional": true + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "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" }, "dependencies": { - "@types/node": { - "version": "8.9.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.9.4.tgz", - "integrity": "sha512-dSvD36qnQs78G1BPsrZFdPpvLgMW/dnvr5+nTW2csMs5TiP9MOXrjUbnMZOEwnIuBklXtn7b6TPA2Cuq07bDHA==" + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "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" + } }, - "long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + "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 }, - "protobufjs": { - "version": "6.8.6", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.6.tgz", - "integrity": "sha512-eH2OTP9s55vojr3b7NBaF9i4WhWPkv/nq55nznWNp/FomKrLViprUcqnBjHph2tFQ+7KciGPTPsVWGz0SOhL0Q==", - "requires": { - "@protobufjs/aspromise": "1.1.2", - "@protobufjs/base64": "1.1.2", - "@protobufjs/codegen": "2.0.4", - "@protobufjs/eventemitter": "1.1.0", - "@protobufjs/fetch": "1.1.0", - "@protobufjs/float": "1.0.2", - "@protobufjs/inquire": "1.1.0", - "@protobufjs/path": "1.1.2", - "@protobufjs/pool": "1.1.0", - "@protobufjs/utf8": "1.1.0", - "@types/long": "3.0.32", - "@types/node": "8.9.4", - "long": "4.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==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" } } } }, - "google-p12-pem": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-0.1.2.tgz", - "integrity": "sha1-M8RqsCGqc0+gMys5YKmj/8svMXc=", + "concat-with-sourcemaps": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/concat-with-sourcemaps/-/concat-with-sourcemaps-1.1.0.tgz", + "integrity": "sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==", + "dev": true, "requires": { - "node-forge": "0.7.1" + "source-map": "^0.6.1" } }, - "google-proto-files": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/google-proto-files/-/google-proto-files-0.15.1.tgz", - "integrity": "sha512-ebtmWgi/ooR5Nl63qRVZZ6VLM6JOb5zTNxTT/ZAU8yfMOdcauoOZNNMOVg0pCmTjqWXeuuVbgPP0CwO5UHHzBQ==", + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "dev": true + }, + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "copy-props": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-4.0.0.tgz", + "integrity": "sha512-bVWtw1wQLzzKiYROtvNlbJgxgBYt2bMJpkCbKmXM3xyijvcjjWXEk5nyrrT3bgJ7ODb19ZohE2T0Y3FgNPyoTw==", + "dev": true, + "requires": { + "each-props": "^3.0.0", + "is-plain-object": "^5.0.0" + } + }, + "core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "cross-spawn": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", + "integrity": "sha512-yAXz/pA1tD8Gtg2S98Ekf/sewp3Lcp3YoFKJ4Hkp5h5yLWnKVTDU0kwjKJ8NDCYcfTLfyGkzTikst+jWypT1iA==", + "dev": true, "requires": { - "globby": "7.1.1", - "power-assert": "1.4.4", - "protobufjs": "6.8.6" + "lru-cache": "^4.0.1", + "which": "^1.2.9" }, "dependencies": { - "@types/node": { - "version": "8.9.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.9.4.tgz", - "integrity": "sha512-dSvD36qnQs78G1BPsrZFdPpvLgMW/dnvr5+nTW2csMs5TiP9MOXrjUbnMZOEwnIuBklXtn7b6TPA2Cuq07bDHA==" - }, - "globby": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/globby/-/globby-7.1.1.tgz", - "integrity": "sha1-+yzP+UAfhgCUXfral0QMypcrhoA=", + "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": { - "array-union": "1.0.2", - "dir-glob": "2.0.0", - "glob": "7.1.2", - "ignore": "3.3.7", - "pify": "3.0.0", - "slash": "1.0.0" + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" } }, - "long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" - }, - "protobufjs": { - "version": "6.8.6", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.6.tgz", - "integrity": "sha512-eH2OTP9s55vojr3b7NBaF9i4WhWPkv/nq55nznWNp/FomKrLViprUcqnBjHph2tFQ+7KciGPTPsVWGz0SOhL0Q==", - "requires": { - "@protobufjs/aspromise": "1.1.2", - "@protobufjs/base64": "1.1.2", - "@protobufjs/codegen": "2.0.4", - "@protobufjs/eventemitter": "1.1.0", - "@protobufjs/fetch": "1.1.0", - "@protobufjs/float": "1.0.2", - "@protobufjs/inquire": "1.1.0", - "@protobufjs/path": "1.1.2", - "@protobufjs/pool": "1.1.0", - "@protobufjs/utf8": "1.1.0", - "@types/long": "3.0.32", - "@types/node": "8.9.4", - "long": "4.0.0" - } + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", + "dev": true } } }, - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "requires": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + } }, - "graceful-readlink": { + "data-view-byte-length": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", - "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + } + }, + "data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "requires": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "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 }, - "growl": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", - "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=", + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "requires": { + "mimic-response": "^3.1.0" + } + }, + "deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "requires": { + "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==" + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, - "grpc": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/grpc/-/grpc-1.9.1.tgz", - "integrity": "sha512-WNW3MWMuAoo63AwIlzFE3T0KzzvNBSvOkg67Hm8WhvHNkXFBlIk1QyJRE3Ocm0O5eIwS7JU8Ssota53QR1zllg==", + "default-require-extensions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", + "integrity": "sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==", + "dev": true, "requires": { - "lodash": "4.17.4", - "nan": "2.9.2", - "node-pre-gyp": "0.6.39", - "protobufjs": "5.0.2" + "strip-bom": "^4.0.0" }, "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true - }, - "ajv": { - "version": "4.11.8", - "bundled": true, - "requires": { - "co": "4.6.0", - "json-stable-stringify": "1.0.1" - } - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true - }, - "are-we-there-yet": { - "version": "1.1.4", - "bundled": true, - "requires": { - "delegates": "1.0.0", - "readable-stream": "2.3.3" - } - }, - "asn1": { - "version": "0.2.3", - "bundled": true - }, - "assert-plus": { - "version": "0.2.0", - "bundled": true - }, - "asynckit": { - "version": "0.4.0", - "bundled": true - }, - "aws-sign2": { - "version": "0.6.0", - "bundled": true - }, - "aws4": { - "version": "1.6.0", - "bundled": true - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true - }, - "bcrypt-pbkdf": { - "version": "1.0.1", - "bundled": true, - "optional": true, - "requires": { - "tweetnacl": "0.14.5" - } - }, - "block-stream": { - "version": "0.0.9", - "bundled": true, - "requires": { - "inherits": "2.0.3" - } - }, - "boom": { - "version": "2.10.1", - "bundled": true, - "requires": { - "hoek": "2.16.3" - } - }, - "brace-expansion": { - "version": "1.1.8", - "bundled": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "caseless": { - "version": "0.12.0", - "bundled": true - }, - "co": { - "version": "4.6.0", - "bundled": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true - }, - "combined-stream": { - "version": "1.0.5", - "bundled": true, - "requires": { - "delayed-stream": "1.0.0" - } - }, - "concat-map": { - "version": "0.0.1", - "bundled": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true - }, - "cryptiles": { - "version": "2.0.5", - "bundled": true, - "requires": { - "boom": "2.10.1" - } - }, - "dashdash": { - "version": "1.14.1", - "bundled": true, - "requires": { - "assert-plus": "1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true - } - } - }, - "debug": { - "version": "2.6.9", - "bundled": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.4.2", - "bundled": true - }, - "delayed-stream": { - "version": "1.0.0", - "bundled": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true - }, - "ecc-jsbn": { - "version": "0.1.1", - "bundled": true, - "optional": true, - "requires": { - "jsbn": "0.1.1" - } - }, - "extend": { - "version": "3.0.1", - "bundled": true - }, - "extsprintf": { - "version": "1.3.0", - "bundled": true - }, - "forever-agent": { - "version": "0.6.1", - "bundled": true - }, - "form-data": { - "version": "2.1.4", - "bundled": true, - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.5", - "mime-types": "2.1.17" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true - }, - "fstream": { - "version": "1.0.11", - "bundled": true, - "requires": { - "graceful-fs": "4.1.11", - "inherits": "2.0.3", - "mkdirp": "0.5.1", - "rimraf": "2.6.2" - } - }, - "fstream-ignore": { - "version": "1.0.5", - "bundled": true, - "requires": { - "fstream": "1.0.11", - "inherits": "2.0.3", - "minimatch": "3.0.4" - } - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "requires": { - "aproba": "1.2.0", - "console-control-strings": "1.1.0", - "has-unicode": "2.0.1", - "object-assign": "4.1.1", - "signal-exit": "3.0.2", - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wide-align": "1.1.2" - } - }, - "getpass": { - "version": "0.1.7", - "bundled": true, - "requires": { - "assert-plus": "1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true - } - } - }, - "glob": { - "version": "7.1.2", - "bundled": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "graceful-fs": { - "version": "4.1.11", - "bundled": true - }, - "har-schema": { - "version": "1.0.5", - "bundled": true - }, - "har-validator": { - "version": "4.2.1", - "bundled": true, - "requires": { - "ajv": "4.11.8", - "har-schema": "1.0.5" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true - }, - "hawk": { - "version": "3.1.3", - "bundled": true, - "requires": { - "boom": "2.10.1", - "cryptiles": "2.0.5", - "hoek": "2.16.3", - "sntp": "1.0.9" - } - }, - "hoek": { - "version": "2.16.3", - "bundled": true - }, - "http-signature": { - "version": "1.1.1", - "bundled": true, - "requires": { - "assert-plus": "0.2.0", - "jsprim": "1.4.1", - "sshpk": "1.13.1" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true - }, - "ini": { - "version": "1.3.5", - "bundled": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, - "is-typedarray": { - "version": "1.0.0", - "bundled": true - }, - "isarray": { - "version": "1.0.0", - "bundled": true - }, - "isstream": { - "version": "0.1.2", - "bundled": true - }, - "jsbn": { - "version": "0.1.1", - "bundled": true, - "optional": true - }, - "json-schema": { - "version": "0.2.3", - "bundled": true - }, - "json-stable-stringify": { - "version": "1.0.1", - "bundled": true, - "requires": { - "jsonify": "0.0.0" - } - }, - "json-stringify-safe": { - "version": "5.0.1", - "bundled": true - }, - "jsonify": { - "version": "0.0.0", - "bundled": true - }, - "jsprim": { - "version": "1.4.1", - "bundled": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true - } - } - }, - "mime-db": { - "version": "1.30.0", - "bundled": true - }, - "mime-types": { - "version": "2.1.17", - "bundled": true, - "requires": { - "mime-db": "1.30.0" - } - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "requires": { - "brace-expansion": "1.1.8" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "bundled": true - }, - "node-pre-gyp": { - "version": "0.6.39", - "bundled": true, - "requires": { - "detect-libc": "1.0.3", - "hawk": "3.1.3", - "mkdirp": "0.5.1", - "nopt": "4.0.1", - "npmlog": "4.1.2", - "rc": "1.2.4", - "request": "2.81.0", - "rimraf": "2.6.2", - "semver": "5.5.0", - "tar": "2.2.1", - "tar-pack": "3.4.1" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "requires": { - "abbrev": "1.1.1", - "osenv": "0.1.4" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "requires": { - "are-we-there-yet": "1.1.4", - "console-control-strings": "1.1.0", - "gauge": "2.7.4", - "set-blocking": "2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true - }, - "oauth-sign": { - "version": "0.8.2", - "bundled": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true - }, - "osenv": { - "version": "0.1.4", - "bundled": true, - "requires": { - "os-homedir": "1.0.2", - "os-tmpdir": "1.0.2" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true - }, - "performance-now": { - "version": "0.2.0", - "bundled": true - }, - "process-nextick-args": { - "version": "1.0.7", - "bundled": true - }, - "punycode": { - "version": "1.4.1", - "bundled": true - }, - "qs": { - "version": "6.4.0", - "bundled": true - }, - "rc": { - "version": "1.2.4", - "bundled": true, - "requires": { - "deep-extend": "0.4.2", - "ini": "1.3.5", - "minimist": "1.2.0", - "strip-json-comments": "2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true - } - } - }, - "readable-stream": { - "version": "2.3.3", - "bundled": true, - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "1.0.7", - "safe-buffer": "5.1.1", - "string_decoder": "1.0.3", - "util-deprecate": "1.0.2" - } - }, - "request": { - "version": "2.81.0", - "bundled": true, - "requires": { - "aws-sign2": "0.6.0", - "aws4": "1.6.0", - "caseless": "0.12.0", - "combined-stream": "1.0.5", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.1.4", - "har-validator": "4.2.1", - "hawk": "3.1.3", - "http-signature": "1.1.1", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.17", - "oauth-sign": "0.8.2", - "performance-now": "0.2.0", - "qs": "6.4.0", - "safe-buffer": "5.1.1", - "stringstream": "0.0.5", - "tough-cookie": "2.3.3", - "tunnel-agent": "0.6.0", - "uuid": "3.2.1" - } - }, - "rimraf": { - "version": "2.6.2", - "bundled": true, - "requires": { - "glob": "7.1.2" - } - }, - "safe-buffer": { - "version": "5.1.1", - "bundled": true - }, - "semver": { - "version": "5.5.0", - "bundled": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true - }, - "sntp": { - "version": "1.0.9", - "bundled": true, - "requires": { - "hoek": "2.16.3" - } - }, - "sshpk": { - "version": "1.13.1", - "bundled": true, - "requires": { - "asn1": "0.2.3", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.1", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.1", - "getpass": "0.1.7", - "jsbn": "0.1.1", - "tweetnacl": "0.14.5" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true - } - } - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - }, - "string_decoder": { - "version": "1.0.3", - "bundled": true, - "requires": { - "safe-buffer": "5.1.1" - } - }, - "stringstream": { - "version": "0.0.5", - "bundled": true - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "requires": { - "ansi-regex": "2.1.1" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true - }, - "tar": { - "version": "2.2.1", - "bundled": true, - "requires": { - "block-stream": "0.0.9", - "fstream": "1.0.11", - "inherits": "2.0.3" - } - }, - "tar-pack": { - "version": "3.4.1", - "bundled": true, - "requires": { - "debug": "2.6.9", - "fstream": "1.0.11", - "fstream-ignore": "1.0.5", - "once": "1.4.0", - "readable-stream": "2.3.3", - "rimraf": "2.6.2", - "tar": "2.2.1", - "uid-number": "0.0.6" - } - }, - "tough-cookie": { - "version": "2.3.3", - "bundled": true, - "requires": { - "punycode": "1.4.1" - } - }, - "tunnel-agent": { - "version": "0.6.0", - "bundled": true, - "requires": { - "safe-buffer": "5.1.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "bundled": true, - "optional": true - }, - "uid-number": { - "version": "0.0.6", - "bundled": true - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true - }, - "uuid": { - "version": "3.2.1", - "bundled": true - }, - "verror": { - "version": "1.10.0", - "bundled": true, - "requires": { - "assert-plus": "1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "1.3.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true - } - } - }, - "wide-align": { - "version": "1.1.2", - "bundled": true, - "requires": { - "string-width": "1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true - } - } - }, - "gtoken": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-1.2.3.tgz", - "integrity": "sha512-wQAJflfoqSgMWrSBk9Fg86q+sd6s7y6uJhIvvIPz++RElGlMtEqsdAR2oWwZ/WTEtp7P9xFbJRrT976oRgzJ/w==", - "requires": { - "google-p12-pem": "0.1.2", - "jws": "3.1.4", - "mime": "1.6.0", - "request": "2.83.0" - } - }, - "gulp": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/gulp/-/gulp-3.9.1.tgz", - "integrity": "sha1-VxzkWSjdQK9lFPxAEYZgFsE4RbQ=", - "dev": true, - "requires": { - "archy": "1.0.0", - "chalk": "1.1.3", - "deprecated": "0.0.1", - "gulp-util": "3.0.8", - "interpret": "1.0.4", - "liftoff": "2.3.0", - "minimist": "1.2.0", - "orchestrator": "0.3.8", - "pretty-hrtime": "1.0.3", - "semver": "4.3.6", - "tildify": "1.2.0", - "v8flags": "2.1.1", - "vinyl-fs": "0.3.14" - } - }, - "gulp-exit": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/gulp-exit/-/gulp-exit-0.0.2.tgz", - "integrity": "sha1-CCMTVIaDrQqwXUMNelYzMNTmE3A=", - "dev": true - }, - "gulp-header": { - "version": "1.8.9", - "resolved": "https://registry.npmjs.org/gulp-header/-/gulp-header-1.8.9.tgz", - "integrity": "sha1-yfEP7gYy2B6Tl4nG7PRaFRvzCYs=", - "dev": true, - "requires": { - "concat-with-sourcemaps": "1.0.4", - "gulp-util": "3.0.8", - "object-assign": "4.1.1", - "through2": "2.0.3" - } - }, - "gulp-istanbul": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/gulp-istanbul/-/gulp-istanbul-1.1.2.tgz", - "integrity": "sha512-53+BDhGlGNHYfeFh/mSXWhNu9wSFmE8qAEFj6ViMiWzTwI9pYxedUxMmGfigwaddsHHQxBl9TgnzUydrX84Kog==", - "dev": true, - "requires": { - "gulp-util": "3.0.8", - "istanbul": "0.4.5", - "istanbul-threshold-checker": "0.2.1", - "lodash": "4.17.4", - "through2": "2.0.3", - "vinyl-sourcemaps-apply": "0.2.1" - } - }, - "gulp-mocha": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/gulp-mocha/-/gulp-mocha-3.0.1.tgz", - "integrity": "sha1-qwyiw5QDcYF03drXUOY6Yb4X4EE=", - "dev": true, - "requires": { - "gulp-util": "3.0.8", - "mocha": "3.5.3", - "plur": "2.1.2", - "req-cwd": "1.0.1", - "temp": "0.8.3", - "through": "2.3.8" - } - }, - "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.3.3", - "replacestream": "4.0.3" - } - }, - "gulp-sourcemaps": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-1.6.0.tgz", - "integrity": "sha1-uG/zSdgBzrVuHZ59x7vLS33uYAw=", - "dev": true, - "requires": { - "convert-source-map": "1.5.0", - "graceful-fs": "4.1.11", - "strip-bom": "2.0.0", - "through2": "2.0.3", - "vinyl": "1.2.0" - }, - "dependencies": { - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "dev": true, - "requires": { - "is-utf8": "0.2.1" - } - }, - "vinyl": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", - "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", - "dev": true, - "requires": { - "clone": "1.0.2", - "clone-stats": "0.0.1", - "replace-ext": "0.0.1" - } - } - } - }, - "gulp-tslint": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/gulp-tslint/-/gulp-tslint-6.1.3.tgz", - "integrity": "sha1-cq02j0JEXyqs+v0vd/oZFm7eeVg=", - "dev": true, - "requires": { - "gulp-util": "3.0.8", - "map-stream": "0.1.0", - "through": "2.3.8" - } - }, - "gulp-typescript": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/gulp-typescript/-/gulp-typescript-3.2.3.tgz", - "integrity": "sha512-Np2sJXgtDUwIAoMtlJ9uXsVmpu1FWXlKZw164hLuo56uJa7qo5W2KZ0yAYiYH/HUsaz5L0O2toMOcLIokpFCPg==", - "dev": true, - "requires": { - "gulp-util": "3.0.8", - "source-map": "0.5.7", - "through2": "2.0.3", - "vinyl-fs": "2.4.4" - }, - "dependencies": { - "glob": { - "version": "5.0.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", - "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", - "dev": true, - "requires": { - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "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.2" - } - }, - "glob-stream": { - "version": "5.3.5", - "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-5.3.5.tgz", - "integrity": "sha1-pVZlqajM3EGRWofHAeMtTgFvrSI=", - "dev": true, - "requires": { - "extend": "3.0.1", - "glob": "5.0.15", - "glob-parent": "3.1.0", - "micromatch": "2.3.11", - "ordered-read-streams": "0.3.0", - "through2": "0.6.5", - "to-absolute-glob": "0.1.1", - "unique-stream": "2.2.1" - }, - "dependencies": { - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "dev": true, - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "0.0.1", - "string_decoder": "0.10.31" - } - }, - "through2": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", - "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", - "dev": true, - "requires": { - "readable-stream": "1.0.34", - "xtend": "4.0.1" - } - } - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "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.1" - } - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "ordered-read-streams": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-0.3.0.tgz", - "integrity": "sha1-cTfmmzKYuzQiR6G77jiByA4v14s=", - "dev": true, - "requires": { - "is-stream": "1.1.0", - "readable-stream": "2.3.3" - } - }, - "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 - }, "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "dev": true, - "requires": { - "is-utf8": "0.2.1" - } - }, - "unique-stream": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.2.1.tgz", - "integrity": "sha1-WqADz76Uxf+GbE59ZouxxNuts2k=", - "dev": true, - "requires": { - "json-stable-stringify": "1.0.1", - "through2-filter": "2.0.0" - } - }, - "vinyl": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", - "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", - "dev": true, - "requires": { - "clone": "1.0.2", - "clone-stats": "0.0.1", - "replace-ext": "0.0.1" - } - }, - "vinyl-fs": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-2.4.4.tgz", - "integrity": "sha1-vm/zJwy1Xf19MGNkDegfJddTIjk=", - "dev": true, - "requires": { - "duplexify": "3.5.1", - "glob-stream": "5.3.5", - "graceful-fs": "4.1.11", - "gulp-sourcemaps": "1.6.0", - "is-valid-glob": "0.3.0", - "lazystream": "1.0.0", - "lodash.isequal": "4.5.0", - "merge-stream": "1.0.1", - "mkdirp": "0.5.1", - "object-assign": "4.1.1", - "readable-stream": "2.3.3", - "strip-bom": "2.0.0", - "strip-bom-stream": "1.0.0", - "through2": "2.0.3", - "through2-filter": "2.0.0", - "vali-date": "1.0.0", - "vinyl": "1.2.0" - } - } - } - }, - "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.3", - "beeper": "1.1.1", - "chalk": "1.1.3", - "dateformat": "2.2.0", - "fancy-log": "1.3.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.6.2", - "minimist": "1.2.0", - "multipipe": "0.1.2", - "object-assign": "3.0.0", - "replace-ext": "0.0.1", - "through2": "2.0.3", - "vinyl": "0.5.3" - }, - "dependencies": { - "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 - } - } - }, - "gulplog": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz", - "integrity": "sha1-4oxNRdBey77YGDY86PnFkmIp/+U=", - "dev": true, - "requires": { - "glogg": "1.0.0" - } - }, - "handlebars": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz", - "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", - "dev": true, - "requires": { - "async": "1.5.2", - "optimist": "0.6.1", - "source-map": "0.4.4", - "uglify-js": "2.8.29" - }, - "dependencies": { - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", - "dev": true - }, - "source-map": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", - "dev": true, - "requires": { - "amdefine": "1.0.1" - } - } - } - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" - }, - "har-validator": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", - "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", - "requires": { - "ajv": "5.3.0", - "har-schema": "2.0.0" - } - }, - "has": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", - "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=", - "dev": true, - "requires": { - "function-bind": "1.1.1" - } - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, - "requires": { - "ansi-regex": "2.1.1" - } - }, - "has-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", - "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", - "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-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "requires": { - "get-value": "2.0.6", - "has-values": "1.0.0", - "isobject": "3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "requires": { - "is-number": "3.0.0", - "kind-of": "4.0.0" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "kind-of": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "hash-stream-validation": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/hash-stream-validation/-/hash-stream-validation-0.2.1.tgz", - "integrity": "sha1-7Mm5l7IYvluzEphii7gHhptz3NE=", - "requires": { - "through2": "2.0.3" - } - }, - "hawk": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", - "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", - "requires": { - "boom": "4.3.1", - "cryptiles": "3.1.2", - "hoek": "4.2.0", - "sntp": "2.1.0" - } - }, - "he": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", - "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", - "dev": true - }, - "hoek": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", - "integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ==" - }, - "homedir-polyfill": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz", - "integrity": "sha1-TCu8inWJmP7r9e1oWA921GdotLw=", - "dev": true, - "requires": { - "parse-passwd": "1.0.0" - } - }, - "hosted-git-info": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", - "integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==", - "dev": true - }, - "http-parser-js": { - "version": "0.4.9", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.9.tgz", - "integrity": "sha1-6hoE+2St/wJC6ZdPKX3Uw8rSceE=" - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "requires": { - "assert-plus": "1.0.0", - "jsprim": "1.4.1", - "sshpk": "1.13.1" - } - }, - "ignore": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.7.tgz", - "integrity": "sha512-YGG3ejvBNHRqu0559EOxxNFihD0AjpvHlC/pdGKd3X3ofe+CoJkYazwNJYTNebqpPKN+VVQbh4ZFn1DivMNuHA==" - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" - }, - "indexof": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", - "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "ini": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz", - "integrity": "sha1-BTfLedr1m1mhpRff9wbIbsA5Fi4=", - "dev": true - }, - "interpret": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.0.4.tgz", - "integrity": "sha1-ggzdWIuGj/sZGoCVBtbJyPISsbA=", - "dev": true - }, - "invert-kv": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" - }, - "irregular-plurals": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-1.4.0.tgz", - "integrity": "sha1-LKmwM2UREYVUEvFr5dd8YqRYp2Y=", - "dev": true - }, - "is": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/is/-/is-3.2.1.tgz", - "integrity": "sha1-0Kwq1V63sL7JJqUmb2xmKqqD3KU=" - }, - "is-absolute": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-0.2.6.tgz", - "integrity": "sha1-IN5p89uULvLYe5wto28XIjWxtes=", - "dev": true, - "requires": { - "is-relative": "0.2.1", - "is-windows": "0.2.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==", - "requires": { - "kind-of": "6.0.2" - }, - "dependencies": { - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - } - } - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" - }, - "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.1.1" - } - }, - "is-callable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.3.tgz", - "integrity": "sha1-hut1OSgF3cM69xySoO7fdO52BLI=", - "dev": true - }, - "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==", - "requires": { - "kind-of": "6.0.2" - }, - "dependencies": { - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - } - } - }, - "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=", - "dev": true + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true + } + } }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, "requires": { - "is-accessor-descriptor": "1.0.0", - "is-data-descriptor": "1.0.0", - "kind-of": "6.0.2" - }, - "dependencies": { - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - } + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" } }, - "is-dotfile": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", - "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", - "dev": true + "define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "requires": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } }, - "is-equal-shallow": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", - "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "del": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz", + "integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==", "dev": true, "requires": { - "is-primitive": "2.0.0" + "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" } }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, - "is-extglob": { + "delegates": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", - "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", "dev": true }, - "is-fullwidth-code-point": { + "detect-file": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "requires": { - "number-is-nan": "1.0.1" - } + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", + "dev": true }, - "is-glob": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", - "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==" + }, + "diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "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": { - "is-extglob": "1.0.0" + "path-type": "^4.0.0" } }, - "is-number": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", - "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, "requires": { - "kind-of": "3.2.2" + "esutils": "^2.0.2" } }, - "is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" - }, - "is-odd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-odd/-/is-odd-2.0.0.tgz", - "integrity": "sha512-OTiixgpZAT1M4NHgS5IguFp/Vz2VI3U7Goh4/HA1adtwyLtSBrxYlcSYkhpAE07s4fKEcjrFxyvtQBND4vFQyQ==", + "duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "optional": true, "requires": { - "is-number": "4.0.0" - }, - "dependencies": { - "is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==" - } + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" } }, - "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=", - "dev": true - }, - "is-path-in-cwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz", - "integrity": "sha1-ZHdYK4IU1gI0YJRWcAO+ip6sBNw=", + "each-props": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/each-props/-/each-props-3.0.0.tgz", + "integrity": "sha512-IYf1hpuWrdzse/s/YJOrFmU15lyhSzxelNVAHTEG3DtP4QsLTWZUzcUL3HMXmKQxXpa4EIrBPpwRgj0aehdvAw==", "dev": true, "requires": { - "is-path-inside": "1.0.0" + "is-plain-object": "^5.0.0", + "object.defaults": "^1.1.0" } }, - "is-path-inside": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.0.tgz", - "integrity": "sha1-/AbloWg/vaE95mev9xe7wQpI838=", + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", "dev": true, "requires": { - "path-is-inside": "1.0.2" + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" } }, - "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==", + "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": { - "isobject": "3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } + "safe-buffer": "^5.0.1" } }, - "is-posix-bracket": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", - "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", + "electron-to-chromium": { + "version": "1.4.738", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.738.tgz", + "integrity": "sha512-lwKft2CLFztD+vEIpesrOtCrko/TFnEJlHFdRhazU7Y/jx5qc4cqsocfVrBg4So4gGe9lvxnbLIoev47WMpg+A==", "dev": true }, - "is-primitive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", - "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", - "dev": true + "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==" }, - "is-regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", - "dev": true, + "end-of-stream": { + "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": { - "has": "1.0.1" + "once": "^1.4.0" } }, - "is-relative": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-0.2.1.tgz", - "integrity": "sha1-0n9MfVFtF1+2ENuEu+7yPDvJeqU=", + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", "dev": true, "requires": { - "is-unc-path": "0.1.2" + "is-arrayish": "^0.2.1" } }, - "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-ended": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.3.tgz", - "integrity": "sha1-oEc7Jnx1ZjVIa+7cfjNE5UnRUqw=" - }, - "is-symbol": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz", - "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=", - "dev": true - }, - "is-typedarray": { + "es-abstract": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.3", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.15" + } + }, + "es-define-property": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, - "is-unc-path": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-0.1.2.tgz", - "integrity": "sha1-arBTpyVzwQJQ/0FqOBTDUXivObk=", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", "dev": true, "requires": { - "unc-path-regex": "0.1.2" + "get-intrinsic": "^1.2.4" } }, - "is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", - "dev": true - }, - "is-valid-glob": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-0.3.0.tgz", - "integrity": "sha1-1LVcafUYhvm2XHDWwmItN+KfSP4=", - "dev": true - }, - "is-windows": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-0.2.0.tgz", - "integrity": "sha1-3hqm1j6indJIc3tp8f+LgALSEIw=", + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "dev": true }, - "isarray": { + "es-object-atoms": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "dev": true, + "requires": { + "es-errors": "^1.3.0" + } }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true + "es-set-tostringtag": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" + } }, - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "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==", "dev": true, "requires": { - "isarray": "1.0.0" + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" } }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "istanbul": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", - "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", - "dev": true, - "requires": { - "abbrev": "1.0.9", - "async": "1.5.2", - "escodegen": "1.8.1", - "esprima": "2.7.3", - "glob": "5.0.15", - "handlebars": "4.0.11", - "js-yaml": "3.10.0", - "mkdirp": "0.5.1", - "nopt": "3.0.6", - "once": "1.4.0", - "resolve": "1.1.7", - "supports-color": "3.2.3", - "which": "1.3.0", - "wordwrap": "1.0.0" + "es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, + "escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==" + }, + "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 + }, + "eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" }, "dependencies": { - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", - "dev": true + "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" + } }, - "glob": { - "version": "5.0.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", - "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "requires": { - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" } }, - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true }, - "supports-color": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", - "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "requires": { - "has-flag": "1.0.0" + "isexe": "^2.0.0" } } } }, - "istanbul-threshold-checker": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/istanbul-threshold-checker/-/istanbul-threshold-checker-0.2.1.tgz", - "integrity": "sha1-xdyU6PLMXNP/0zVFL4S1U8QkgzE=", + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "requires": { - "istanbul": "0.4.5", - "lodash": "4.17.4" + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" } }, - "istextorbinary": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-1.0.2.tgz", - "integrity": "sha1-rOGTVNGpoBc+/rEITOD4ewrX3s8=", + "eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true + }, + "espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "requires": { - "binaryextensions": "1.0.1", - "textextensions": "1.0.2" + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" } }, - "js-tokens": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true }, - "js-yaml": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz", - "integrity": "sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA==", + "esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", "dev": true, "requires": { - "argparse": "1.0.9", - "esprima": "4.0.0" + "estraverse": "^5.1.0" }, "dependencies": { - "esprima": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", - "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true } } }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "optional": true + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } + } }, - "json-parse-better-errors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.1.tgz", - "integrity": "sha512-xyQpxeWWMKyJps9CuGJYeng6ssI5bpqS9ltQpdVQ90t4ql6NdnxFKh95JcRt2cun/DjMVNrdjniLPuMA69xmCw==", + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true }, - "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" + "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 }, - "json-stable-stringify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", - "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==" + }, + "expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", "dev": true, "requires": { - "jsonify": "0.0.0" + "homedir-polyfill": "^1.0.1" } }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, - "json3": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", - "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", - "dev": true + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + } }, - "jsonify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", "dev": true }, - "jsonwebtoken": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.1.0.tgz", - "integrity": "sha1-xjl80uX9WD1lwAeoPce7eOaYK4M=", - "requires": { - "jws": "3.1.4", - "lodash.includes": "4.3.0", - "lodash.isboolean": "3.0.3", - "lodash.isinteger": "4.0.4", - "lodash.isnumber": "3.0.3", - "lodash.isplainobject": "4.0.6", - "lodash.isstring": "4.0.1", - "lodash.once": "4.1.1", - "ms": "2.0.0", - "xtend": "4.0.1" + "fancy-log": { + "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" } }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "farmhash": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/farmhash/-/farmhash-3.3.1.tgz", + "integrity": "sha512-XUizHanzlr/v7suBr/o85HSakOoWh6HKXZjFYl5C2+Gj0f0rkw+XTUZzrd9odDsgI9G5tRUcF4wSbKaX04T0DQ==", "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" + "node-addon-api": "^5.1.0", + "prebuild-install": "^7.1.2" } }, - "just-extend": { - "version": "1.1.27", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-1.1.27.tgz", - "integrity": "sha512-mJVp13Ix6gFo3SBAy9U/kL+oeZqzlYYYLQBwXVBlVzIsZwBqGREnOro24oC/8s8aox+rJhtZ2DiQof++IrkA+g==", + "fast-deep-equal": { + "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-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", "dev": true }, - "jwa": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.1.5.tgz", - "integrity": "sha1-oFUs4CIHQs1S4VN3SjKQXDDnVuU=", + "fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, "requires": { - "base64url": "2.0.0", - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.9", - "safe-buffer": "5.1.1" + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" } }, - "jws": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.1.4.tgz", - "integrity": "sha1-+ei5M46KhHJ31kRLFGT2GIDgUKI=", + "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", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "fast-xml-parser": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.3.6.tgz", + "integrity": "sha512-M2SovcRxD4+vC493Uc2GZVcZaj66CCJhWurC4viynVSTvrpErCShNcDz1lAho6n9REQKvL/ll4A4/fw6Y9z8nw==", + "optional": true, "requires": { - "base64url": "2.0.0", - "jwa": "1.1.5", - "safe-buffer": "5.1.1" + "strnum": "^1.0.5" } }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true + }, + "fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", "requires": { - "is-buffer": "1.1.6" + "websocket-driver": ">=0.5.1" } }, - "lazy-cache": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", - "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", - "dev": true, - "optional": true + "fclone": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/fclone/-/fclone-1.0.11.tgz", + "integrity": "sha512-GDqVQezKzRABdeqflsgMr7ktzgF9CyS+p2oe0jJqUY6izSSbhPIQJDpoU4PtGcD7VPM9xh/dVrTu6z1nwgmEGw==", + "dev": true }, - "lazystream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", - "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, "requires": { - "readable-stream": "2.3.3" + "flat-cache": "^3.0.4" } }, - "lcid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "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": { - "invert-kv": "1.0.0" + "to-regex-range": "^5.0.1" } }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", "dev": true, "requires": { - "prelude-ls": "1.1.2", - "type-check": "0.3.2" + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" } }, - "liftoff": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-2.3.0.tgz", - "integrity": "sha1-qY8v9nGD2Lp8+soQVIvX/wVQs4U=", + "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": { - "extend": "3.0.1", - "findup-sync": "0.4.3", - "fined": "1.1.0", - "flagged-respawn": "0.3.2", - "lodash.isplainobject": "4.0.6", - "lodash.isstring": "4.0.1", - "lodash.mapvalues": "4.6.0", - "rechoir": "0.6.2", - "resolve": "1.5.0" + "locate-path": "^6.0.0", + "path-exists": "^4.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=", + "findup-sync": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-5.0.0.tgz", + "integrity": "sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==", "dev": true, "requires": { - "graceful-fs": "4.1.11", - "parse-json": "4.0.0", - "pify": "3.0.0", - "strip-bom": "3.0.0" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "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 - } + "detect-file": "^1.0.0", + "is-glob": "^4.0.3", + "micromatch": "^4.0.4", + "resolve-dir": "^1.0.1" } }, - "lodash": { - "version": "4.17.4", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", - "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" - }, - "lodash._baseassign": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", - "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", + "fined": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-2.0.0.tgz", + "integrity": "sha512-OFRzsL6ZMHz5s0JrsEr+TpdGNCtrVtnuG3x1yzGNiQHT0yaDnXAj8V/lWcpJVrnoDpcwXcASxAZYbuXda2Y82A==", "dev": true, "requires": { - "lodash._basecopy": "3.0.1", - "lodash.keys": "3.1.2" + "expand-tilde": "^2.0.2", + "is-plain-object": "^5.0.0", + "object.defaults": "^1.1.0", + "object.pick": "^1.3.0", + "parse-filepath": "^1.0.2" } }, - "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._basecreate": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz", - "integrity": "sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE=", - "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=", + "firebase-token-generator": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/firebase-token-generator/-/firebase-token-generator-2.0.0.tgz", + "integrity": "sha512-EX/Rw6C0NLF6StuszW9Pn4zGUU8dw0UdHY6u8zP5t/CsbYRwWVh0CwN6INFE5U4IizZtgqbWQhcAQNkBtNkyfQ==", "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=", + "flagged-respawn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-2.0.0.tgz", + "integrity": "sha512-Gq/a6YCi8zexmGHMuJwahTGzXlAZAOsbCVKduWXC6TlLCjjFRlExMJc4GC2NYPYZ0r/brw9P7CpRgQmlPVeOoA==", "dev": true }, - "lodash._reevaluate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reevaluate/-/lodash._reevaluate-3.0.0.tgz", - "integrity": "sha1-WLx0xAZklTrgsSTYBpltrKQx4u0=", + "flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", "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 + "flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "requires": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + } }, - "lodash._root": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._root/-/lodash._root-3.0.1.tgz", - "integrity": "sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI=", + "flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, - "lodash.create": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz", - "integrity": "sha1-1/KEnw29p+BGgruM1yqwIkYd6+c=", + "flush-write-stream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", + "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", "dev": true, "requires": { - "lodash._baseassign": "3.2.0", - "lodash._basecreate": "3.0.3", - "lodash._isiterateecall": "3.0.9" + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "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" + } + }, + "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 + }, + "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" + } + } } }, - "lodash.escape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-3.2.0.tgz", - "integrity": "sha1-mV7g3BjBtIzJLv+ucaEKq1tIdpg=", + "for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", "dev": true, "requires": { - "lodash._root": "3.0.1" + "is-callable": "^1.1.3" } }, - "lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", "dev": true }, - "lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + "for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==", + "dev": true, + "requires": { + "for-in": "^1.0.1" + } }, - "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 + "foreground-child": { + "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": "^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" + } + }, + "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" + } + } + } }, - "lodash.isarray": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", - "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", "dev": true }, - "lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + "form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } }, - "lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", + "fromentries": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", + "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", "dev": true }, - "lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" - }, - "lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" - }, - "lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" - }, - "lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" }, - "lodash.keys": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", - "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", + "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": { - "lodash._getnative": "3.9.1", - "lodash.isarguments": "3.1.0", - "lodash.isarray": "3.0.4" + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" } }, - "lodash.mapvalues": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz", - "integrity": "sha1-G6+lAF3p3W9PJmaMMMo3IwzJaJw=", - "dev": true - }, - "lodash.merge": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.1.tgz", - "integrity": "sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ==" - }, - "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.template": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-3.6.2.tgz", - "integrity": "sha1-+M3sxhaaJVvpCYrosMU9N4kx0U8=", + "fs-minipass": { + "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": { - "lodash._basecopy": "3.0.1", - "lodash._basetostring": "3.0.1", - "lodash._basevalues": "3.0.0", - "lodash._isiterateecall": "3.0.9", - "lodash._reinterpolate": "3.0.0", - "lodash.escape": "3.2.0", - "lodash.keys": "3.1.2", - "lodash.restparam": "3.6.1", - "lodash.templatesettings": "3.1.1" + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "requires": { + "yallist": "^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=", + "fs-mkdirp-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-2.0.1.tgz", + "integrity": "sha512-UTOY+59K6IA94tec8Wjqm0FSh5OVudGNB0NL/P6fB3HiE3bYOY3VYBGijsnOHNkQSwC1FKkU77pmq7xp9CskLw==", "dev": true, "requires": { - "lodash._reinterpolate": "3.0.0", - "lodash.escape": "3.2.0" + "graceful-fs": "^4.2.8", + "streamx": "^2.12.0" } }, - "log-driver": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.5.tgz", - "integrity": "sha1-euTsJXMC/XkNVXyxDJcQDYV7AFY=" - }, - "lolex": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.3.1.tgz", - "integrity": "sha512-mQuW55GhduF3ppo+ZRUTz1PRjEh1hS5BbqU7d8D0ez2OKxHDod7StPPeAVKisZR5aLkHZjdGWSL42LSONUJsZw==", + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, - "long": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz", - "integrity": "sha1-2CG3E4yhy1gcFymQ7xTbIAtcR0s=" - }, - "longest": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", - "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", - "dev": true + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "optional": true }, - "lru-cache": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", - "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true }, - "make-dir": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.2.0.tgz", - "integrity": "sha512-aNUAa4UMg/UougV25bbrU4ZaaKNjJ/3/xnvg/twpmKROPdKZPZ9wGgI0opdZzO8q/zUFawoUuixuOv33eZ61Iw==", + "function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, "requires": { - "pify": "3.0.0" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" - } + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" } }, - "make-error": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.0.tgz", - "integrity": "sha1-Uq06M5zPEM5itAQLcI/nByRLi5Y=", - "dev": true - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" + "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": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "optional": true }, - "map-stream": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", - "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=", + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "dev": true, "requires": { - "object-visit": "1.0.1" + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" } }, - "memorystream": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", - "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI=", - "dev": true + "gaxios": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.5.0.tgz", + "integrity": "sha512-R9QGdv8j4/dlNoQbX3hSaK/S0rkMijqjVvW3YM06CoBdbU/VdKd159j4hePpng0KuE6Lh6JJ7UdmVGJZFcAG1w==", + "optional": true, + "requires": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + } }, - "merge-stream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-1.0.1.tgz", - "integrity": "sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE=", - "dev": true, + "gcp-metadata": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz", + "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==", + "optional": true, "requires": { - "readable-stream": "2.3.3" + "gaxios": "^6.0.0", + "json-bigint": "^1.0.0" } }, - "merge2": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.2.0.tgz", - "integrity": "sha1-D4ghUdmIsfPQdYlFQE+nPuWSPT8=", + "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 }, - "methmeth": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/methmeth/-/methmeth-1.1.0.tgz", - "integrity": "sha1-6AomYY5S9cQiKGG7dIUQvRDikIk=" - }, - "micromatch": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", - "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", - "dev": true, - "requires": { - "arr-diff": "2.0.0", - "array-unique": "0.2.1", - "braces": "1.8.5", - "expand-brackets": "0.1.5", - "extglob": "0.3.2", - "filename-regex": "2.0.1", - "is-extglob": "1.0.0", - "is-glob": "2.0.1", - "kind-of": "3.2.2", - "normalize-path": "2.1.1", - "object.omit": "2.0.1", - "parse-glob": "3.0.4", - "regex-cache": "0.4.4" - } - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + "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==" }, - "mime-db": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", - "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" + "get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true }, - "mime-types": { - "version": "2.1.17", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", - "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", + "get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, "requires": { - "mime-db": "1.30.0" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" } }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "1.1.8" - } + "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 }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "get-prop": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/get-prop/-/get-prop-0.0.10.tgz", + "integrity": "sha512-XRSGBgcIisSMLJ/dwe1y/eMm9yzLicEJKmEXA3ArBkDkCW2nyRroLOoKIz+SdxuG5SI7ym2QHaTU5ifCl7MTDg==", "dev": true }, - "mixin-deep": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", - "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", - "requires": { - "for-in": "1.0.2", - "is-extendable": "1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "2.0.4" - } - } + "get-symbol-description": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "dev": true, + "requires": { + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" } }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", "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 - } + "assert-plus": "^1.0.0" } }, - "mocha": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-3.5.3.tgz", - "integrity": "sha512-/6na001MJWEtYxHOV1WLfsmR4YIynkUEhBwzsb+fk2qmQ3iqsi258l/Q2MWHJMImAcNpZ8DEdYAK72NHoIQ9Eg==", - "dev": true, - "requires": { - "browser-stdout": "1.3.0", - "commander": "2.9.0", - "debug": "2.6.8", - "diff": "3.2.0", - "escape-string-regexp": "1.0.5", - "glob": "7.1.1", - "growl": "1.9.2", - "he": "1.1.1", - "json3": "3.3.2", - "lodash.create": "3.1.1", - "mkdirp": "0.5.1", - "supports-color": "3.1.2" + "github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, "dependencies": { - "glob": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz", - "integrity": "sha1-gFIR3wT6rxxjo2ADBs31reULLsg=", - "dev": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "supports-color": { + "minimatch": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz", - "integrity": "sha1-cqJiiU2dQIuVbKBf83su2KbiotU=", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "requires": { - "has-flag": "1.0.0" + "brace-expansion": "^1.1.7" } } } }, - "modelo": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/modelo/-/modelo-4.2.3.tgz", - "integrity": "sha512-9DITV2YEMcw7XojdfvGl3gDD8J9QjZTJ7ZOUuSAkP+F3T6rDbzMJuPktxptsdHYEvZcmXrCD3LMOhdSAEq6zKA==" - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "multipipe": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/multipipe/-/multipipe-0.1.2.tgz", - "integrity": "sha1-Ko8t33Du1WTf8tV/HhoTfZ8FB4s=", + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "requires": { - "duplexer2": "0.0.2" + "is-glob": "^4.0.1" } }, - "nan": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.9.2.tgz", - "integrity": "sha512-ltW65co7f3PQWBDbqVvaU1WtFJUsNW7sWWm4HINhbMQIyVyzIeyZ8toX5TC5eeooE6piZoaEh4cZkueSKG3KYw==" - }, - "nanomatch": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.9.tgz", - "integrity": "sha512-n8R9bS8yQ6eSXaV6jHUpKzD8gLsin02w1HSFiegwrs9E098Ylhw5jdyKPaYqvHknHaSCKTPp7C8dGCQ0q9koXA==", - "requires": { - "arr-diff": "4.0.0", - "array-unique": "0.3.2", - "define-property": "2.0.2", - "extend-shallow": "3.0.2", - "fragment-cache": "0.2.1", - "is-odd": "2.0.0", - "is-windows": "1.0.2", - "kind-of": "6.0.2", - "object.pick": "1.3.0", - "regex-not": "1.0.2", - "snapdragon": "0.8.1", - "to-regex": "3.0.2" + "glob-stream": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-8.0.2.tgz", + "integrity": "sha512-R8z6eTB55t3QeZMmU1C+Gv+t5UnNRkA55c5yo67fAVfxODxieTwsjNG7utxS/73NdP1NbDgCrhVEg2h00y4fFw==", + "dev": true, + "requires": { + "@gulpjs/to-absolute-glob": "^4.0.0", + "anymatch": "^3.1.3", + "fastq": "^1.13.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "is-negated-glob": "^1.0.0", + "normalize-path": "^3.0.0", + "streamx": "^2.12.5" }, "dependencies": { - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "requires": { - "assign-symbols": "1.0.0", - "is-extendable": "1.0.1" - } - }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, "requires": { - "is-plain-object": "2.0.4" + "is-glob": "^4.0.3" } - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" } } }, - "natives": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/natives/-/natives-1.1.0.tgz", - "integrity": "sha1-6f+EFBimsux6SV6TmYT3jxY+bjE=", - "dev": true - }, - "nise": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/nise/-/nise-1.2.0.tgz", - "integrity": "sha512-q9jXh3UNsMV28KeqI43ILz5+c3l+RiNW8mhurEwCKckuHQbL+hTJIKKTiUlCPKlgQ/OukFvSnKB/Jk3+sFbkGA==", + "glob-watcher": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-6.0.0.tgz", + "integrity": "sha512-wGM28Ehmcnk2NqRORXFOTOR064L4imSw3EeOqU5bIwUf62eXGwg89WivH6VMahL8zlQHeodzvHpXplrqzrz3Nw==", "dev": true, "requires": { - "formatio": "1.2.0", - "just-extend": "1.1.27", - "lolex": "1.6.0", - "path-to-regexp": "1.7.0", - "text-encoding": "0.6.4" - }, - "dependencies": { - "lolex": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-1.6.0.tgz", - "integrity": "sha1-OpoCg0UqR9dDnnJzG54H1zhuSfY=", - "dev": true - } + "async-done": "^2.0.0", + "chokidar": "^3.5.3" } }, - "nock": { - "version": "9.1.6", - "resolved": "https://registry.npmjs.org/nock/-/nock-9.1.6.tgz", - "integrity": "sha512-DuKF+1W/FnMO6MXIGgCIWcM95bETjBbmFdR4v7dAj1zH9a9XhOjAa//PuWh98XIXxcZt7wdiv0JlO0AA0e2kqQ==", - "dev": true, - "requires": { - "chai": "3.5.0", - "debug": "2.6.8", - "deep-equal": "1.0.1", - "json-stringify-safe": "5.0.1", - "lodash": "4.17.4", - "mkdirp": "0.5.1", - "propagate": "0.4.0", - "qs": "6.5.1", - "semver": "5.5.0" - }, - "dependencies": { - "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", - "dev": true - } + "global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "requires": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" } }, - "node-forge": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.1.tgz", - "integrity": "sha1-naYR6giYL0uUIGs760zJZl8gwwA=" + "global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", + "dev": true, + "requires": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + } }, - "nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "requires": { - "abbrev": "1.0.9" + "type-fest": "^0.20.2" } }, - "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==", + "globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", "dev": true, "requires": { - "hosted-git-info": "2.5.0", - "is-builtin-module": "1.0.0", - "semver": "4.3.6", - "validate-npm-package-license": "3.0.1" + "define-properties": "^1.1.3" } }, - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, "requires": { - "remove-trailing-separator": "1.1.0" + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" } }, - "npm-run-all": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.2.tgz", - "integrity": "sha512-Z2aRlajMK4SQ8u19ZA75NZZu7wupfCNQWdYosIi8S6FgBdGf/8Y6Hgyjdc8zU2cYmIRVCx1nM80tJPkdEd+UYg==", - "dev": true, - "requires": { - "ansi-styles": "3.2.0", - "chalk": "2.3.0", - "cross-spawn": "5.1.0", - "memorystream": "0.3.1", - "minimatch": "3.0.4", - "ps-tree": "1.1.0", - "read-pkg": "3.0.0", - "shell-quote": "1.6.1", - "string.prototype.padend": "3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", - "dev": true, - "requires": { - "color-convert": "1.9.0" - } - }, - "chalk": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", - "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", - "dev": true, - "requires": { - "ansi-styles": "3.2.0", - "escape-string-regexp": "1.0.5", - "supports-color": "4.5.0" - } - }, - "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", - "dev": true - }, - "supports-color": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", - "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", - "dev": true, - "requires": { - "has-flag": "2.0.0" - } - } + "glogg": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/glogg/-/glogg-2.2.0.tgz", + "integrity": "sha512-eWv1ds/zAlz+M1ioHsyKJomfY7jbDDPpwSkv14KQj89bycx1nvK5/2Cj/T9g7kzJcX5Bc7Yv22FjfBZS/jl94A==", + "dev": true, + "requires": { + "sparkles": "^2.1.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=" + "google-auth-library": { + "version": "9.8.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.8.0.tgz", + "integrity": "sha512-TJJXFzMlVGRlIH27gYZ6XXyPf5Y3OItsKFfefsDAafNNywYRTkei83nEO29IrYj8GtdHWU78YnW+YZdaZaXIJA==", + "optional": true, + "requires": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + } }, - "nyc": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/nyc/-/nyc-11.3.0.tgz", - "integrity": "sha512-oUu0WHt1k/JMIODvAYXX6C50Mupw2GO34P/Jdg2ty9xrLufBthHiKR2gf08aF+9S0abW1fl24R7iKRBXzibZmg==", - "dev": true, - "requires": { - "archy": "1.0.0", - "arrify": "1.0.1", - "caching-transform": "1.0.1", - "convert-source-map": "1.5.0", - "debug-log": "1.0.1", - "default-require-extensions": "1.0.0", - "find-cache-dir": "0.1.1", - "find-up": "2.1.0", - "foreground-child": "1.5.6", - "glob": "7.1.2", - "istanbul-lib-coverage": "1.1.1", - "istanbul-lib-hook": "1.1.0", - "istanbul-lib-instrument": "1.9.1", - "istanbul-lib-report": "1.1.2", - "istanbul-lib-source-maps": "1.2.2", - "istanbul-reports": "1.1.3", - "md5-hex": "1.3.0", - "merge-source-map": "1.0.4", - "micromatch": "2.3.11", - "mkdirp": "0.5.1", - "resolve-from": "2.0.0", - "rimraf": "2.6.2", - "signal-exit": "3.0.2", - "spawn-wrap": "1.3.8", - "test-exclude": "4.1.1", - "yargs": "10.0.3", - "yargs-parser": "8.0.0" - }, - "dependencies": { - "align-text": { - "version": "0.1.4", - "bundled": true, - "dev": true, - "requires": { - "kind-of": "3.2.2", - "longest": "1.0.1", - "repeat-string": "1.6.1" - } - }, - "amdefine": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "bundled": true, - "dev": true - }, - "append-transform": { - "version": "0.4.0", - "bundled": true, - "dev": true, - "requires": { - "default-require-extensions": "1.0.0" - } - }, - "archy": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "arr-diff": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "requires": { - "arr-flatten": "1.1.0" - } - }, - "arr-flatten": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "array-unique": { - "version": "0.2.1", - "bundled": true, - "dev": true - }, - "arrify": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "async": { - "version": "1.5.2", - "bundled": true, - "dev": true - }, - "babel-code-frame": { - "version": "6.26.0", - "bundled": true, - "dev": true, - "requires": { - "chalk": "1.1.3", - "esutils": "2.0.2", - "js-tokens": "3.0.2" - } - }, - "babel-generator": { - "version": "6.26.0", - "bundled": true, - "dev": true, - "requires": { - "babel-messages": "6.23.0", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "detect-indent": "4.0.0", - "jsesc": "1.3.0", - "lodash": "4.17.4", - "source-map": "0.5.7", - "trim-right": "1.0.1" - } - }, - "babel-messages": { - "version": "6.23.0", - "bundled": true, - "dev": true, - "requires": { - "babel-runtime": "6.26.0" - } - }, - "babel-runtime": { - "version": "6.26.0", - "bundled": true, - "dev": true, - "requires": { - "core-js": "2.5.1", - "regenerator-runtime": "0.11.0" - } - }, - "babel-template": { - "version": "6.26.0", - "bundled": true, - "dev": true, - "requires": { - "babel-runtime": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "lodash": "4.17.4" - } - }, - "babel-traverse": { - "version": "6.26.0", - "bundled": true, - "dev": true, - "requires": { - "babel-code-frame": "6.26.0", - "babel-messages": "6.23.0", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "debug": "2.6.9", - "globals": "9.18.0", - "invariant": "2.2.2", - "lodash": "4.17.4" - } - }, - "babel-types": { - "version": "6.26.0", - "bundled": true, - "dev": true, - "requires": { - "babel-runtime": "6.26.0", - "esutils": "2.0.2", - "lodash": "4.17.4", - "to-fast-properties": "1.0.3" - } - }, - "babylon": { - "version": "6.18.0", - "bundled": true, - "dev": true - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "brace-expansion": { - "version": "1.1.8", - "bundled": true, - "dev": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "1.8.5", - "bundled": true, - "dev": true, - "requires": { - "expand-range": "1.8.2", - "preserve": "0.2.0", - "repeat-element": "1.1.2" - } - }, - "builtin-modules": { - "version": "1.1.1", - "bundled": true, - "dev": true - }, - "caching-transform": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "requires": { - "md5-hex": "1.3.0", - "mkdirp": "0.5.1", - "write-file-atomic": "1.3.4" - } - }, - "camelcase": { - "version": "1.2.1", - "bundled": true, - "dev": true, - "optional": true - }, - "center-align": { - "version": "0.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "align-text": "0.1.4", - "lazy-cache": "1.0.4" - } - }, - "chalk": { - "version": "1.1.3", - "bundled": true, - "dev": true, - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" - } - }, - "cliui": { - "version": "2.1.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "center-align": "0.1.3", - "right-align": "0.1.3", - "wordwrap": "0.0.2" - }, - "dependencies": { - "wordwrap": { - "version": "0.0.2", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "commondir": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true - }, - "convert-source-map": { - "version": "1.5.0", - "bundled": true, - "dev": true - }, - "core-js": { - "version": "2.5.1", - "bundled": true, - "dev": true - }, - "cross-spawn": { - "version": "4.0.2", - "bundled": true, - "dev": true, - "requires": { - "lru-cache": "4.1.1", - "which": "1.3.0" - } - }, - "debug": { - "version": "2.6.9", - "bundled": true, - "dev": true, + "google-gax": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.3.3.tgz", + "integrity": "sha512-f4F2Y9X4+mqsrJuLZsuTljYuQpcBnQsCt9ScvZpdM8jGjqrcxyJi5JUiqtq0jtpdHVPzyit0N7f5t07e+kH5EA==", + "optional": true, + "requires": { + "@grpc/grpc-js": "~1.10.3", + "@grpc/proto-loader": "^0.7.0", + "@types/long": "^4.0.0", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "google-auth-library": "^9.3.0", + "node-fetch": "^2.6.1", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^2.0.0", + "protobufjs": "7.2.6", + "retry-request": "^7.0.0", + "uuid": "^9.0.1" + }, + "dependencies": { + "protobufjs": { + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.6.tgz", + "integrity": "sha512-dgJaEDDL6x8ASUZ1YqWciTRrdOuYNzoOf27oHNfdyvKqHr5i0FV7FSLU+aIeFjyFgVxrpTOtQUi0BLLBymZaBw==", + "optional": true, "requires": { - "ms": "2.0.0" + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" } - }, - "debug-log": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "decamelize": { - "version": "1.2.0", - "bundled": true, - "dev": true - }, - "default-require-extensions": { - "version": "1.0.0", - "bundled": true, + } + } + }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.3" + } + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "optional": true, + "requires": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + } + }, + "gulp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/gulp/-/gulp-5.0.0.tgz", + "integrity": "sha512-S8Z8066SSileaYw1S2N1I64IUc/myI2bqe2ihOBzO6+nKpvNSg7ZcWJt/AwF8LC/NVN+/QZ560Cb/5OPsyhkhg==", + "dev": true, + "requires": { + "glob-watcher": "^6.0.0", + "gulp-cli": "^3.0.0", + "undertaker": "^2.0.0", + "vinyl-fs": "^4.0.0" + }, + "dependencies": { + "cliui": { + "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": { - "strip-bom": "2.0.0" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" } }, - "detect-indent": { - "version": "4.0.0", - "bundled": true, + "gulp-cli": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-3.0.0.tgz", + "integrity": "sha512-RtMIitkT8DEMZZygHK2vEuLPqLPAFB4sntSxg4NoDta7ciwGZ18l7JuhCTiS5deOJi2IoK0btE+hs6R4sfj7AA==", "dev": true, "requires": { - "repeating": "2.0.1" + "@gulpjs/messages": "^1.1.0", + "chalk": "^4.1.2", + "copy-props": "^4.0.0", + "gulplog": "^2.2.0", + "interpret": "^3.1.1", + "liftoff": "^5.0.0", + "mute-stdout": "^2.0.0", + "replace-homedir": "^2.0.0", + "semver-greatest-satisfied-range": "^2.0.0", + "string-width": "^4.2.3", + "v8flags": "^4.0.0", + "yargs": "^16.2.0" } }, - "error-ex": { - "version": "1.3.1", - "bundled": true, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, "requires": { - "is-arrayish": "0.2.1" + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" } }, - "escape-string-regexp": { - "version": "1.0.5", - "bundled": true, + "yargs-parser": { + "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 + } + } + }, + "gulp-filter": { + "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": "^5.0.0", + "plugin-error": "^1.0.1", + "streamfilter": "^3.0.0", + "to-absolute-glob": "^2.0.2" + } + }, + "gulp-header": { + "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": "^1.1.0", + "lodash.template": "^4.5.0", + "map-stream": "0.0.7", + "through2": "^2.0.0" + } + }, + "gulp-typescript": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/gulp-typescript/-/gulp-typescript-5.0.1.tgz", + "integrity": "sha512-YuMMlylyJtUSHG1/wuSVTrZp60k1dMEFKYOvDf7OvbAJWrDtxxD4oZon4ancdWwzjj30ztiidhe4VXJniF0pIQ==", + "dev": true, + "requires": { + "ansi-colors": "^3.0.5", + "plugin-error": "^1.0.1", + "source-map": "^0.7.3", + "through2": "^3.0.0", + "vinyl": "^2.1.0", + "vinyl-fs": "^3.0.3" + }, + "dependencies": { + "ansi-colors": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", + "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", "dev": true }, - "esutils": { - "version": "2.0.2", - "bundled": true, + "convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "dev": true }, - "execa": { - "version": "0.7.0", - "bundled": true, + "fs-mkdirp-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz", + "integrity": "sha512-+vSd9frUnapVC2RZYfL3FCB2p3g4TBhaUmrsWlSudsGdnxIuUvBB2QM1VZeBtc49QFwrp+wQLrDs3+xxDgI5gQ==", "dev": true, "requires": { - "cross-spawn": "5.1.0", - "get-stream": "3.0.0", - "is-stream": "1.1.0", - "npm-run-path": "2.0.2", - "p-finally": "1.0.0", - "signal-exit": "3.0.2", - "strip-eof": "1.0.0" + "graceful-fs": "^4.1.11", + "through2": "^2.0.3" }, "dependencies": { - "cross-spawn": { - "version": "5.1.0", - "bundled": true, + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "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" + } + }, + "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": { - "lru-cache": "4.1.1", - "shebang-command": "1.2.0", - "which": "1.3.0" + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" } } } }, - "expand-brackets": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "requires": { - "is-posix-bracket": "0.1.1" - } - }, - "expand-range": { - "version": "1.8.2", - "bundled": true, - "dev": true, - "requires": { - "fill-range": "2.2.3" - } - }, - "extglob": { - "version": "0.3.2", - "bundled": true, - "dev": true, - "requires": { - "is-extglob": "1.0.0" - } - }, - "filename-regex": { - "version": "2.0.1", - "bundled": true, - "dev": true - }, - "fill-range": { - "version": "2.2.3", - "bundled": true, - "dev": true, - "requires": { - "is-number": "2.1.0", - "isobject": "2.1.0", - "randomatic": "1.1.7", - "repeat-element": "1.1.2", - "repeat-string": "1.6.1" - } - }, - "find-cache-dir": { - "version": "0.1.1", - "bundled": true, - "dev": true, - "requires": { - "commondir": "1.0.1", - "mkdirp": "0.5.1", - "pkg-dir": "1.0.0" - } - }, - "find-up": { - "version": "2.1.0", - "bundled": true, - "dev": true, - "requires": { - "locate-path": "2.0.0" - } - }, - "for-in": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "for-own": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "requires": { - "for-in": "1.0.2" - } - }, - "foreground-child": { - "version": "1.5.6", - "bundled": true, - "dev": true, - "requires": { - "cross-spawn": "4.0.2", - "signal-exit": "3.0.2" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "get-caller-file": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "get-stream": { - "version": "3.0.0", - "bundled": true, - "dev": true - }, - "glob": { - "version": "7.1.2", - "bundled": true, - "dev": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "glob-base": { - "version": "0.3.0", - "bundled": true, - "dev": true, - "requires": { - "glob-parent": "2.0.0", - "is-glob": "2.0.1" - } - }, "glob-parent": { - "version": "2.0.0", - "bundled": true, + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", "dev": true, "requires": { - "is-glob": "2.0.1" + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" } }, - "globals": { - "version": "9.18.0", - "bundled": true, - "dev": true - }, - "graceful-fs": { - "version": "4.1.11", - "bundled": true, - "dev": true - }, - "handlebars": { - "version": "4.0.11", - "bundled": true, - "dev": true, - "requires": { - "async": "1.5.2", - "optimist": "0.6.1", - "source-map": "0.4.4", - "uglify-js": "2.8.29" + "glob-stream": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", + "integrity": "sha512-uMbLGAP3S2aDOHUDfdoYcdIePUCfysbAd0IAoWVZbeGU/oNQ8asHVSshLDJUPWxfzj8zsCG7/XeHPHTtow0nsw==", + "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": { - "source-map": { - "version": "0.4.4", - "bundled": true, + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "requires": { - "amdefine": "1.0.1" + "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" } } } }, - "has-ansi": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "2.1.1" - } - }, - "has-flag": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "hosted-git-info": { - "version": "2.5.0", - "bundled": true, - "dev": true - }, - "imurmurhash": { - "version": "0.1.4", - "bundled": true, - "dev": true - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true - }, - "invariant": { - "version": "2.2.2", - "bundled": true, - "dev": true, - "requires": { - "loose-envify": "1.3.1" - } - }, - "invert-kv": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "is-arrayish": { - "version": "0.2.1", - "bundled": true, - "dev": true - }, - "is-buffer": { - "version": "1.1.5", - "bundled": true, - "dev": true - }, - "is-builtin-module": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "builtin-modules": "1.1.1" - } - }, - "is-dotfile": { - "version": "1.0.3", - "bundled": true, - "dev": true - }, - "is-equal-shallow": { - "version": "0.1.3", - "bundled": true, - "dev": true, - "requires": { - "is-primitive": "2.0.0" - } - }, - "is-extendable": { - "version": "0.1.1", - "bundled": true, - "dev": true - }, - "is-extglob": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "is-finite": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, "is-glob": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "requires": { - "is-extglob": "1.0.0" - } - }, - "is-number": { - "version": "2.1.0", - "bundled": true, + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", "dev": true, "requires": { - "kind-of": "3.2.2" + "is-extglob": "^2.1.0" } }, - "is-posix-bracket": { - "version": "0.1.1", - "bundled": true, - "dev": true - }, - "is-primitive": { - "version": "2.0.0", - "bundled": true, - "dev": true - }, - "is-stream": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "is-utf8": { - "version": "0.2.1", - "bundled": true, - "dev": true - }, - "isarray": { + "lead": { "version": "1.0.0", - "bundled": true, - "dev": true - }, - "isexe": { - "version": "2.0.0", - "bundled": true, - "dev": true - }, - "isobject": { - "version": "2.1.0", - "bundled": true, - "dev": true, - "requires": { - "isarray": "1.0.0" - } - }, - "istanbul-lib-coverage": { - "version": "1.1.1", - "bundled": true, - "dev": true - }, - "istanbul-lib-hook": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "requires": { - "append-transform": "0.4.0" - } - }, - "istanbul-lib-instrument": { - "version": "1.9.1", - "bundled": true, - "dev": true, - "requires": { - "babel-generator": "6.26.0", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "istanbul-lib-coverage": "1.1.1", - "semver": "5.4.1" - } - }, - "istanbul-lib-report": { - "version": "1.1.2", - "bundled": true, + "resolved": "https://registry.npmjs.org/lead/-/lead-1.0.0.tgz", + "integrity": "sha512-IpSVCk9AYvLHo5ctcIXxOBpMWUe+4TKN3VPWAKUbJikkmsGp0VrSM8IttVc32D6J4WUsiPE6aEFRNmIoF/gdow==", "dev": true, "requires": { - "istanbul-lib-coverage": "1.1.1", - "mkdirp": "0.5.1", - "path-parse": "1.0.5", - "supports-color": "3.2.3" - }, - "dependencies": { - "supports-color": { - "version": "3.2.3", - "bundled": true, - "dev": true, - "requires": { - "has-flag": "1.0.0" - } - } + "flush-write-stream": "^1.0.2" } }, - "istanbul-lib-source-maps": { - "version": "1.2.2", - "bundled": true, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", "dev": true, "requires": { - "debug": "3.1.0", - "istanbul-lib-coverage": "1.1.1", - "mkdirp": "0.5.1", - "rimraf": "2.6.2", - "source-map": "0.5.7" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "bundled": true, - "dev": true, - "requires": { - "ms": "2.0.0" - } - } + "remove-trailing-separator": "^1.0.1" } }, - "istanbul-reports": { - "version": "1.1.3", - "bundled": true, + "now-and-later": { + "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": { - "handlebars": "4.0.11" + "once": "^1.3.2" } }, - "js-tokens": { - "version": "3.0.2", - "bundled": true, - "dev": true - }, - "jsesc": { - "version": "1.3.0", - "bundled": true, + "replace-ext": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", + "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", "dev": true }, - "kind-of": { - "version": "3.2.2", - "bundled": true, + "resolve-options": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-1.1.0.tgz", + "integrity": "sha512-NYDgziiroVeDC29xq7bp/CacZERYsA9bXYd1ZmcJlF3BcrZv5pTb4NG7SjdyKDnXZ84aC4vo2u6sNKIA1LCu/A==", "dev": true, "requires": { - "is-buffer": "1.1.5" + "value-or-function": "^3.0.0" } }, - "lazy-cache": { - "version": "1.0.4", - "bundled": true, - "dev": true, - "optional": true + "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 }, - "lcid": { - "version": "1.0.0", - "bundled": true, + "source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "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==", "dev": true, "requires": { - "invert-kv": "1.0.0" + "safe-buffer": "~5.1.0" } }, - "load-json-file": { - "version": "1.1.0", - "bundled": true, + "through2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", "dev": true, "requires": { - "graceful-fs": "4.1.11", - "parse-json": "2.2.0", - "pify": "2.3.0", - "pinkie-promise": "2.0.1", - "strip-bom": "2.0.0" + "inherits": "^2.0.4", + "readable-stream": "2 || 3" } }, - "locate-path": { + "to-through": { "version": "2.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/to-through/-/to-through-2.0.0.tgz", + "integrity": "sha512-+QIz37Ly7acM4EMdw2PRN389OneM5+d844tirkGp4dPKzI5OE72V9OsbFp+CIYJDahZ41ZV05hNtcPAQUAm9/Q==", "dev": true, "requires": { - "p-locate": "2.0.0", - "path-exists": "3.0.0" + "through2": "^2.0.3" }, "dependencies": { - "path-exists": { - "version": "3.0.0", - "bundled": true, - "dev": true + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "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" + } + }, + "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" + } } } }, - "lodash": { - "version": "4.17.4", - "bundled": true, - "dev": true - }, - "longest": { - "version": "1.0.1", - "bundled": true, + "value-or-function": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-3.0.0.tgz", + "integrity": "sha512-jdBB2FrWvQC/pnPtIqcLsMaQgjhdb6B7tk1MMyTKapox+tQZbdRP4uLxu/JY0t7fbfDCUMnuelzEYv5GsxHhdg==", "dev": true }, - "loose-envify": { - "version": "1.3.1", - "bundled": true, + "vinyl": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", + "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", "dev": true, "requires": { - "js-tokens": "3.0.2" + "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" } }, - "lru-cache": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "requires": { - "pseudomap": "1.0.2", - "yallist": "2.1.2" + "vinyl-fs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-3.0.3.tgz", + "integrity": "sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng==", + "dev": true, + "requires": { + "fs-mkdirp-stream": "^1.0.0", + "glob-stream": "^6.1.0", + "graceful-fs": "^4.0.0", + "is-valid-glob": "^1.0.0", + "lazystream": "^1.0.0", + "lead": "^1.0.0", + "object.assign": "^4.0.4", + "pumpify": "^1.3.5", + "readable-stream": "^2.3.3", + "remove-bom-buffer": "^3.0.0", + "remove-bom-stream": "^1.2.0", + "resolve-options": "^1.1.0", + "through2": "^2.0.0", + "to-through": "^2.0.0", + "value-or-function": "^3.0.0", + "vinyl": "^2.0.0", + "vinyl-sourcemap": "^1.1.0" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "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" + } + }, + "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" + } + } } }, - "md5-hex": { - "version": "1.3.0", - "bundled": true, + "vinyl-sourcemap": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz", + "integrity": "sha512-NiibMgt6VJGJmyw7vtzhctDcfKch4e4n9TBeoWlirb7FMg9/1Ov9k+A5ZRAtywBpRPiyECvQRQllYM8dECegVA==", "dev": true, "requires": { - "md5-o-matic": "0.1.1" + "append-buffer": "^1.0.2", + "convert-source-map": "^1.5.0", + "graceful-fs": "^4.1.6", + "normalize-path": "^2.1.1", + "now-and-later": "^2.0.0", + "remove-bom-buffer": "^3.0.0", + "vinyl": "^2.0.0" } - }, - "md5-o-matic": { - "version": "0.1.1", - "bundled": true, + } + } + }, + "gulplog": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-2.2.0.tgz", + "integrity": "sha512-V2FaKiOhpR3DRXZuYdRLn/qiY0yI5XmqbTKrYbdemJ+xOh2d2MOweI/XFgMzd/9+1twdvMwllnZbWZNJ+BOm4A==", + "dev": true, + "requires": { + "glogg": "^2.2.0" + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "dev": true + }, + "har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "dev": true, + "requires": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", "dev": true - }, - "mem": { - "version": "1.1.0", - "bundled": true, + } + } + }, + "has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "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 + }, + "has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "requires": { + "es-define-property": "^1.0.0" + } + }, + "has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true + }, + "has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "requires": { + "has-symbols": "^1.0.3" + } + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "dev": true + }, + "hasha": { + "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": "^2.0.0", + "type-fest": "^0.8.0" + }, + "dependencies": { + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + } + } + }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "requires": { + "function-bind": "^1.1.2" + } + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "homedir-polyfill": { + "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.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-entities": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", + "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", + "optional": true + }, + "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", + "integrity": "sha512-KABKXT347AYvQoaMZg9/K+/GqW6gfB4pKCiTyMUYnosfkdkaBkrXE/cWGSLk5jvD5tiDeLFlYSHLhhPhQKbRrA==", + "dev": true, + "requires": { + "buffer": "^4.9.1", + "concat-stream": "^1.5.1", + "get-prop": "0.0.10", + "minimist": "^1.2.0", + "stream-buffers": "^3.0.0" + }, + "dependencies": { + "buffer": { + "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": { - "mimic-fn": "1.1.0" + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" } - }, - "merge-source-map": { - "version": "1.0.4", - "bundled": true, - "dev": true, + } + } + }, + "http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==" + }, + "http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "optional": true, + "requires": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "dependencies": { + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "optional": true, "requires": { - "source-map": "0.5.7" + "debug": "4" } - }, - "micromatch": { - "version": "2.3.11", - "bundled": true, + } + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "optional": true, + "requires": { + "agent-base": "^7.0.2", + "debug": "4" + } + }, + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, + "idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "dev": true + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "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", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "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", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "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.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "internal-slot": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "dev": true, + "requires": { + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + } + }, + "interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true + }, + "is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "dev": true, + "requires": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + } + }, + "is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "requires": { + "has-bigints": "^1.0.1" + } + }, + "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": { + "binary-extensions": "^2.0.0" + } + }, + "is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true + }, + "is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "requires": { + "hasown": "^2.0.0" + } + }, + "is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, + "requires": { + "is-typed-array": "^1.1.13" + } + }, + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-extendable": { + "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" + }, + "dependencies": { + "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==", "dev": true, "requires": { - "arr-diff": "2.0.0", - "array-unique": "0.2.1", - "braces": "1.8.5", - "expand-brackets": "0.1.5", - "extglob": "0.3.2", - "filename-regex": "2.0.1", - "is-extglob": "1.0.0", - "is-glob": "2.0.1", - "kind-of": "3.2.2", - "normalize-path": "2.1.1", - "object.omit": "2.0.1", - "parse-glob": "3.0.4", - "regex-cache": "0.4.4" + "isobject": "^3.0.1" } - }, - "mimic-fn": { - "version": "1.1.0", - "bundled": true, + } + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": 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==" + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-negated-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", + "integrity": "sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug==", + "dev": true + }, + "is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "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 + }, + "is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-path-cwd": { + "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-inside": { + "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", + "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": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true + }, + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "dev": true, + "requires": { + "is-unc-path": "^1.0.0" + } + }, + "is-shared-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "dev": true, + "requires": { + "call-bind": "^1.0.7" + } + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" + }, + "is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "is-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dev": true, + "requires": { + "which-typed-array": "^1.1.14" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true + }, + "is-unc-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "dev": true, + "requires": { + "unc-path-regex": "^0.1.2" + } + }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==", + "dev": true + }, + "is-valid-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", + "integrity": "sha512-AhiROmoEFDSsjx8hW+5sGwgKVIORcXnrlAx/R0ZSeaPw70Vw0CqkGBBhHGL58Uox2eXnU1AnvXJl1XlyedO5bA==", + "dev": true + }, + "is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true + }, + "istanbul-lib-hook": { + "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": "^2.0.0" + } + }, + "istanbul-lib-instrument": { + "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/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, + } + } + }, + "istanbul-lib-processinfo": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz", + "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==", + "dev": true, + "requires": { + "archy": "^1.0.0", + "cross-spawn": "^7.0.3", + "istanbul-lib-coverage": "^3.2.0", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "uuid": "^8.3.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": { - "brace-expansion": "1.1.8" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" } }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, + "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": { - "minimist": "0.0.8" + "aggregate-error": "^3.0.0" } }, - "ms": { - "version": "2.0.0", - "bundled": true, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "dev": true }, - "normalize-package-data": { - "version": "2.4.0", - "bundled": true, - "dev": true, - "requires": { - "hosted-git-info": "2.5.0", - "is-builtin-module": "1.0.0", - "semver": "5.4.1", - "validate-npm-package-license": "3.0.1" - } - }, - "normalize-path": { - "version": "2.1.1", - "bundled": true, - "dev": true, - "requires": { - "remove-trailing-separator": "1.1.0" - } - }, - "npm-run-path": { + "which": { "version": "2.0.2", - "bundled": true, - "dev": true, - "requires": { - "path-key": "2.0.1" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true - }, - "object.omit": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "requires": { - "for-own": "0.1.5", - "is-extendable": "0.1.1" - } - }, - "once": { - "version": "1.4.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "requires": { - "wrappy": "1.0.2" + "isexe": "^2.0.0" } - }, - "optimist": { - "version": "0.6.1", - "bundled": true, + } + } + }, + "istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, "requires": { - "minimist": "0.0.8", - "wordwrap": "0.0.3" + "semver": "^7.5.3" } }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "os-locale": { - "version": "2.1.0", - "bundled": 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": { - "execa": "0.7.0", - "lcid": "1.0.0", - "mem": "1.1.0" + "has-flag": "^4.0.0" } - }, - "p-finally": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "p-limit": { - "version": "1.1.0", - "bundled": true, + } + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + } + }, + "istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", + "dev": true + }, + "jose": { + "version": "4.15.5", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.5.tgz", + "integrity": "sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg==" + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true - }, - "p-locate": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "requires": { - "p-limit": "1.1.0" - } - }, - "parse-glob": { - "version": "3.0.4", - "bundled": true, - "dev": true, + } + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "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" + } + }, + "json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "dependencies": { + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", "requires": { - "glob-base": "0.3.0", - "is-dotfile": "1.0.3", - "is-extglob": "1.0.0", - "is-glob": "2.0.1" + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" } }, - "parse-json": { - "version": "2.2.0", - "bundled": true, - "dev": true, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", "requires": { - "error-ex": "1.3.1" + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" } - }, - "path-exists": { - "version": "2.1.0", - "bundled": true, - "dev": true, + } + } + }, + "jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, + "just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true + }, + "jwa": { + "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", + "safe-buffer": "^5.0.1" + } + }, + "jwks-rsa": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.1.0.tgz", + "integrity": "sha512-v7nqlfezb9YfHHzYII3ef2a2j1XnGeSE/bK3WfumaYCqONAIstJbrEGapz4kadScZzEt7zYCN7bucj8C0Mv/Rg==", + "requires": { + "@types/express": "^4.17.17", + "@types/jsonwebtoken": "^9.0.2", + "debug": "^4.3.4", + "jose": "^4.14.6", + "limiter": "^1.1.5", + "lru-memoizer": "^2.2.0" + }, + "dependencies": { + "@types/jsonwebtoken": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.6.tgz", + "integrity": "sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==", "requires": { - "pinkie-promise": "2.0.1" + "@types/node": "*" } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "path-key": { - "version": "2.0.1", - "bundled": true, - "dev": true - }, - "path-parse": { - "version": "1.0.5", - "bundled": true, - "dev": true - }, - "path-type": { - "version": "1.1.0", - "bundled": true, + } + } + }, + "jws": { + "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" + } + }, + "keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "requires": { + "json-buffer": "3.0.1" + } + }, + "kind-of": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", + "integrity": "sha512-aUH6ElPnMGon2/YkxRIigV32MOpTVcoXQ1Oo8aYn40s+sJ3j+0gFZsT8HKDcxNy7Fi9zuquWtGaGAahOdv5p/g==", + "dev": true + }, + "last-run": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/last-run/-/last-run-2.0.0.tgz", + "integrity": "sha512-j+y6WhTLN4Itnf9j5ZQos1BGPCS8DAwmgMroR3OzfxAsBxam0hMw7J8M3KqZl0pLQJ1jNnwIexg5DYpC/ctwEQ==", + "dev": true + }, + "lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "requires": { + "readable-stream": "^2.0.5" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "requires": { - "graceful-fs": "4.1.11", - "pify": "2.3.0", - "pinkie-promise": "2.0.1" + "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" } }, - "pify": { - "version": "2.3.0", - "bundled": true, - "dev": true - }, - "pinkie": { - "version": "2.0.4", - "bundled": true, + "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 }, - "pinkie-promise": { - "version": "2.0.1", - "bundled": 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==", "dev": true, "requires": { - "pinkie": "2.0.4" + "safe-buffer": "~5.1.0" } - }, - "pkg-dir": { - "version": "1.0.0", - "bundled": true, - "dev": true, + } + } + }, + "lead": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lead/-/lead-4.0.0.tgz", + "integrity": "sha512-DpMa59o5uGUWWjruMp71e6knmwKU3jRBBn1kjuLWN9EeIOxNeSAwvHf03WIl8g/ZMR2oSQC9ej3yeLBwdDc/pg==", + "dev": true + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "liftoff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-5.0.0.tgz", + "integrity": "sha512-a5BQjbCHnB+cy+gsro8lXJ4kZluzOijzJ1UVVfyJYZC+IP2pLv1h4+aysQeKuTmyO8NAqfyQAk4HWaP/HjcKTg==", + "dev": true, + "requires": { + "extend": "^3.0.2", + "findup-sync": "^5.0.0", + "fined": "^2.0.0", + "flagged-respawn": "^2.0.0", + "is-plain-object": "^5.0.0", + "rechoir": "^0.8.0", + "resolve": "^1.20.0" + } + }, + "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": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "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": { + "p-locate": "^5.0.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==", + "dev": true + }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "optional": true + }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + }, + "lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", + "dev": true + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "dev": true + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "lodash.template": { + "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._reinterpolate": "^3.0.0", + "lodash.templatesettings": "^4.0.0" + } + }, + "lodash.templatesettings": { + "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" + } + }, + "log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + } + }, + "long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, + "loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "requires": { + "get-func-name": "^2.0.1" + } + }, + "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==", + "requires": { + "yallist": "^4.0.0" + } + }, + "lru-memoizer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.2.0.tgz", + "integrity": "sha512-QfOZ6jNkxCcM/BkIPnFsqDhtrazLRsghi9mBwFAzol5GCvj4EkFT899Za3+QwikCg5sRX8JstioBDwOxEyzaNw==", + "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": "sha512-uQw9OqphAGiZhkuPlpFGmdTU2tEuhxTourM/19qGJrxBPHAr/f8BT1a0i/lOclESnGatdJG/UCkP9kZB/Lh1iw==", "requires": { - "find-up": "1.1.2" - }, - "dependencies": { - "find-up": { - "version": "1.1.2", - "bundled": true, - "dev": true, - "requires": { - "path-exists": "2.1.0", - "pinkie-promise": "2.0.1" - } - } + "pseudomap": "^1.0.1", + "yallist": "^2.0.0" } }, - "preserve": { - "version": "0.2.0", - "bundled": true, - "dev": true - }, - "pseudomap": { - "version": "1.0.2", - "bundled": true, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==" + } + } + }, + "make-dir": { + "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==", + "dev": true, + "requires": { + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true - }, - "randomatic": { - "version": "1.1.7", - "bundled": true, - "dev": true, - "requires": { - "is-number": "3.0.0", - "kind-of": "4.0.0" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "bundled": true, - "dev": true, - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "bundled": true, - "dev": true, - "requires": { - "is-buffer": "1.1.5" - } - } - } - }, - "kind-of": { - "version": "4.0.0", - "bundled": true, - "dev": true, - "requires": { - "is-buffer": "1.1.5" - } - } - } - }, - "read-pkg": { - "version": "1.1.0", - "bundled": true, + } + } + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", + "dev": true + }, + "map-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz", + "integrity": "sha512-C0X0KQmGm3N2ftbTGBhSyuydQ+vV1LC3f3zPvT3RXHXNZrvfPZcoXp/N5DOa8vedX/rTMm2CjTtivFg2STJMRQ==", + "dev": true + }, + "memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "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": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "optional": true + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" + }, + "minimatch": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", + "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" + }, + "minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", "dev": true, "requires": { - "load-json-file": "1.1.0", - "normalize-package-data": "2.4.0", - "path-type": "1.1.0" + "yallist": "^4.0.0" } + } + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, + "mocha": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.4.0.tgz", + "integrity": "sha512-eqhGB8JKapEYcC4ytX/xrzKforgEc3j1pGlAXVy3eRwrtAy5/nIfT1SvgGzfN0XZZxeLq0aQWkOUAmqIJiv+bA==", + "dev": true, + "requires": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "8.1.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "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 }, - "read-pkg-up": { - "version": "1.0.1", - "bundled": true, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "requires": { - "find-up": "1.1.2", - "read-pkg": "1.1.0" - }, - "dependencies": { - "find-up": { - "version": "1.1.2", - "bundled": true, - "dev": true, - "requires": { - "path-exists": "2.1.0", - "pinkie-promise": "2.0.1" - } - } + "balanced-match": "^1.0.0" } }, - "regenerator-runtime": { - "version": "0.11.0", - "bundled": true, - "dev": true - }, - "regex-cache": { - "version": "0.4.4", - "bundled": true, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", "dev": true, "requires": { - "is-equal-shallow": "0.1.3" + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" } }, - "remove-trailing-separator": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "repeat-element": { - "version": "1.1.2", - "bundled": true, - "dev": true - }, - "repeat-string": { - "version": "1.6.1", - "bundled": true, - "dev": true - }, - "repeating": { - "version": "2.0.1", - "bundled": true, + "cliui": { + "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": { - "is-finite": "1.0.2" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" } }, - "require-directory": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "require-main-filename": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "resolve-from": { - "version": "2.0.0", - "bundled": true, - "dev": true - }, - "right-align": { - "version": "0.1.3", - "bundled": true, + "glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", "dev": true, - "optional": true, "requires": { - "align-text": "0.1.4" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" } }, - "rimraf": { - "version": "2.6.2", - "bundled": true, + "minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", "dev": true, "requires": { - "glob": "7.1.2" + "brace-expansion": "^2.0.1" } }, - "semver": { - "version": "5.4.1", - "bundled": true, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, - "set-blocking": { - "version": "2.0.0", - "bundled": 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 }, - "shebang-command": { - "version": "1.2.0", - "bundled": true, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, "requires": { - "shebang-regex": "1.0.0" + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" } }, - "shebang-regex": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true - }, - "slide": { - "version": "1.1.6", - "bundled": true, + "yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", "dev": true - }, - "source-map": { - "version": "0.5.7", - "bundled": true, + } + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "multimatch": { + "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", + "array-differ": "^3.0.0", + "array-union": "^2.1.0", + "arrify": "^2.0.1", + "minimatch": "^3.0.4" + } + }, + "mute-stdout": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-2.0.0.tgz", + "integrity": "sha512-32GSKM3Wyc8dg/p39lWPKYu8zci9mJFzV1Np9Of0ZEpe6Fhssn/FbI7ywAMd40uX+p3ZKh3T5EeCFv81qS3HmQ==", + "dev": true + }, + "mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "requires": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "nise": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-6.0.0.tgz", + "integrity": "sha512-K8ePqo9BFvN31HXwEtTNGzgrPpmvgciDsFz8aztFjt4LqKO/JeFD8tBOeuDiCMXrIl/m1YvfH8auSpxfaD09wg==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/text-encoding": "^0.7.2", + "just-extend": "^6.2.0", + "path-to-regexp": "^6.2.1" + } + }, + "nock": { + "version": "13.5.4", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.5.4.tgz", + "integrity": "sha512-yAyTfdeNJGGBFxWdzSKCBYxs5FxLbCg5X5Q4ets974hcQzG1+qCxvIyOo4j2Ry6MUlhWVMX4OoYDefAIIwupjw==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "propagate": "^2.0.0" + } + }, + "node-abi": { + "version": "3.58.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.58.0.tgz", + "integrity": "sha512-pXY1jnGf5T7b8UNzWzIqf0EkX4bx/w8N2AvwlGnk2SYYA/kzDVPaH0Dh0UG4EwxBB5eKOIZKPr8VAHSHL1DPGg==", + "requires": { + "semver": "^7.3.5" + } + }, + "node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" + }, + "node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==" + }, + "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": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, + "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": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "normalize-package-data": { + "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", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true - }, - "spawn-wrap": { - "version": "1.3.8", - "bundled": true, + } + } + }, + "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 + }, + "now-and-later": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-3.0.0.tgz", + "integrity": "sha512-pGO4pzSdaxhWTGkfSfHx3hVzJVslFPwBp2Myq9MYN/ChfJZF87ochMAXnvz6/58RJSf5ik2q9tXprBBrk2cpcg==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "npm-run-all": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", + "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "memorystream": "^0.3.1", + "minimatch": "^3.0.4", + "pidtree": "^0.3.0", + "read-pkg": "^3.0.0", + "shell-quote": "^1.6.1", + "string.prototype.padend": "^3.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": { - "foreground-child": "1.5.6", - "mkdirp": "0.5.1", - "os-homedir": "1.0.2", - "rimraf": "2.6.2", - "signal-exit": "3.0.2", - "which": "1.3.0" + "color-convert": "^1.9.0" } }, - "spdx-correct": { - "version": "1.0.2", - "bundled": 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": { - "spdx-license-ids": "1.2.2" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" } }, - "spdx-expression-parse": { - "version": "1.0.4", - "bundled": true, - "dev": true - }, - "spdx-license-ids": { - "version": "1.2.2", - "bundled": true, - "dev": true - }, - "string-width": { - "version": "2.1.1", - "bundled": true, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, "requires": { - "is-fullwidth-code-point": "2.0.0", - "strip-ansi": "4.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "bundled": true, - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "bundled": true, - "dev": true - }, - "strip-ansi": { - "version": "4.0.0", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "3.0.0" - } - } + "color-name": "1.1.3" } }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "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==", "dev": true, "requires": { - "ansi-regex": "2.1.1" + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" } }, - "strip-bom": { - "version": "2.0.0", - "bundled": true, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true + }, + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", "dev": true, "requires": { - "is-utf8": "0.2.1" + "shebang-regex": "^1.0.0" } }, - "strip-eof": { + "shebang-regex": { "version": "1.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", "dev": true }, "supports-color": { - "version": "2.0.0", - "bundled": true, + "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" + } + } + } + }, + "npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "dev": true, + "requires": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "nyc": { + "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": "^2.0.0", + "test-exclude": "^6.0.0", + "yargs": "^15.0.2" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true }, - "test-exclude": { - "version": "4.1.1", - "bundled": true, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", "dev": true, "requires": { - "arrify": "1.0.1", - "micromatch": "2.3.11", - "object-assign": "4.1.1", - "read-pkg-up": "1.0.1", - "require-main-filename": "1.0.1" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" } }, - "to-fast-properties": { - "version": "1.0.3", - "bundled": true, + "convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "dev": true }, - "trim-right": { - "version": "1.0.1", - "bundled": true, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", "dev": true }, - "uglify-js": { - "version": "2.8.29", - "bundled": 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, - "optional": true, "requires": { - "source-map": "0.5.7", - "uglify-to-browserify": "1.0.2", - "yargs": "3.10.0" - }, - "dependencies": { - "yargs": { - "version": "3.10.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "camelcase": "1.2.1", - "cliui": "2.1.0", - "decamelize": "1.2.0", - "window-size": "0.1.0" - } - } + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" } }, - "uglify-to-browserify": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "validate-npm-package-license": { - "version": "3.0.1", - "bundled": true, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, "requires": { - "spdx-correct": "1.0.2", - "spdx-expression-parse": "1.0.4" + "p-locate": "^4.1.0" } }, - "which": { - "version": "1.3.0", - "bundled": true, + "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": { - "isexe": "2.0.0" + "p-try": "^2.0.0" } }, - "which-module": { - "version": "2.0.0", - "bundled": true, - "dev": true - }, - "window-size": { - "version": "0.1.0", - "bundled": true, + "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==", "dev": true, - "optional": true - }, - "wordwrap": { - "version": "0.0.3", - "bundled": true, - "dev": true + "requires": { + "p-limit": "^2.2.0" + } }, - "wrap-ansi": { - "version": "2.1.0", - "bundled": true, + "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": { - "string-width": "1.0.2", - "strip-ansi": "3.0.1" - }, - "dependencies": { - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - } + "aggregate-error": "^3.0.0" } }, - "wrappy": { - "version": "1.0.2", - "bundled": 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 }, - "write-file-atomic": { - "version": "1.3.4", - "bundled": 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==", "dev": true, "requires": { - "graceful-fs": "4.1.11", - "imurmurhash": "0.1.4", - "slide": "1.1.6" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" } }, "y18n": { - "version": "3.2.1", - "bundled": true, - "dev": true - }, - "yallist": { - "version": "2.1.2", - "bundled": true, + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", "dev": true }, "yargs": { - "version": "10.0.3", - "bundled": true, - "dev": true, - "requires": { - "cliui": "3.2.0", - "decamelize": "1.2.0", - "find-up": "2.1.0", - "get-caller-file": "1.0.2", - "os-locale": "2.1.0", - "require-directory": "2.1.1", - "require-main-filename": "1.0.1", - "set-blocking": "2.0.0", - "string-width": "2.1.1", - "which-module": "2.0.0", - "y18n": "3.2.1", - "yargs-parser": "8.0.0" - }, - "dependencies": { - "cliui": { - "version": "3.2.0", - "bundled": true, - "dev": true, - "requires": { - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wrap-ansi": "2.1.0" - }, - "dependencies": { - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - } - } - } + "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": "^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": "8.0.0", - "bundled": true, + "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": "4.1.0" - }, - "dependencies": { - "camelcase": { - "version": "4.1.0", - "bundled": true, - "dev": true - } + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" } } } }, "oauth-sign": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "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=", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "dev": true }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "requires": { - "copy-descriptor": "0.1.1", - "define-property": "0.2.5", - "kind-of": "3.2.2" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "0.1.6" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "requires": { - "kind-of": "3.2.2" - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "requires": { - "kind-of": "3.2.2" - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "requires": { - "is-accessor-descriptor": "0.1.6", - "is-data-descriptor": "0.1.4", - "kind-of": "5.1.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" - } - } - } - } + "object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "optional": true + }, + "object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "dev": true }, "object-keys": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz", - "integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=" + "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", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dev": true, "requires": { - "isobject": "3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" } }, "object.defaults": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", - "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=", - "dev": true, - "requires": { - "array-each": "1.0.1", - "array-slice": "1.0.0", - "for-own": "1.0.0", - "isobject": "3.0.1" - }, - "dependencies": { - "for-own": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", - "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", - "dev": true, - "requires": { - "for-in": "1.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "object.omit": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", - "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "integrity": "sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA==", "dev": true, "requires": { - "for-own": "0.1.5", - "is-extendable": "0.1.1" + "array-each": "^1.0.1", + "array-slice": "^1.0.0", + "for-own": "^1.0.0", + "isobject": "^3.0.0" } }, "object.pick": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", + "dev": true, "requires": { - "isobject": "3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } + "isobject": "^3.0.1" } }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1.0.2" - } - }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "dev": true, + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "requires": { - "minimist": "0.0.10", - "wordwrap": "0.0.3" - }, - "dependencies": { - "minimist": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", - "dev": true - }, - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", - "dev": true - } + "wrappy": "1" } }, "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "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", - "wordwrap": "1.0.0" + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" } }, - "optjs": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/optjs/-/optjs-3.2.2.tgz", - "integrity": "sha1-aabOicRCpEQDFBrS+bNwvVu29O4=" - }, - "orchestrator": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/orchestrator/-/orchestrator-0.3.8.tgz", - "integrity": "sha1-FOfp4nZPcxX7rBhOUGx6pt+UrX4=", + "ordered-read-streams": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", + "integrity": "sha512-Z87aSjx3r5c0ZB7bcJqIgIRX5bxR7A4aSzvIbaxd0oTkWBCOoKfuGHiKj60CHVUgg1Phm5yMZzBdt8XqRs73Mw==", "dev": true, "requires": { - "end-of-stream": "0.1.5", - "sequencify": "0.0.7", - "stream-consume": "0.1.0" + "readable-stream": "^2.0.1" }, "dependencies": { - "end-of-stream": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-0.1.5.tgz", - "integrity": "sha1-jhdyBsPICDfYVjLouTWd/osvbq8=", + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "requires": { - "once": "1.3.3" + "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" } }, - "once": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", - "integrity": "sha1-suJhVXzkwxTsgwTz+oJmPkKXyiA=", + "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 + }, + "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": { - "wrappy": "1.0.2" + "safe-buffer": "~5.1.0" } } } }, - "ordered-read-streams": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-0.1.0.tgz", - "integrity": "sha1-/VZamvjrRHO6abbtijQ1LLVS8SY=", - "dev": true + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "requires": { + "yocto-queue": "^0.1.0" + } }, - "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 + "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" + } }, - "os-locale": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", - "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "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": { - "lcid": "1.0.0" + "aggregate-error": "^3.0.0" } }, - "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=", + "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==", "dev": true }, - "parse-filepath": { + "package-hash": { + "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": "^5.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + } + }, + "parent-module": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.1.tgz", - "integrity": "sha1-FZ1hVdQ5BNFsEO9piRHaHpGWm3M=", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "requires": { - "is-absolute": "0.2.6", - "map-cache": "0.2.2", - "path-root": "0.1.1" + "callsites": "^3.0.0" } }, - "parse-glob": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", - "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "parse-filepath": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", + "integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==", "dev": true, "requires": { - "glob-base": "0.3.0", - "is-dotfile": "1.0.3", - "is-extglob": "1.0.0", - "is-glob": "2.0.1" + "is-absolute": "^1.0.0", + "map-cache": "^0.2.0", + "path-root": "^0.1.1" } }, "parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", "dev": true, "requires": { - "error-ex": "1.3.1", - "json-parse-better-errors": "1.0.1" + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" } }, + "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", - "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", + "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", "dev": true }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" - }, "path-dirname": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=" + "integrity": "sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==", + "dev": true + }, + "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 }, "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=" + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "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=", + "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 }, "path-parse": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", - "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, "path-root": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", - "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=", + "integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==", "dev": true, "requires": { - "path-root-regex": "0.1.2" + "path-root-regex": "^0.1.0" } }, "path-root-regex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", - "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=", + "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==", "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=", - "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 - } - } + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", + "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==", + "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==", - "requires": { - "pify": "3.0.0" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" - } - } + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true }, - "pause-stream": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", - "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=", - "dev": true, - "requires": { - "through": "2.3.8" - } + "pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", "dev": true }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", "dev": true }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "dev": true, - "requires": { - "pinkie": "2.0.4" - } - }, - "plur": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/plur/-/plur-2.1.2.tgz", - "integrity": "sha1-dIJFLBoPUI4+NE6uwxLJHCncZVo=", - "dev": true, - "requires": { - "irregular-plurals": "1.4.0" - } - }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" - }, - "power-assert": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/power-assert/-/power-assert-1.4.4.tgz", - "integrity": "sha1-kpXqdDcZb1pgH95CDwQmMRhtdRc=", - "requires": { - "define-properties": "1.1.2", - "empower": "1.2.3", - "power-assert-formatter": "1.4.1", - "universal-deep-strict-equal": "1.2.2", - "xtend": "4.0.1" - } + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true }, - "power-assert-context-formatter": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/power-assert-context-formatter/-/power-assert-context-formatter-1.1.1.tgz", - "integrity": "sha1-7bo1LT7YpgMRTWZyZazOYNaJzN8=", - "requires": { - "core-js": "2.5.3", - "power-assert-context-traversal": "1.1.1" - } + "pidtree": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", + "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==", + "dev": true }, - "power-assert-context-reducer-ast": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/power-assert-context-reducer-ast/-/power-assert-context-reducer-ast-1.1.2.tgz", - "integrity": "sha1-SEqZ4m9Jc/+IMuXFzHVnAuYJQXQ=", - "requires": { - "acorn": "4.0.13", - "acorn-es7-plugin": "1.1.7", - "core-js": "2.5.3", - "espurify": "1.7.0", - "estraverse": "4.2.0" - }, - "dependencies": { - "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=" - } - } + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true }, - "power-assert-context-traversal": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/power-assert-context-traversal/-/power-assert-context-traversal-1.1.1.tgz", - "integrity": "sha1-iMq8oNE7Y1nwfT0+ivppkmRXftk=", + "pkg-dir": { + "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": { - "core-js": "2.5.3", - "estraverse": "4.2.0" + "find-up": "^4.0.0" }, "dependencies": { - "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=" + "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" + } + }, + "locate-path": { + "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": "^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==", + "dev": 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==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } } } }, - "power-assert-formatter": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/power-assert-formatter/-/power-assert-formatter-1.4.1.tgz", - "integrity": "sha1-XcEl7VCj37HdomwZNH879Y7CiEo=", - "requires": { - "core-js": "2.5.3", - "power-assert-context-formatter": "1.1.1", - "power-assert-context-reducer-ast": "1.1.2", - "power-assert-renderer-assertion": "1.1.1", - "power-assert-renderer-comparison": "1.1.1", - "power-assert-renderer-diagram": "1.1.2", - "power-assert-renderer-file": "1.1.1" - } - }, - "power-assert-renderer-assertion": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/power-assert-renderer-assertion/-/power-assert-renderer-assertion-1.1.1.tgz", - "integrity": "sha1-y/wOd+AIao+Wrz8djme57n4ozpg=", - "requires": { - "power-assert-renderer-base": "1.1.1", - "power-assert-util-string-width": "1.1.1" - } - }, - "power-assert-renderer-base": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/power-assert-renderer-base/-/power-assert-renderer-base-1.1.1.tgz", - "integrity": "sha1-lqZQxv0F7hvB9mtUrWFELIs/Y+s=" - }, - "power-assert-renderer-comparison": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/power-assert-renderer-comparison/-/power-assert-renderer-comparison-1.1.1.tgz", - "integrity": "sha1-10Odl9hRVr5OMKAPL7WnJRTOPAg=", - "requires": { - "core-js": "2.5.3", - "diff-match-patch": "1.0.0", - "power-assert-renderer-base": "1.1.1", - "stringifier": "1.3.0", - "type-name": "2.0.2" - } - }, - "power-assert-renderer-diagram": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/power-assert-renderer-diagram/-/power-assert-renderer-diagram-1.1.2.tgz", - "integrity": "sha1-ZV+PcRk1qbbVQbhjJ2VHF8Y3qYY=", + "plugin-error": { + "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": { - "core-js": "2.5.3", - "power-assert-renderer-base": "1.1.1", - "power-assert-util-string-width": "1.1.1", - "stringifier": "1.3.0" + "ansi-colors": "^1.0.1", + "arr-diff": "^4.0.0", + "arr-union": "^3.1.0", + "extend-shallow": "^3.0.2" } }, - "power-assert-renderer-file": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/power-assert-renderer-file/-/power-assert-renderer-file-1.1.1.tgz", - "integrity": "sha1-o34rvReMys0E5427eckv40kzxec=", - "requires": { - "power-assert-renderer-base": "1.1.1" - } + "possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true }, - "power-assert-util-string-width": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/power-assert-util-string-width/-/power-assert-util-string-width-1.1.1.tgz", - "integrity": "sha1-vmWet5N/3S5smncmjar2S9W3xZI=", - "requires": { - "eastasianwidth": "0.1.1" + "prebuild-install": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", + "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", + "requires": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" } }, "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, - "preserve": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", - "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, - "pretty-hrtime": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", - "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", + "process-nextick-args": { + "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==", "dev": true }, - "process-nextick-args": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + "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" + } }, "promise-polyfill": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-7.1.0.tgz", - "integrity": "sha512-P6NJ2wU/8fac44ENORsuqT8TiolKGB2u0fEClPtXezn7w5cmLIjM/7mhPlTebke2EPr6tmqZbXvnX0TxwykGrg==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-6.1.0.tgz", + "integrity": "sha512-g0LWaH0gFsxovsU7R5LrrhHhWAWiHRnh1GPrhXnPgYsDkIqjRYUYSZEsej/wtleDrz5xVSIDbeKfidztp2XHFQ==", "dev": true }, "propagate": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/propagate/-/propagate-0.4.0.tgz", - "integrity": "sha1-8/zKCm/gZzanulcpZgaWF8EwtIE=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", "dev": true }, - "protobufjs": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-5.0.2.tgz", - "integrity": "sha1-WXSNfc8D0tsiwT2p/rAk4Wq4DJE=", + "proto3-json-serializer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.1.tgz", + "integrity": "sha512-8awBvjO+FwkMd6gNoGFZyqkHZXCFd54CIYTb6De7dPaufGJ2XNW+QUNqbMr8MaAocMdb+KpsD4rxEOaTBDCffA==", + "optional": true, "requires": { - "ascli": "1.0.1", - "bytebuffer": "5.0.1", - "glob": "7.1.2", - "yargs": "3.32.0" + "protobufjs": "^7.2.5" } }, - "ps-tree": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.1.0.tgz", - "integrity": "sha1-tCGyQUDWID8e08dplrRCewjowBQ=", - "dev": true, + "protobufjs": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.3.0.tgz", + "integrity": "sha512-YWD03n3shzV9ImZRX3ccbjqLxj7NokGN0V/ESiBV5xWqrommYHYiihuIyavq03pWSGqlyvYUFmfoMKd+1rPA/g==", + "optional": true, "requires": { - "event-stream": "3.3.4" + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" } }, "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==" + }, + "psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true }, "pump": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", "requires": { - "end-of-stream": "1.4.0", - "once": "1.4.0" + "end-of-stream": "^1.1.0", + "once": "^1.3.1" } }, "pumpify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.4.0.tgz", - "integrity": "sha512-2kmNR9ry+Pf45opRVirpNuIFotsxUGLaYqxIwuR77AYrYRMuFCz9eryHBS52L360O+NcR383CL4QYlMKPq4zYA==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dev": true, "requires": { - "duplexify": "3.5.3", - "inherits": "2.0.3", - "pump": "2.0.1" + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" }, "dependencies": { "duplexify": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.5.3.tgz", - "integrity": "sha512-g8ID9OroF9hKt2POf8YLayy+9594PzmM3scI00/uBXocX3TWNgoB67hjzkFe9ITAbQOne/lLdBxHXvYUM4ZgGA==", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dev": true, "requires": { - "end-of-stream": "1.4.0", - "inherits": "2.0.3", - "readable-stream": "2.3.3", - "stream-shift": "1.0.0" + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" } - } - } - }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - }, - "qs": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" - }, - "randomatic": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", - "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==", - "dev": true, - "requires": { - "is-number": "3.0.0", - "kind-of": "4.0.0" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + }, + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", "dev": true, "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "1.1.6" - } - } + "end-of-stream": "^1.1.0", + "once": "^1.3.1" } }, - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "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" + } + }, + "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 + }, + "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": { - "is-buffer": "1.1.6" + "safe-buffer": "~5.1.0" } } } }, - "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.4.0", - "path-type": "3.0.0" - } + "punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true }, - "readable-stream": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", - "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "1.0.7", - "safe-buffer": "5.1.1", - "string_decoder": "1.0.3", - "util-deprecate": "1.0.2" - } + "qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "dev": true }, - "rechoir": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "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 + }, + "queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "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": { - "resolve": "1.5.0" + "safe-buffer": "^5.1.0" } }, - "regex-cache": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", - "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", - "dev": true, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "requires": { - "is-equal-shallow": "0.1.3" + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" } }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", + "dev": true, "requires": { - "extend-shallow": "3.0.2", - "safe-regex": "1.1.0" + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" }, "dependencies": { - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "requires": { - "assign-symbols": "1.0.0", - "is-extendable": "1.0.1" - } - }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "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": { - "is-plain-object": "2.0.4" + "pify": "^3.0.0" } } } }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", - "dev": true - }, - "repeat-element": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", - "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=" - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" - }, - "replace-ext": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz", - "integrity": "sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=", - "dev": true - }, - "replacestream": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/replacestream/-/replacestream-4.0.3.tgz", - "integrity": "sha512-AC0FiLS352pBBiZhd4VXB1Ab/lh0lEgpP+GGvZqbQh8a5cmXVoTe5EX/YeTFArnp4SRGTHh1qCHu9lGs1qG8sA==", - "dev": true, + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "requires": { - "escape-string-regexp": "1.0.5", - "object-assign": "4.1.1", - "readable-stream": "2.3.3" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" } }, - "req-cwd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/req-cwd/-/req-cwd-1.0.1.tgz", - "integrity": "sha1-DXOurpJm5penj3l2AZZ352rPD/8=", + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, "requires": { - "req-from": "1.0.1" + "picomatch": "^2.2.1" } }, - "req-from": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/req-from/-/req-from-1.0.1.tgz", - "integrity": "sha1-v4HaUUeUfTLRO5R9wSpYrUWHNQ4=", + "rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", "dev": true, "requires": { - "resolve-from": "2.0.0" - } - }, - "request": { - "version": "2.83.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", - "integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==", - "requires": { - "aws-sign2": "0.7.0", - "aws4": "1.6.0", - "caseless": "0.12.0", - "combined-stream": "1.0.5", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.3.1", - "har-validator": "5.0.3", - "hawk": "6.0.2", - "http-signature": "1.2.0", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.17", - "oauth-sign": "0.8.2", - "performance-now": "2.1.0", - "qs": "6.5.1", - "safe-buffer": "5.1.1", - "stringstream": "0.0.5", - "tough-cookie": "2.3.3", - "tunnel-agent": "0.6.0", - "uuid": "3.1.0" + "resolve": "^1.20.0" } }, - "request-promise": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.2.tgz", - "integrity": "sha1-0epG1lSm7k+O5qT+oQGMIpEZBLQ=", + "regexp.prototype.flags": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", "dev": true, "requires": { - "bluebird": "3.5.1", - "request-promise-core": "1.1.1", - "stealthy-require": "1.1.1", - "tough-cookie": "2.3.3" + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.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=", + "release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==", "dev": true, "requires": { - "lodash": "4.17.4" + "es6-error": "^4.0.1" } }, - "resolve": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.5.0.tgz", - "integrity": "sha512-hgoSGrc3pjzAPHNBg+KnFcK2HwlHTs/YrAGUr6qgTVUZmXv1UEXXl0bZNBKMA9fud6lRYFdPGz0xXxycPzmmiw==", + "remove-bom-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz", + "integrity": "sha512-8v2rWhaakv18qcvNeli2mZ/TMTL2nEyAKRvzo1WtnZBl15SHyEhrCu2/xKlJyUFKHiHgfXIyuY6g2dObJJycXQ==", "dev": true, "requires": { - "path-parse": "1.0.5" + "is-buffer": "^1.1.5", + "is-utf8": "^0.2.1" } }, - "resolve-dir": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-0.1.1.tgz", - "integrity": "sha1-shklmlYC+sXFxJatiUpujMQwJh4=", + "remove-bom-stream": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/remove-bom-stream/-/remove-bom-stream-1.2.0.tgz", + "integrity": "sha512-wigO8/O08XHb8YPzpDDT+QmRANfW6vLqxfaXm1YXhnFf3AkSLyjfG3GEFg4McZkmgL7KvCj5u2KczkvSP6NfHA==", "dev": true, "requires": { - "expand-tilde": "1.2.2", - "global-modules": "0.2.3" + "remove-bom-buffer": "^3.0.0", + "safe-buffer": "^5.1.0", + "through2": "^2.0.3" } }, - "resolve-from": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", - "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=", + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", "dev": true }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" + "replace-ext": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", + "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", + "dev": true }, - "retry-axios": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/retry-axios/-/retry-axios-0.3.2.tgz", - "integrity": "sha512-jp4YlI0qyDFfXiXGhkCOliBN1G7fRH03Nqy8YdShzGqbY5/9S2x/IR6C88ls2DFkbWuL3ASkP7QD3pVrNpPgwQ==" + "replace-homedir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-homedir/-/replace-homedir-2.0.0.tgz", + "integrity": "sha512-bgEuQQ/BHW0XkkJtawzrfzHFSN70f/3cNOiHa2QsYxqrjaC30X1k74FJ6xswVBP0sr0SpGIdVFuPwfrYziVeyw==", + "dev": true }, - "retry-request": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-3.3.1.tgz", - "integrity": "sha512-PjAmtWIxjNj4Co/6FRtBl8afRP3CxrrIAnUzb1dzydfROd+6xt7xAebFeskgQgkfFf8NmzrXIoaB3HxmswXyxw==", - "requires": { - "request": "2.83.0", - "through2": "2.0.3" + "request": { + "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", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "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" + } + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } } }, - "right-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", - "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "request-promise": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.6.tgz", + "integrity": "sha512-HCHI3DJJUakkOr8fNoCc73E5nU5bqITjOYFMDrKHYOXWXrgD/SBaC7LjwuPymUprRyuF06UK7hd/lMHkmUXglQ==", "dev": true, - "optional": true, "requires": { - "align-text": "0.1.4" + "bluebird": "^3.5.0", + "request-promise-core": "1.1.4", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" } }, - "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "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": { - "glob": "7.1.2" + "lodash": "^4.17.19" } }, - "run-sequence": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/run-sequence/-/run-sequence-1.2.2.tgz", - "integrity": "sha1-UJWgvr6YczsBQL0I3YDsAw3azes=", + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" + }, + "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 + }, + "resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dev": true, "requires": { - "chalk": "1.1.3", - "gulp-util": "3.0.8" + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" } }, - "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" - }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", + "dev": true, "requires": { - "ret": "0.1.15" + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" } }, - "samsam": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.3.0.tgz", - "integrity": "sha512-1HwIYD/8UlOtFS3QO3w7ey+SdSDFE4HRNLZoZRYVQefrOY3l17epswImeB1ijgJFQJodIaHcwkp3r/myBjFVbg==", - "dev": true - }, - "semver": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz", - "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=", - "dev": true - }, - "sequencify": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/sequencify/-/sequencify-0.0.7.tgz", - "integrity": "sha1-kM/xnQLgcCf9dn9erT57ldHnOAw=", + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true }, - "set-getter": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/set-getter/-/set-getter-0.1.0.tgz", - "integrity": "sha1-12nBgsnVpR9AkUXy+6guXoboA3Y=", - "requires": { - "to-object-path": "0.3.0" - } - }, - "set-value": { + "resolve-options": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", - "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-2.0.0.tgz", + "integrity": "sha512-/FopbmmFOQCfsCx77BRFdKOniglTiHumLgwvd6IDPihy1GKkadZbgQJBcTb2lMzSR1pndzd96b1nZrreZ7+9/A==", + "dev": true, "requires": { - "extend-shallow": "2.0.1", - "is-extendable": "0.1.1", - "is-plain-object": "2.0.4", - "split-string": "3.1.0" + "value-or-function": "^4.0.0" } }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, + "retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "optional": true + }, + "retry-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz", + "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==", + "optional": true, "requires": { - "shebang-regex": "1.0.0" + "@types/request": "^2.48.8", + "extend": "^3.0.2", + "teeny-request": "^9.0.0" } }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "dev": true }, - "shell-quote": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz", - "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=", + "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": { - "array-filter": "0.0.1", - "array-map": "0.0.0", - "array-reduce": "0.0.0", - "jsonify": "0.0.0" + "glob": "^7.1.3" } }, - "sigmund": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", - "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", - "dev": true - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + "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" + } }, - "sinon": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-4.1.3.tgz", - "integrity": "sha512-c7u0ZuvBRX1eXuB4jN3BRCAOGiUTlM8SE3TxbJHrNiHUKL7wonujMOB6Fi1gQc00U91IscFORQHDga/eccqpbw==", + "run-sequence": { + "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": { - "diff": "3.2.0", - "formatio": "1.2.0", - "lodash.get": "4.4.2", - "lolex": "2.3.1", - "nise": "1.2.0", - "supports-color": "4.5.0", - "type-detect": "4.0.5" + "chalk": "^1.1.3", + "fancy-log": "^1.3.2", + "plugin-error": "^0.1.2" }, "dependencies": { - "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", "dev": true }, - "supports-color": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", - "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "dev": true + }, + "arr-diff": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-1.1.0.tgz", + "integrity": "sha512-OQwDZUqYaQwyyhDJHThmzId8daf4/RFNLaeh3AevmSeZ5Y7ug4Ga/yKc6l6kTZOBW781rCj103ZuTh8GAsB3+Q==", "dev": true, "requires": { - "has-flag": "2.0.0" + "arr-flatten": "^1.0.1", + "array-slice": "^0.2.3" } }, - "type-detect": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.5.tgz", - "integrity": "sha512-N9IvkQslUGYGC24RkJk1ba99foK6TkwC2FHAEBlQFBP0RxQZS8ZpJuAZcwiY/w9ZJHFQb1aOXBI60OdxhTrwEQ==", + "arr-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-2.1.0.tgz", + "integrity": "sha512-t5db90jq+qdgk8aFnxEkjqta0B/GHrM1pxzuuZz2zWsOXc5nKu3t+76s/PQBA8FTcM/ipspIH9jWG4OxCBc2eA==", "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==", - "dev": true - }, - "slash": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", - "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=" - }, - "snakeize": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/snakeize/-/snakeize-0.1.0.tgz", - "integrity": "sha1-EMCI2LWOsHazIpu1oE4jLOEmQi0=" - }, - "snapdragon": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.1.tgz", - "integrity": "sha1-4StUh/re0+PeoKyR6UAL91tAE3A=", - "requires": { - "base": "0.11.2", - "debug": "2.6.8", - "define-property": "0.2.5", - "extend-shallow": "2.0.1", - "map-cache": "0.2.2", - "source-map": "0.5.7", - "source-map-resolve": "0.5.1", - "use": "2.0.2" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + }, + "array-slice": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz", + "integrity": "sha512-rlVfZW/1Ph2SNySXwR9QYkChp8EkOEiTMO5Vwx60usw04i4nWemkm9RXmQqgkQFaLHsqLuADvjp6IfgL9l2M8Q==", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dev": true, "requires": { - "is-descriptor": "0.1.6" + "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" } }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "extend-shallow": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-1.1.4.tgz", + "integrity": "sha512-L7AGmkO6jhDkEBBGWlLtftA80Xq8DipnrRPr0pyi7GQLXkaq9JYA4xF4z6qnadIC6euiTDKco0cGSU9muw+WTw==", + "dev": true, "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } - } + "kind-of": "^1.1.0" } }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "plugin-error": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz", + "integrity": "sha512-WzZHcm4+GO34sjFMxQMqZbsz3xiNEgonCskQ9v+IroMmYgk/tas8dG+Hr2D6IbRPybZ12oWpzE/w3cGJ6FJzOw==", + "dev": true, "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } - } + "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" } }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, "requires": { - "is-accessor-descriptor": "0.1.6", - "is-data-descriptor": "0.1.4", - "kind-of": "5.1.0" + "ansi-regex": "^2.0.0" } }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "dev": true } } }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "safe-array-concat": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "dev": true, "requires": { - "define-property": "1.0.0", - "isobject": "3.0.1", - "snapdragon-util": "3.0.1" + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" }, "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "1.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true } } }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "requires": { - "kind-of": "3.2.2" - } + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" }, - "sntp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", - "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", + "safe-regex-test": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "dev": true, "requires": { - "hoek": "4.2.0" + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-regex": "^1.1.4" } }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true }, - "source-map-resolve": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.1.tgz", - "integrity": "sha512-0KW2wvzfxm8NCTb30z0LMNyPqWCdDGE2viwzUaucqJdkTRXtZiSY3I+2A6nVAjmdOy0I4gU8DwnVVGsk9jvP2A==", + "semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "requires": { - "atob": "2.0.3", - "decode-uri-component": "0.2.0", - "resolve-url": "0.2.1", - "source-map-url": "0.4.0", - "urix": "0.1.0" + "lru-cache": "^6.0.0" } }, - "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==", + "semver-greatest-satisfied-range": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-2.0.0.tgz", + "integrity": "sha512-lH3f6kMbwyANB7HuOWRMlLCa2itaCrZJ+SAqqkSZrZKO/cAsk2EOyaKHUtNkVLFyFW9pct22SFesFp3Z7zpA0g==", "dev": true, "requires": { - "source-map": "0.5.7" + "sver": "^1.8.3" } }, - "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" + "serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } }, - "sparkles": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.0.tgz", - "integrity": "sha1-Gsu/tZJDbRC76PeFt8xvgoFQEsM=", + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "dev": true }, - "spdx-correct": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz", - "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=", + "set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dev": true, "requires": { - "spdx-license-ids": "1.2.2" + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" } }, - "spdx-expression-parse": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz", - "integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=", + "set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "requires": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + } + }, + "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 }, - "spdx-license-ids": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz", - "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=", + "shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", "dev": true }, - "split": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", - "integrity": "sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=", + "side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dev": true, "requires": { - "through": "2.3.8" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" } }, - "split-array-stream": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/split-array-stream/-/split-array-stream-1.0.3.tgz", - "integrity": "sha1-0rdajl4Ngk1S/eyLgiWDncLjXfo=", + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" + }, + "simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", "requires": { - "async": "2.6.0", - "is-stream-ended": "0.1.3" + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" } }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "sinon": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-18.0.0.tgz", + "integrity": "sha512-+dXDXzD1sBO6HlmZDd7mXZCR/y5ECiEiGCBSGuFD/kZ0bDTofPYc6JaeGmPSF+1j1MejGUWkORbYOLDyvqCWpA==", + "dev": true, "requires": { - "extend-shallow": "3.0.2" + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.2.0", + "nise": "^6.0.0", + "supports-color": "^7" }, "dependencies": { - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "requires": { - "assign-symbols": "1.0.0", - "is-extendable": "1.0.1" - } + "diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "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": { - "is-plain-object": "2.0.4" + "has-flag": "^4.0.0" } } } }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "sinon-chai": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-3.7.0.tgz", + "integrity": "sha512-mf5NURdUaSdnatJx3uhoBOrY9dtL19fiOtAdT1Azxg3+lNJFiuN0uzaU3xX1LeAfL17kHQhTAJgpsfhbMJMY2g==", "dev": true }, - "sshpk": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", - "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", - "requires": { - "asn1": "0.2.3", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.1", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.1", - "getpass": "0.1.7", - "jsbn": "0.1.1", - "tweetnacl": "0.14.5" - } + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "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 + }, + "sparkles": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-2.1.0.tgz", + "integrity": "sha512-r7iW1bDw8R/cFifrD3JnQJX0K1jqT0kprL48BiBpLZLJPmAm34zsVBsK5lc7HirZYZqMW65dOXZgbAGt/I6frg==", + "dev": true + }, + "spawn-wrap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", + "dev": true, "requires": { - "define-property": "0.2.5", - "object-copy": "0.1.0" + "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": "^2.0.1" }, "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "0.1.6" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "requires": { - "is-accessor-descriptor": "0.1.6", - "is-data-descriptor": "0.1.4", - "kind-of": "5.1.0" + "isexe": "^2.0.0" } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" } } }, + "spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true + }, + "spdx-expression-parse": { + "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", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz", + "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==", + "dev": true + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "dev": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, "stealthy-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", - "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", + "integrity": "sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g==", "dev": true }, - "stream-combiner": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", - "integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=", + "stream-buffers": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-3.0.2.tgz", + "integrity": "sha512-DQi1h8VEBA/lURbSwFtEHnSTb9s2/pwLEaFuNhXwy1Dx3Sa0lOuYT2yNUr4/j2fs8oCAMANtrZ5OrPZtyVs3MQ==", + "dev": true + }, + "stream-composer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-composer/-/stream-composer-1.0.2.tgz", + "integrity": "sha512-bnBselmwfX5K10AH6L4c8+S5lgZMWI7ZYrz2rvYjCPB2DIMC4Ig8OpxGpNJSxRZ58oti7y1IcNvjBAz9vW5m4w==", "dev": true, "requires": { - "duplexer": "0.1.1" + "streamx": "^2.13.2" } }, - "stream-consume": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/stream-consume/-/stream-consume-0.1.0.tgz", - "integrity": "sha1-pB6tGm1ggc63n2WwYZAbbY89HQ8=", - "dev": true - }, "stream-events": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.2.tgz", - "integrity": "sha1-q/OfZsCJCk63lbyNXoWbJhW1kLI=", + "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" + "stubs": "^3.0.0" } }, + "stream-exhaust": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-exhaust/-/stream-exhaust-1.0.2.tgz", + "integrity": "sha512-b/qaq/GlBK5xaq1yrK9/zFcyRSTNxmcZwFLGSTG0mXgZl/4Z6GgiyYOXOvY7N3eEvFRAG1bkDRz5EPGSvPYQlw==", + "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.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==" }, - "string-format-obj": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string-format-obj/-/string-format-obj-1.1.1.tgz", - "integrity": "sha512-Mm+sROy+pHJmx0P/0Bs1uxIX6UhGJGj6xDGQZ5zh9v/SZRmLGevp+p0VJxV7lirrkAmQ2mvva/gHKpnF/pTb+Q==" + "streamfilter": { + "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" + } + }, + "streamx": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.16.1.tgz", + "integrity": "sha512-m9QYj6WygWyWa3H1YY69amr4nVgy61xfjys7xO7kviL5rfIEc2naf+ewFiOA+aEJD7y0JO3h2GoiUv4TDwEGzQ==", + "dev": true, + "requires": { + "bare-events": "^2.2.0", + "fast-fifo": "^1.1.0", + "queue-tick": "^1.0.1" + } + }, + "string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true }, "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" } }, "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.6", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.6.tgz", + "integrity": "sha512-XZpspuSB7vJWhvJc9DLSlrXl1mcA2BdoY5jjnS135ydXqLoqhs96JjDtCkjJEQHvfqZIp9hBuBMgI589peyx9Q==", "dev": true, "requires": { - "define-properties": "1.1.2", - "es-abstract": "1.9.0", - "function-bind": "1.1.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.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==", + "string.prototype.trim": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "dev": true, "requires": { - "safe-buffer": "5.1.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" } }, - "stringifier": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/stringifier/-/stringifier-1.3.0.tgz", - "integrity": "sha1-3vGDQvaTPbDy2/yaoCF1tEjBeVk=", + "string.prototype.trimend": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "dev": true, "requires": { - "core-js": "2.5.3", - "traverse": "0.6.6", - "type-name": "2.0.2" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" } }, - "stringstream": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", - "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, "requires": { - "ansi-regex": "2.1.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" } }, - "strip-bom": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-1.0.0.tgz", - "integrity": "sha1-hbiGLzhEtabV7IRnqTWYFzo295Q=", - "dev": 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==", "requires": { - "first-chunk-stream": "1.0.0", - "is-utf8": "0.2.1" + "safe-buffer": "~5.2.0" } }, - "strip-bom-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-bom-stream/-/strip-bom-stream-1.0.0.tgz", - "integrity": "sha1-5xRDmFd9Uaa+0PoZlPoF9D/ZiO4=", - "dev": true, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "requires": { - "first-chunk-stream": "1.0.0", - "strip-bom": "2.0.0" - }, - "dependencies": { - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "dev": true, - "requires": { - "is-utf8": "0.2.1" - } - } + "ansi-regex": "^5.0.1" } }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true + }, "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 + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==" + }, + "strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", + "optional": true }, "stubs": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", - "integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls=" + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "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=", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true }, - "temp": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/temp/-/temp-0.8.3.tgz", - "integrity": "sha1-4Ma8TSa5AxJEEOT+2BEDAU38H1k=", + "sver": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/sver/-/sver-1.8.4.tgz", + "integrity": "sha512-71o1zfzyawLfIWBOmw8brleKyvnbn73oVHNCsu51uPMz/HWiKkkXsI31JjHW5zqXEqnPYkIiHd8ZmL7FCimLEA==", "dev": true, "requires": { - "os-tmpdir": "1.0.2", - "rimraf": "2.2.8" + "semver": "^6.3.0" }, "dependencies": { - "rimraf": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", - "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=", - "dev": true + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "optional": true } } }, - "text-encoding": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz", - "integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=", - "dev": true + "tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "dependencies": { + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true + } + } }, - "textextensions": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-1.0.2.tgz", - "integrity": "sha1-ZUhjk+4fK7A5pgy7oFsLaL2VAdI=", - "dev": true + "tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true + "tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } }, - "through2": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", - "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", + "teeny-request": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", + "integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==", + "optional": true, "requires": { - "readable-stream": "2.3.3", - "xtend": "4.0.1" + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.9", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + }, + "dependencies": { + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "optional": true, + "requires": { + "debug": "4" + } + }, + "https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "optional": true, + "requires": { + "agent-base": "6", + "debug": "4" + } + } } }, - "through2-filter": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-2.0.0.tgz", - "integrity": "sha1-YLxVoNrLdghdsfna6Zq0P4PWIuw=", + "teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", "dev": true, "requires": { - "through2": "2.0.3", - "xtend": "4.0.1" + "streamx": "^2.12.5" } }, - "tildify": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tildify/-/tildify-1.2.0.tgz", - "integrity": "sha1-3OwD9V3Km3qj5bBPIYF+tW5jWIo=", + "test-exclude": { + "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": { - "os-homedir": "1.0.2" + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" } }, - "time-stamp": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", - "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=", + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, - "to-absolute-glob": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-0.1.1.tgz", - "integrity": "sha1-HN+kcqnvUMI57maZm2YsoOs5k38=", + "thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", "dev": true, "requires": { - "extend-shallow": "2.0.1" + "any-promise": "^1.0.0" } }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, "requires": { - "kind-of": "3.2.2" + "thenify": ">= 3.1.0 < 4" } }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "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": { - "define-property": "2.0.2", - "extend-shallow": "3.0.2", - "regex-not": "1.0.2", - "safe-regex": "1.1.0" + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" }, "dependencies": { - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, "requires": { - "assign-symbols": "1.0.0", - "is-extendable": "1.0.1" + "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" } }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "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 + }, + "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": { - "is-plain-object": "2.0.4" + "safe-buffer": "~5.1.0" } } } }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "through2-filter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.1.0.tgz", + "integrity": "sha512-VhZsTsfrIJjyUi6GeecnwcOJlmoqgIdGFDjqnV5ape+F1DN8GejfPO66XyIhoinxmxGImiUTrq9RwpTN5yszGA==", + "dev": true, "requires": { - "is-number": "3.0.0", - "repeat-string": "1.6.1" + "through2": "^4.0.2" }, "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "dev": true, "requires": { - "kind-of": "3.2.2" + "readable-stream": "3" } } } }, - "tough-cookie": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", - "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=", + "time-stamp": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", + "integrity": "sha512-gLCeArryy2yNTRzTGKbZbloctj64jkZ57hj5zdraXue6aFgd6PmvVtEyiUU+hvU0v7q08oVv8r8ev0tRo6bvgw==", + "dev": true + }, + "timsort": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", + "integrity": "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==", + "dev": true + }, + "to-absolute-glob": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", + "integrity": "sha512-rtwLUQEwT8ZeKQbyFJyomBRYXyE16U5VKuy0ftxLMK/PZb2fkOsg5r9kHdauuVDbsNdIBoC/HCthpidamQFXYA==", + "dev": true, "requires": { - "punycode": "1.4.1" + "is-absolute": "^1.0.0", + "is-negated-glob": "^1.0.0" } }, - "traverse": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", - "integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=" + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true }, - "ts-node": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-3.3.0.tgz", - "integrity": "sha1-wTxqMCTjC+EYDdUwOPwgkonUv2k=", - "dev": true, - "requires": { - "arrify": "1.0.1", - "chalk": "2.3.0", - "diff": "3.2.0", - "make-error": "1.3.0", - "minimist": "1.2.0", - "mkdirp": "0.5.1", - "source-map-support": "0.4.18", - "tsconfig": "6.0.0", - "v8flags": "3.0.1", - "yn": "2.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", - "dev": true, - "requires": { - "color-convert": "1.9.0" - } - }, - "chalk": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", - "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", - "dev": true, - "requires": { - "ansi-styles": "3.2.0", - "escape-string-regexp": "1.0.5", - "supports-color": "4.5.0" - } - }, - "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", - "dev": true - }, - "supports-color": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", - "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", - "dev": true, - "requires": { - "has-flag": "2.0.0" - } - }, - "v8flags": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.0.1.tgz", - "integrity": "sha1-3Oj8N5wX2fLJ6e142JzgAFKxt2s=", - "dev": true, - "requires": { - "homedir-polyfill": "1.0.1" - } - } + "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" } }, - "tsconfig": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-6.0.0.tgz", - "integrity": "sha1-aw6DdgA9evGGT434+J3QBZ/80DI=", + "to-through": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/to-through/-/to-through-3.0.0.tgz", + "integrity": "sha512-y8MN937s/HVhEoBU1SxfHC+wxCHkV1a9gW8eAdTadYh/bGyesZIVcbjI+mSpFbSVwQici/XjBjuUyri1dnXwBw==", "dev": true, "requires": { - "strip-bom": "3.0.0", - "strip-json-comments": "2.0.1" - }, - "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 - } + "streamx": "^2.12.5" } }, - "tslib": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.0.tgz", - "integrity": "sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ==" - }, - "tslint": { - "version": "5.9.1", - "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.9.1.tgz", - "integrity": "sha1-ElX4ej/1frCw4fDmEKi0dIBGya4=", - "dev": true, - "requires": { - "babel-code-frame": "6.26.0", - "builtin-modules": "1.1.1", - "chalk": "2.3.0", - "commander": "2.13.0", - "diff": "3.2.0", - "glob": "7.1.2", - "js-yaml": "3.10.0", - "minimatch": "3.0.4", - "resolve": "1.5.0", - "semver": "5.5.0", - "tslib": "1.9.0", - "tsutils": "2.19.1" + "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" + } + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "requires": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" }, "dependencies": { - "ansi-styles": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", - "dev": true, - "requires": { - "color-convert": "1.9.0" - } - }, - "chalk": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", - "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", - "dev": true, - "requires": { - "ansi-styles": "3.2.0", - "escape-string-regexp": "1.0.5", - "supports-color": "4.5.0" - } - }, - "commander": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz", - "integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==", - "dev": true - }, - "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", - "dev": true - }, - "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", - "dev": true - }, - "supports-color": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", - "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", - "dev": true, - "requires": { - "has-flag": "2.0.0" - } + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true } } }, + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, "tsutils": { - "version": "2.19.1", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.19.1.tgz", - "integrity": "sha512-1B3z4H4HddgzWptqLzwrJloDEsyBt8DvZhnFO14k7A4RsQL/UhEfQjD4hpcY5NpF3veBkjJhQJ8Bl7Xp96cN+A==", + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", "dev": true, "requires": { - "tslib": "1.9.0" + "tslib": "^1.8.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } } }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "requires": { - "safe-buffer": "5.1.1" + "safe-buffer": "^5.0.1" } }, "tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "optional": true + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true }, "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "requires": { - "prelude-ls": "1.1.2" + "prelude-ls": "^1.2.1" } }, "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-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/type-name/-/type-name-2.0.2.tgz", - "integrity": "sha1-7+fUEj2KxSr/9/QMfk3sUmYAj7Q=" + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + }, + "typed-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" + } + }, + "typed-array-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + } + }, + "typed-array-byte-offset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + } + }, + "typed-array-length": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + } }, "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" - }, - "typescript": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.6.1.tgz", - "integrity": "sha1-7znN6ierrAtQAkLWcmq5DgyEZjE=", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "dev": true }, - "uglify-js": { - "version": "2.8.29", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", - "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "typedarray-to-buffer": { + "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==", "dev": true, - "optional": true, "requires": { - "source-map": "0.5.7", - "uglify-to-browserify": "1.0.2", - "yargs": "3.10.0" - }, - "dependencies": { - "camelcase": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", - "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", - "dev": true, - "optional": true - }, - "cliui": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", - "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", - "dev": true, - "optional": true, - "requires": { - "center-align": "0.1.3", - "right-align": "0.1.3", - "wordwrap": "0.0.2" - } - }, - "window-size": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", - "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", - "dev": true, - "optional": true - }, - "wordwrap": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", - "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", - "dev": true, - "optional": true - }, - "yargs": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", - "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", - "dev": true, - "optional": true, - "requires": { - "camelcase": "1.2.1", - "cliui": "2.1.0", - "decamelize": "1.2.0", - "window-size": "0.1.0" - } - } + "is-typedarray": "^1.0.0" } }, - "uglify-to-browserify": { + "typescript": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "dev": true + }, + "unbox-primitive": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", - "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", "dev": true, - "optional": true + "requires": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + } }, "unc-path-regex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", - "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=", + "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==", "dev": true }, - "union-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", - "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "undertaker": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-2.0.0.tgz", + "integrity": "sha512-tO/bf30wBbTsJ7go80j0RzA2rcwX6o7XPBpeFcb+jzoeb4pfMM2zUeSDIkY1AWqeZabWxaQZ/h8N9t35QKDLPQ==", + "dev": true, "requires": { - "arr-union": "3.1.0", - "get-value": "2.0.6", - "is-extendable": "0.1.1", - "set-value": "0.4.3" + "bach": "^2.0.1", + "fast-levenshtein": "^3.0.0", + "last-run": "^2.0.0", + "undertaker-registry": "^2.0.0" }, "dependencies": { - "set-value": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", - "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", + "fast-levenshtein": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz", + "integrity": "sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==", + "dev": true, "requires": { - "extend-shallow": "2.0.1", - "is-extendable": "0.1.1", - "is-plain-object": "2.0.4", - "to-object-path": "0.3.0" + "fastest-levenshtein": "^1.0.7" } } } }, - "unique-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-1.0.0.tgz", - "integrity": "sha1-1ZpKdUJ0R9mqbJHnAmP40mpLEEs=", + "undertaker-registry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-2.0.0.tgz", + "integrity": "sha512-+hhVICbnp+rlzZMgxXenpvTxpuvA67Bfgtt+O9WOE5jo7w/dyiF1VmoZVIHvP2EkUjsyKyTwYKlLhA+j47m1Ew==", "dev": true }, - "unique-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", - "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", - "requires": { - "crypto-random-string": "1.0.0" - } + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, - "universal-deep-strict-equal": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/universal-deep-strict-equal/-/universal-deep-strict-equal-1.2.2.tgz", - "integrity": "sha1-DaSsL3PP95JMgfpN4BjKViyisKc=", + "unique-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.3.1.tgz", + "integrity": "sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A==", + "dev": true, "requires": { - "array-filter": "1.0.0", - "indexof": "0.0.1", - "object-keys": "1.0.11" - }, - "dependencies": { - "array-filter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-1.0.0.tgz", - "integrity": "sha1-uveeYubvTCpMC4MSMtr/7CUfnYM=" - } + "json-stable-stringify-without-jsonify": "^1.0.1", + "through2-filter": "^3.0.0" } }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true + }, + "update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, "requires": { - "has-value": "0.3.1", - "isobject": "3.0.1" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "requires": { - "get-value": "2.0.6", - "has-values": "0.1.4", - "isobject": "2.1.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - } + "escalade": "^3.1.1", + "picocolors": "^1.0.0" } }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" - }, - "use": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/use/-/use-2.0.2.tgz", - "integrity": "sha1-riig1y+TvyJCKhii43mZMRLeyOg=", + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, "requires": { - "define-property": "0.2.5", - "isobject": "3.0.1", - "lazy-cache": "2.0.2" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "0.1.6" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "1.1.6" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "requires": { - "is-accessor-descriptor": "0.1.6", - "is-data-descriptor": "0.1.4", - "kind-of": "5.1.0" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" - }, - "lazy-cache": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-2.0.2.tgz", - "integrity": "sha1-uRkKT5EzVGlIQIWfio9whNiCImQ=", - "requires": { - "set-getter": "0.1.0" - } - } + "punycode": "^2.1.0" } }, - "user-home": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/user-home/-/user-home-1.1.1.tgz", - "integrity": "sha1-K1viOjK2Onyd640PKNSFcko98ZA=", - "dev": true - }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "uuid": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", - "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" }, - "v8flags": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-2.1.1.tgz", - "integrity": "sha1-qrGh+jDUX4jdMhFIh1rALAtV5bQ=", - "dev": true, - "requires": { - "user-home": "1.1.1" - } + "v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true }, - "vali-date": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/vali-date/-/vali-date-1.0.0.tgz", - "integrity": "sha1-G5BKWWCfsyjvB4E4Qgk09rhnCaY=", + "v8flags": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-4.0.1.tgz", + "integrity": "sha512-fcRLaS4H/hrZk9hYwbdRM35D0U8IYMfEClhXxCivOojl+yTRAZH3Zy2sSy6qVCiGbV9YAtPssP6jaChqC9vPCg==", "dev": true }, "validate-npm-package-license": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz", - "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "dev": true, "requires": { - "spdx-correct": "1.0.2", - "spdx-expression-parse": "1.0.4" + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" } }, + "validator": { + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", + "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==", + "dev": true + }, + "value-or-function": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-4.0.0.tgz", + "integrity": "sha512-aeVK81SIuT6aMJfNo9Vte8Dw0/FZINGBV8BfCraGtqVxIeLAEhJyoWs8SmvRVmXfGss2PmmOwZCuBPbZR+IYWg==", + "dev": true + }, "verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "dev": true, "requires": { - "assert-plus": "1.0.0", + "assert-plus": "^1.0.0", "core-util-is": "1.0.2", - "extsprintf": "1.3.0" + "extsprintf": "^1.2.0" + }, + "dependencies": { + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true + } } }, "vinyl": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.5.3.tgz", - "integrity": "sha1-sEVbOPxeDPMNQyUTLkYZcMIJHN4=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", + "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", "dev": true, "requires": { - "clone": "1.0.2", - "clone-stats": "0.0.1", - "replace-ext": "0.0.1" + "clone": "^2.1.2", + "clone-stats": "^1.0.0", + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" } }, - "vinyl-fs": { - "version": "0.3.14", - "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-0.3.14.tgz", - "integrity": "sha1-mmhRzhysHBzqX+hsCTHWIMLPqeY=", - "dev": true, - "requires": { - "defaults": "1.0.3", - "glob-stream": "3.1.18", - "glob-watcher": "0.0.6", - "graceful-fs": "3.0.11", - "mkdirp": "0.5.1", - "strip-bom": "1.0.0", - "through2": "0.6.5", - "vinyl": "0.4.6" + "vinyl-contents": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-contents/-/vinyl-contents-2.0.0.tgz", + "integrity": "sha512-cHq6NnGyi2pZ7xwdHSW1v4Jfnho4TEGtxZHw01cmnc8+i7jgR6bRnED/LbrKan/Q7CvVLbnvA5OepnhbpjBZ5Q==", + "dev": true, + "requires": { + "bl": "^5.0.0", + "vinyl": "^3.0.0" }, "dependencies": { - "clone": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/clone/-/clone-0.2.0.tgz", - "integrity": "sha1-xhJqkK1Pctv1rNskPMN3JP6T/B8=", - "dev": true - }, - "graceful-fs": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-3.0.11.tgz", - "integrity": "sha1-dhPHeKGv6mLyXGMKCG1/Osu92Bg=", - "dev": true, - "requires": { - "natives": "1.1.0" - } - }, - "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.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "dev": true, - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "0.0.1", - "string_decoder": "0.10.31" - } - }, - "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 - }, - "through2": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", - "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", + "bl": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", "dev": true, "requires": { - "readable-stream": "1.0.34", - "xtend": "4.0.1" + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" } }, - "vinyl": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.4.6.tgz", - "integrity": "sha1-LzVsh6VQolVGHza76ypbqL94SEc=", + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", "dev": true, "requires": { - "clone": "0.2.0", - "clone-stats": "0.0.1" + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" } } } }, - "vinyl-sourcemaps-apply": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz", - "integrity": "sha1-q2VJ1h0XLCsbh75cUI0jnI74dwU=", + "vinyl-fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-4.0.0.tgz", + "integrity": "sha512-7GbgBnYfaquMk3Qu9g22x000vbYkOex32930rBnc3qByw6HfMEAoELjCjoJv4HuEQxHAurT+nvMHm6MnJllFLw==", + "dev": true, + "requires": { + "fs-mkdirp-stream": "^2.0.1", + "glob-stream": "^8.0.0", + "graceful-fs": "^4.2.11", + "iconv-lite": "^0.6.3", + "is-valid-glob": "^1.0.0", + "lead": "^4.0.0", + "normalize-path": "3.0.0", + "resolve-options": "^2.0.0", + "stream-composer": "^1.0.2", + "streamx": "^2.14.0", + "to-through": "^3.0.0", + "value-or-function": "^4.0.0", + "vinyl": "^3.0.0", + "vinyl-sourcemap": "^2.0.0" + } + }, + "vinyl-sourcemap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-2.0.0.tgz", + "integrity": "sha512-BAEvWxbBUXvlNoFQVFVHpybBbjW1r03WhohJzJDSfgrrK5xVYIDTan6xN14DlyImShgDRv2gl9qhM6irVMsV0Q==", "dev": true, "requires": { - "source-map": "0.5.7" + "convert-source-map": "^2.0.0", + "graceful-fs": "^4.2.10", + "now-and-later": "^3.0.0", + "streamx": "^2.12.5", + "vinyl": "^3.0.0", + "vinyl-contents": "^2.0.0" } }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, "websocket-driver": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.0.tgz", - "integrity": "sha1-DK+dLXVdk67gSdS90NP+LMoqJOs=", + "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.9", - "websocket-extensions": "0.1.2" + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" } }, "websocket-extensions": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.2.tgz", - "integrity": "sha1-Dhh4HeYpoYMIzhSBZQ9n/6JpOl0=" + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } }, "which": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", - "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "dev": true, "requires": { - "isexe": "2.0.0" + "isexe": "^2.0.0" } }, - "window-size": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.4.tgz", - "integrity": "sha1-+OGqHuWlPsW/FR/6CXQqatdpeHY=" + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "dev": true + }, + "which-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.2" + } + }, + "wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "workerpool": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", "dev": true }, "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "requires": { - "string-width": "1.0.2", - "strip-ansi": "3.0.1" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" } }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "write-file-atomic": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz", - "integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==", + "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==", + "dev": true, "requires": { - "graceful-fs": "4.1.11", - "imurmurhash": "0.1.4", - "signal-exit": "3.0.2" + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" } }, - "xdg-basedir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", - "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=" - }, - "xmlhttprequest": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz", - "integrity": "sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw=", - "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==", + "dev": true }, "y18n": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", - "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" }, "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yargs": { - "version": "3.32.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.32.0.tgz", - "integrity": "sha1-AwiOnr+edWtpdRYR0qXvWRSCyZU=", + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "requires": { - "camelcase": "2.1.1", - "cliui": "3.2.0", - "decamelize": "1.2.0", - "os-locale": "1.4.0", - "string-width": "1.0.2", - "window-size": "0.1.4", - "y18n": "3.2.1" + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" } }, - "yn": { + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" + }, + "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" + } + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", "dev": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + }, + "z-schema": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", + "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", + "dev": true, + "requires": { + "commander": "^9.4.1", + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + } } } } diff --git a/package.json b/package.json index 3db349b931..5e73bf3f5b 100644 --- a/package.json +++ b/package.json @@ -1,21 +1,33 @@ { "name": "firebase-admin", - "version": "5.10.0", + "version": "12.1.1", "description": "Firebase admin SDK for Node.js", "author": "Firebase (https://firebase.google.com/)", "license": "Apache-2.0", "homepage": "https://firebase.google.com/", + "engines": { + "node": ">=14" + }, "scripts": { "build": "gulp build", - "lint": "run-p lint:src lint:unit lint:integration", + "build:tests": "gulp compile_test", + "prepare": "npm run build && npm run esm-wrap", + "lint": "run-p lint:src lint:test", "test": "run-s lint test:unit", "integration": "run-s build test:integration", - "test:unit": "mocha test/unit/*.spec.ts --compilers ts:ts-node/register", - "test:integration": "mocha test/integration/*.ts --slow 5000 --compilers ts:ts-node/register", + "test:unit": "mocha test/unit/*.spec.ts --require ts-node/register", + "test:integration": "mocha test/integration/*.ts --slow 5000 --timeout 20000 --require ts-node/register", "test:coverage": "nyc npm run test:unit", - "lint:src": "tslint --format stylish -p tsconfig.json", - "lint:unit": "tslint -c tslint-test.json --format stylish test/unit/*.ts test/unit/**/*.ts", - "lint:integration": "tslint -c tslint-test.json --format stylish test/integration/*.ts" + "lint:src": "eslint src/ --ext .ts", + "lint:test": "eslint test/ --ext .ts", + "apidocs": "run-s api-extractor:local api-documenter", + "api-extractor": "node generate-reports.js", + "api-extractor:local": "npm run build && node generate-reports.js --local", + "esm-wrap": "node generate-esm-wrapper.js", + "api-documenter": "run-s api-documenter:markdown api-documenter:toc api-documenter:post", + "api-documenter:markdown": "api-documenter-fire markdown --input temp --output docgen/markdown -s --project admin", + "api-documenter:toc": "api-documenter-fire toc --input temp --output docgen/markdown -p /docs/reference/admin/node -s", + "api-documenter:post": "node docgen/post-process.js" }, "nyc": { "extension": [ @@ -48,56 +60,207 @@ "package.json" ], "types": "./lib/index.d.ts", + "typesVersions": { + "*": { + "app": [ + "lib/app" + ], + "app-check": [ + "lib/app-check" + ], + "auth": [ + "lib/auth" + ], + "eventarc": [ + "lib/eventarc" + ], + "extensions": [ + "lib/extensions" + ], + "database": [ + "lib/database" + ], + "firestore": [ + "lib/firestore" + ], + "functions": [ + "lib/functions" + ], + "installations": [ + "lib/installations" + ], + "instance-id": [ + "lib/instance-id" + ], + "machine-learning": [ + "lib/machine-learning" + ], + "messaging": [ + "lib/messaging" + ], + "project-management": [ + "lib/project-management" + ], + "remote-config": [ + "lib/remote-config" + ], + "security-rules": [ + "lib/security-rules" + ], + "storage": [ + "lib/storage" + ] + } + }, + "exports": { + ".": "./lib/index.js", + "./app": { + "types": "./lib/app/index.d.ts", + "require": "./lib/app/index.js", + "import": "./lib/esm/app/index.js" + }, + "./app-check": { + "types": "./lib/app-check/index.d.ts", + "require": "./lib/app-check/index.js", + "import": "./lib/esm/app-check/index.js" + }, + "./auth": { + "types": "./lib/auth/index.d.ts", + "require": "./lib/auth/index.js", + "import": "./lib/esm/auth/index.js" + }, + "./database": { + "types": "./lib/database/index.d.ts", + "require": "./lib/database/index.js", + "import": "./lib/esm/database/index.js" + }, + "./eventarc": { + "types": "./lib/eventarc/index.d.ts", + "require": "./lib/eventarc/index.js", + "import": "./lib/esm/eventarc/index.js" + }, + "./extensions": { + "types": "./lib/extensions/index.d.ts", + "require": "./lib/extensions/index.js", + "import": "./lib/esm/extensions/index.js" + }, + "./firestore": { + "types": "./lib/firestore/index.d.ts", + "require": "./lib/firestore/index.js", + "import": "./lib/esm/firestore/index.js" + }, + "./functions": { + "types": "./lib/functions/index.d.ts", + "require": "./lib/functions/index.js", + "import": "./lib/esm/functions/index.js" + }, + "./installations": { + "types": "./lib/installations/index.d.ts", + "require": "./lib/installations/index.js", + "import": "./lib/esm/installations/index.js" + }, + "./instance-id": { + "types": "./lib/instance-id/index.d.ts", + "require": "./lib/instance-id/index.js", + "import": "./lib/esm/instance-id/index.js" + }, + "./machine-learning": { + "types": "./lib/machine-learning/index.d.ts", + "require": "./lib/machine-learning/index.js", + "import": "./lib/esm/machine-learning/index.js" + }, + "./messaging": { + "types": "./lib/messaging/index.d.ts", + "require": "./lib/messaging/index.js", + "import": "./lib/esm/messaging/index.js" + }, + "./project-management": { + "types": "./lib/project-management/index.d.ts", + "require": "./lib/project-management/index.js", + "import": "./lib/esm/project-management/index.js" + }, + "./remote-config": { + "types": "./lib/remote-config/index.d.ts", + "require": "./lib/remote-config/index.js", + "import": "./lib/esm/remote-config/index.js" + }, + "./security-rules": { + "types": "./lib/security-rules/index.d.ts", + "require": "./lib/security-rules/index.js", + "import": "./lib/esm/security-rules/index.js" + }, + "./storage": { + "types": "./lib/storage/index.d.ts", + "require": "./lib/storage/index.js", + "import": "./lib/esm/storage/index.js" + } + }, "dependencies": { - "@firebase/app": "^0.1.10", - "@firebase/database": "^0.2.0", - "@google-cloud/firestore": "^0.13.0", - "@google-cloud/storage": "^1.6.0", - "@types/google-cloud__storage": "^1.1.7", - "@types/node": "^8.0.53", - "faye-websocket": "0.9.3", - "jsonwebtoken": "8.1.0", - "node-forge": "0.7.1" + "@fastify/busboy": "^2.1.0", + "@firebase/database-compat": "^1.0.2", + "@firebase/database-types": "^1.0.0", + "@types/node": "^20.10.3", + "farmhash": "^3.3.1", + "jsonwebtoken": "^9.0.0", + "jwks-rsa": "^3.1.0", + "long": "^5.2.3", + "node-forge": "^1.3.1", + "uuid": "^9.0.0" + }, + "optionalDependencies": { + "@google-cloud/firestore": "^7.7.0", + "@google-cloud/storage": "^7.7.0" }, "devDependencies": { - "@types/chai": "^3.4.34", - "@types/chai-as-promised": "0.0.29", + "@firebase/api-documenter": "^0.4.0", + "@firebase/app-compat": "^0.2.1", + "@firebase/auth-compat": "^0.4.1", + "@firebase/auth-types": "^0.12.0", + "@microsoft/api-extractor": "^7.11.2", + "@types/bcrypt": "^5.0.0", + "@types/chai": "^4.0.0", + "@types/chai-as-promised": "^7.1.0", "@types/firebase-token-generator": "^2.0.28", - "@types/lodash": "^4.14.85", - "@types/mocha": "^2.2.32", - "@types/nock": "^9.1.0", - "@types/request": "2.0.6", - "@types/request-promise": "^4.1.33", - "@types/sinon": "^4.1.2", - "@types/sinon-chai": "^2.7.27", - "chai": "^3.5.0", - "chai-as-promised": "^6.0.0", - "chalk": "^1.1.3", - "del": "^2.2.1", - "firebase": "~4.11.0", + "@types/jsonwebtoken": "8.5.1", + "@types/lodash": "^4.14.104", + "@types/minimist": "^1.2.2", + "@types/mocha": "^10.0.0", + "@types/nock": "^11.1.0", + "@types/request": "^2.47.0", + "@types/request-promise": "^4.1.41", + "@types/sinon": "^17.0.2", + "@types/sinon-chai": "^3.0.0", + "@types/uuid": "^9.0.1", + "@typescript-eslint/eslint-plugin": "^5.62.0", + "@typescript-eslint/parser": "^5.62.0", + "bcrypt": "^5.0.0", + "chai": "^4.2.0", + "chai-as-promised": "^7.0.0", + "chai-exclude": "^2.1.0", + "chalk": "^4.1.1", + "child-process-promise": "^2.2.1", + "del": "^6.0.0", + "eslint": "^8.12.0", "firebase-token-generator": "^2.0.0", - "gulp": "^3.9.1", - "gulp-exit": "0.0.2", - "gulp-header": "^1.8.8", - "gulp-istanbul": "^1.1.1", - "gulp-mocha": "^3.0.1", - "gulp-replace": "^0.5.4", - "gulp-tslint": "^6.0.2", - "gulp-typescript": "^3.1.2", - "lodash": "^4.6.1", - "merge2": "^1.0.2", - "minimist": "^1.2.0", - "mocha": "^3.5.0", - "nock": "^9.1.0", - "npm-run-all": "^4.1.2", - "nyc": "^11.3.0", + "gulp": "^5.0.0", + "gulp-filter": "^7.0.0", + "gulp-header": "^2.0.9", + "gulp-typescript": "^5.0.1", + "http-message-parser": "^0.0.34", + "lodash": "^4.17.15", + "minimist": "^1.2.6", + "mocha": "^10.0.0", + "mz": "^2.7.0", + "nock": "^13.0.0", + "npm-run-all": "^4.1.5", + "nyc": "^15.1.0", "request": "^2.75.0", "request-promise": "^4.1.1", - "run-sequence": "^1.1.5", - "sinon": "^4.1.3", - "sinon-chai": "^2.8.0", - "ts-node": "^3.3.0", - "tslint": "^5.9.0", - "typescript": "^2.6.1" + "run-sequence": "^2.2.1", + "sinon": "^18.0.0", + "sinon-chai": "^3.0.0", + "ts-node": "^10.2.0", + "typescript": "5.1.6", + "yargs": "^17.0.1" } } 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..6e99bc2c64 --- /dev/null +++ b/src/app-check/app-check-api-client-internal.ts @@ -0,0 +1,267 @@ +/*! + * @license + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { App } from '../app'; +import { FirebaseApp } from '../app/firebase-app'; +import { + HttpRequestConfig, HttpClient, HttpError, AuthorizedHttpClient, HttpResponse +} from '../utils/api-request'; +import { PrefixedFirebaseError } from '../utils/error'; +import * as utils from '../utils/index'; +import * as validator from '../utils/validator'; +import { AppCheckToken } from './app-check-api' + +// App Check backend constants +const FIREBASE_APP_CHECK_V1_API_URL_FORMAT = 'https://firebaseappcheck.googleapis.com/v1/projects/{projectId}/apps/{appId}:exchangeCustomToken'; +const ONE_TIME_USE_TOKEN_VERIFICATION_URL_FORMAT = 'https://firebaseappcheck.googleapis.com/v1beta/projects/{projectId}:verifyAppCheckToken'; + +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: App) { + 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 as FirebaseApp); + } + + /** + * Exchange a signed custom token to App Check token + * + * @param customToken - The custom token to be exchanged. + * @param appId - The mobile App ID. + * @returns 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); + }); + } + + public verifyReplayProtection(token: string): Promise { + if (!validator.isNonEmptyString(token)) { + throw new FirebaseAppCheckError( + 'invalid-argument', + '`token` must be a non-empty string.'); + } + return this.getVerifyTokenUrl() + .then((url) => { + const request: HttpRequestConfig = { + method: 'POST', + url, + headers: FIREBASE_APP_CHECK_CONFIG_HEADERS, + data: { app_check_token: token } + }; + return this.httpClient.send(request); + }) + .then((resp) => { + if (typeof resp.data.alreadyConsumed !== 'undefined' + && !validator.isBoolean(resp.data?.alreadyConsumed)) { + throw new FirebaseAppCheckError( + 'invalid-argument', '`alreadyConsumed` must be a boolean value.'); + } + return resp.data.alreadyConsumed || false; + }) + .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 getVerifyTokenUrl(): Promise { + return this.getProjectId() + .then((projectId) => { + const urlParams = { + projectId + }; + const baseUrl = utils.formatString(ONE_TIME_USE_TOKEN_VERIFICATION_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. + * @returns An AppCheckToken instance. + */ + private toAppCheckToken(resp: HttpResponse): AppCheckToken { + const token = resp.data.token; + // `ttl` is a string with the suffix "s" preceded by the number of seconds, + // with nanoseconds expressed as fractional seconds. + const ttlMillis = this.stringToMilliseconds(resp.data.ttl); + 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". + * + * @returns 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 code - The error code. + * @param 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-api.ts b/src/app-check/app-check-api.ts new file mode 100644 index 0000000000..de44a5a854 --- /dev/null +++ b/src/app-check/app-check-api.ts @@ -0,0 +1,141 @@ +/*! + * @license + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Interface representing 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 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 options for the {@link AppCheck.verifyToken} method. + */ +export interface VerifyAppCheckTokenOptions { + /** + * To use the replay protection feature, set this to `true`. The {@link AppCheck.verifyToken} + * method will mark the token as consumed after verifying it. + * + * Tokens that are found to be already consumed will be marked as such in the response. + * + * Tokens are only considered to be consumed if it is sent to App Check backend by calling the + * {@link AppCheck.verifyToken} method with this field set to `true`; other uses of the token + * do not consume it. + * + * This replay protection feature requires an additional network call to the App Check backend + * and forces your clients to obtain a fresh attestation from your chosen attestation providers. + * This can therefore negatively impact performance and can potentially deplete your attestation + * providers' quotas faster. We recommend that you use this feature only for protecting + * low volume, security critical, or expensive operations. + */ + consume?: boolean; +} + +/** + * Interface representing a decoded Firebase App Check token, returned from the + * {@link AppCheck.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 {@link DecodedAppCheckToken.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 {@link DecodedAppCheckToken.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 {@link DecodedAppCheckToken.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: DecodedAppCheckToken; + + /** + * Indicates weather this token was already consumed. + * If this is the first time {@link AppCheck.verifyToken} method has seen this token, + * this field will contain the value `false`. The given token will then be + * marked as `already_consumed` for all future invocations of this {@link AppCheck.verifyToken} + * method for this token. + * + * When this field is `true`, the caller is attempting to reuse a previously consumed token. + * You should take precautions against such a caller; for example, you can take actions such as + * rejecting the request or ask the caller to pass additional layers of security checks. + */ + alreadyConsumed?: boolean; +} diff --git a/src/app-check/app-check-namespace.ts b/src/app-check/app-check-namespace.ts new file mode 100644 index 0000000000..13e5beb3a7 --- /dev/null +++ b/src/app-check/app-check-namespace.ts @@ -0,0 +1,86 @@ +/*! + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { App } from '../app'; +import { + AppCheckToken as TAppCheckToken, + AppCheckTokenOptions as TAppCheckTokenOptions, + DecodedAppCheckToken as TDecodedAppCheckToken, + VerifyAppCheckTokenOptions as TVerifyAppCheckTokenOptions, + VerifyAppCheckTokenResponse as TVerifyAppCheckTokenResponse, +} from './app-check-api'; +import { AppCheck as TAppCheck } from './app-check'; + +/** + * Gets the {@link firebase-admin.app-check#AppCheck} service for the default app or a given app. + * + * `admin.appCheck()` can be called with no arguments to access the default + * app's `AppCheck` service or as `admin.appCheck(app)` to access the + * `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. + * + * @returns The default `AppCheck` service if no + * app is provided, or the `AppCheck` service associated with the provided + * app. + */ +export declare function appCheck(app?: App): appCheck.AppCheck; + +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace appCheck { + /** + * Type alias to {@link firebase-admin.app-check#AppCheck}. + */ + export type AppCheck = TAppCheck; + + /** + * Type alias to {@link firebase-admin.app-check#AppCheckToken}. + */ + export type AppCheckToken = TAppCheckToken; + + /** + * Type alias to {@link firebase-admin.app-check#DecodedAppCheckToken}. + */ + export type DecodedAppCheckToken = TDecodedAppCheckToken; + + /** + * Type alias to {@link firebase-admin.app-check#VerifyAppCheckTokenResponse}. + */ + export type VerifyAppCheckTokenResponse = TVerifyAppCheckTokenResponse; + + /** + * Type alias to {@link firebase-admin.app-check#AppCheckTokenOptions}. + */ + export type AppCheckTokenOptions = TAppCheckTokenOptions; + + /** + * Type alias to {@link firebase-admin.app-check#VerifyAppCheckTokenOptions}. + */ + export type VerifyAppCheckTokenOptions = TVerifyAppCheckTokenOptions; +} diff --git a/src/app-check/app-check.ts b/src/app-check/app-check.ts new file mode 100644 index 0000000000..c81a04acf5 --- /dev/null +++ b/src/app-check/app-check.ts @@ -0,0 +1,118 @@ +/*! + * @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 { App } from '../app'; +import { AppCheckApiClient, FirebaseAppCheckError } from './app-check-api-client-internal'; +import { + appCheckErrorFromCryptoSignerError, AppCheckTokenGenerator, +} from './token-generator'; +import { AppCheckTokenVerifier } from './token-verifier'; +import { cryptoSignerFromApp } from '../utils/crypto-signer'; + +import { + AppCheckToken, + AppCheckTokenOptions, + VerifyAppCheckTokenOptions, + VerifyAppCheckTokenResponse, +} from './app-check-api'; + +/** + * The Firebase `AppCheck` service interface. + */ +export class AppCheck { + + private readonly client: AppCheckApiClient; + private readonly tokenGenerator: AppCheckTokenGenerator; + private readonly appCheckTokenVerifier: AppCheckTokenVerifier; + + /** + * @param app - The app for this AppCheck service. + * @constructor + * @internal + */ + constructor(readonly app: App) { + 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 AppCheckToken} that can be sent + * 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. + * + * @returns A promise that fulfills with a `AppCheckToken`. + */ + public createToken(appId: string, options?: AppCheckTokenOptions): Promise { + return this.tokenGenerator.createCustomToken(appId, options) + .then((customToken) => { + return this.client.exchangeToken(customToken, appId); + }); + } + + /** + * 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. + * @param options - Optional {@link VerifyAppCheckTokenOptions} object when verifying an App Check Token. + * + * @returns A promise fulfilled with the token's decoded claims + * if the App Check token is valid; otherwise, a rejected promise. + */ + public verifyToken(appCheckToken: string, options?: VerifyAppCheckTokenOptions) + : Promise { + this.validateVerifyAppCheckTokenOptions(options); + return this.appCheckTokenVerifier.verifyToken(appCheckToken) + .then((decodedToken) => { + if (options?.consume) { + return this.client.verifyReplayProtection(appCheckToken) + .then((alreadyConsumed) => { + return { + alreadyConsumed, + appId: decodedToken.app_id, + token: decodedToken, + }; + }); + } + return { + appId: decodedToken.app_id, + token: decodedToken, + }; + }); + } + + private validateVerifyAppCheckTokenOptions(options?: VerifyAppCheckTokenOptions): void { + if (typeof options === 'undefined') { + return; + } + if (!validator.isNonNullObject(options)) { + throw new FirebaseAppCheckError( + 'invalid-argument', + 'VerifyAppCheckTokenOptions must be a non-null object.'); + } + } +} diff --git a/src/app-check/index.ts b/src/app-check/index.ts new file mode 100644 index 0000000000..54cd9291d8 --- /dev/null +++ b/src/app-check/index.ts @@ -0,0 +1,70 @@ +/*! + * @license + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Firebase App Check. + * + * @packageDocumentation + */ + +import { App, getApp } from '../app'; +import { FirebaseApp } from '../app/firebase-app'; +import { AppCheck } from './app-check'; + +export { + AppCheckToken, + AppCheckTokenOptions, + DecodedAppCheckToken, + VerifyAppCheckTokenOptions, + VerifyAppCheckTokenResponse, +} from './app-check-api'; +export { AppCheck } from './app-check'; + +/** + * Gets the {@link AppCheck} service for the default app or a given app. + * + * `getAppCheck()` can be called with no arguments to access the default + * app's `AppCheck` service or as `getAppCheck(app)` to access the + * `AppCheck` service associated with a specific app. + * + * @example + * ```javascript + * // Get the `AppCheck` service for the default app + * const defaultAppCheck = getAppCheck(); + * ``` + * + * @example + * ```javascript + * // Get the `AppCheck` service for a given app + * const otherAppCheck = getAppCheck(otherApp); + * ``` + * + * @param app - Optional app for which to return the `AppCheck` service. + * If not provided, the default `AppCheck` service is returned. + * + * @returns The default `AppCheck` service if no + * app is provided, or the `AppCheck` service associated with the provided + * app. + */ +export function getAppCheck(app?: App): AppCheck { + if (typeof app === 'undefined') { + app = getApp(); + } + + const firebaseApp: FirebaseApp = app as FirebaseApp; + return firebaseApp.getOrInitService('appCheck', (app) => new AppCheck(app)); +} diff --git a/src/app-check/token-generator.ts b/src/app-check/token-generator.ts new file mode 100644 index 0000000000..2cfe3ca14c --- /dev/null +++ b/src/app-check/token-generator.ts @@ -0,0 +1,180 @@ +/*! + * @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, transformMillisecondsToSecondsString } 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 { AppCheckTokenOptions } from './app-check-api'; +import { HttpError } from '../utils/api-request'; + +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 +const FIREBASE_APP_CHECK_AUDIENCE = 'https://firebaseappcheck.googleapis.com/google.firebase.appcheck.v1.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. + * + * @returns 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, 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, + typ: 'JWT', + }; + const iat = Math.floor(Date.now() / 1000); + const body = { + iss: account, + sub: account, + app_id: appId, + aud: FIREBASE_APP_CHECK_AUDIENCE, + exp: iat + (ONE_MINUTE_IN_SECONDS * 5), + iat, + ...customOptions, + }; + 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(/=+$/, ''); + } + + /** + * 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 {}; + } +} + +/** + * 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 + * @returns 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 signing 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..6c6503f05d --- /dev/null +++ b/src/app-check/token-verifier.ts @@ -0,0 +1,163 @@ +/*! + * 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 * as util from '../utils/index'; +import { FirebaseAppCheckError } from './app-check-api-client-internal'; +import { + ALGORITHM_RS256, DecodedToken, decodeJwt, JwtError, + JwtErrorCode, PublicKeySignatureVerifier, SignatureVerifier +} from '../utils/jwt'; + +import { DecodedAppCheckToken } from './app-check-api' +import { App } from '../app'; + +const APP_CHECK_ISSUER = 'https://firebaseappcheck.googleapis.com/'; +const JWKS_URL = 'https://firebaseappcheck.googleapis.com/v1/jwks'; + +/** + * Class for verifying Firebase App Check tokens. + * + * @internal + */ +export class AppCheckTokenVerifier { + private readonly signatureVerifier: SignatureVerifier; + + constructor(private readonly app: App) { + 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. + * @returns 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; + 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/app/core.ts b/src/app/core.ts new file mode 100644 index 0000000000..f37e3c112c --- /dev/null +++ b/src/app/core.ts @@ -0,0 +1,207 @@ +/*! + * @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 { Agent } from 'http'; + +import { Credential } from './credential'; + +/** + * Available options to pass to {@link firebase-admin.app#initializeApp}. + */ +export interface AppOptions { + + /** + * A {@link firebase-admin.app#Credential} object used to + * authenticate the Admin SDK. + * + * See {@link https://firebase.google.com/docs/admin/setup#initialize_the_sdk | Initialize the SDK} + * for detailed documentation and code samples. + */ + credential?: Credential; + + /** + * The object to use as the {@link https://firebase.google.com/docs/reference/security/database/#auth | 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 + * {@link https://firebase.google.com/docs/database/admin/start#authenticate-with-limited-privileges | + * 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 {@link https://nodejs.org/api/http.html#http_class_http_agent | 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; +} + +/** + * A Firebase app holds the initialization information for a collection of + * services. + */ +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]" + * initializeApp(defaultAppConfig); + * console.log(admin.app().name); // "[DEFAULT]" + * ``` + * + * @example + * ```javascript + * // A named app's name is what you provide to initializeApp() + * const otherApp = 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 firebase-admin.app#initializeApp}. + * + * @example + * ```javascript + * const app = initializeApp(config); + * console.log(app.options.credential === config.credential); // true + * console.log(app.options.databaseURL === config.databaseURL); // true + * ``` + */ + options: AppOptions; +} + +/** + * `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 for troubleshooting the cause of the error with + * {@link https://firebase.google.com/support | Firebase Support}. + */ + stack?: string; + + /** + * Returns a JSON-serializable object representation of this error. + * + * @returns 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; +} diff --git a/src/app/credential-factory.ts b/src/app/credential-factory.ts new file mode 100644 index 0000000000..9bb32a8869 --- /dev/null +++ b/src/app/credential-factory.ts @@ -0,0 +1,155 @@ +/*! + * @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 { Agent } from 'http'; + +import { Credential, ServiceAccount } from './credential'; +import { + ServiceAccountCredential, RefreshTokenCredential, getApplicationDefault +} from './credential-internal'; + +let globalAppDefaultCred: Credential | undefined; +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 firebase-admin.app#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 + * initializeApp({ + * credential: applicationDefault(), + * databaseURL: "https://.firebaseio.com" + * }); + * ``` + * + * @param httpAgent - Optional {@link https://nodejs.org/api/http.html#http_class_http_agent | HTTP Agent} + * to be used when retrieving access tokens from Google token servers. + * + * @returns 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 firebase-admin.app#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 + * const serviceAccount = require("path/to/serviceAccountKey.json"); + * initializeApp({ + * credential: cert(serviceAccount), + * databaseURL: "https://.firebaseio.com" + * }); + * ``` + * + * @example + * ```javascript + * // Providing a service account object inline + * initializeApp({ + * 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 {@link https://nodejs.org/api/http.html#http_class_http_agent | HTTP Agent} + * to be used when retrieving access tokens from Google token servers. + * + * @returns A credential authenticated via the + * provided service account that can be used to initialize an app. + */ +export function cert(serviceAccountPathOrObject: string | ServiceAccount, 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 firebase-admin.app#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 + * const refreshToken = require("path/to/refreshToken.json"); + * initializeApp({ + * 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 {@link https://nodejs.org/api/http.html#http_class_http_agent | HTTP Agent} + * to be used when retrieving access tokens from Google token servers. + * + * @returns 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]; +} + +/** + * Clears the global ADC cache. Exported for testing. + */ +export function clearGlobalAppDefaultCred(): void { + globalAppDefaultCred = undefined; +} diff --git a/src/app/credential-internal.ts b/src/app/credential-internal.ts new file mode 100644 index 0000000000..f6a3d3bc78 --- /dev/null +++ b/src/app/credential-internal.ts @@ -0,0 +1,628 @@ +/*! + * @license + * 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 fs = require('fs'); +import os = require('os'); +import path = require('path'); + +import { Agent } from 'http'; +import { Credential, GoogleOAuthAccessToken } from './credential'; +import { AppErrorCodes, FirebaseAppError } from '../utils/error'; +import { HttpClient, HttpRequestConfig, HttpError, HttpResponse } from '../utils/api-request'; +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_IDENTITY_PATH = '/computeMetadata/v1/instance/service-accounts/default/identity'; +const GOOGLE_METADATA_SERVICE_PROJECT_ID_PATH = '/computeMetadata/v1/project/project-id'; +const GOOGLE_METADATA_SERVICE_ACCOUNT_ID_PATH = '/computeMetadata/v1/instance/service-accounts/default/email'; + +const configDir = (() => { + // Windows has a dedicated low-rights location for apps at ~/Application Data + 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); + } + + // eslint-disable-next-line @typescript-eslint/naming-convention + 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; + private accountId?: string; + + constructor(httpAgent?: Agent) { + this.httpAgent = httpAgent; + } + + public getAccessToken(): Promise { + const request = this.buildRequest(GOOGLE_METADATA_SERVICE_TOKEN_PATH); + return requestAccessToken(this.httpClient, request); + } + + /** + * getIDToken returns a OIDC token from the compute metadata service + * that can be used to make authenticated calls to audience + * @param audience the URL the returned ID token will be used to call. + */ + public getIDToken(audience: string): Promise { + const request = this.buildRequest(`${GOOGLE_METADATA_SERVICE_IDENTITY_PATH}?audience=${audience}`); + return requestIDToken(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}`); + }); + } + + public getServiceAccountEmail(): Promise { + if (this.accountId) { + return Promise.resolve(this.accountId); + } + + const request = this.buildRequest(GOOGLE_METADATA_SERVICE_ACCOUNT_ID_PATH); + return this.httpClient.send(request) + .then((resp) => { + this.accountId = resp.text!; + return this.accountId; + }) + .catch((err) => { + const detail: string = (err instanceof HttpError) ? getDetailFromResponse(err.response) : err.message; + throw new FirebaseAppError( + AppErrorCodes.INVALID_CREDENTIAL, + `Failed to determine service account email: ${detail}`); + }); + } + + private buildRequest(urlPath: string): HttpRequestConfig { + return { + method: 'GET', + 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); + } + } +} + + +/** + * Implementation of Credential that uses impersonated service account. + */ +export class ImpersonatedServiceAccountCredential implements Credential { + + private readonly impersonatedServiceAccount: ImpersonatedServiceAccount; + private readonly httpClient: HttpClient; + + /** + * Creates a new ImpersonatedServiceAccountCredential from the given parameters. + * + * @param impersonatedServiceAccountPathOrObject - Impersonated 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 optional boolean indicating whether this credential was implicitly + * discovered from the environment, as opposed to being explicitly specified by the developer. + * + * @constructor + */ + constructor( + impersonatedServiceAccountPathOrObject: string | object, + private readonly httpAgent?: Agent, + readonly implicit: boolean = false) { + + this.impersonatedServiceAccount = (typeof impersonatedServiceAccountPathOrObject === 'string') ? + ImpersonatedServiceAccount.fromPath(impersonatedServiceAccountPathOrObject) + : new ImpersonatedServiceAccount(impersonatedServiceAccountPathOrObject); + this.httpClient = new HttpClient(); + } + + public getAccessToken(): Promise { + const postData = + 'client_id=' + this.impersonatedServiceAccount.clientId + '&' + + 'client_secret=' + this.impersonatedServiceAccount.clientSecret + '&' + + 'refresh_token=' + this.impersonatedServiceAccount.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); + } +} + +/** + * A struct containing the properties necessary to use impersonated service account JSON credentials. + */ +class ImpersonatedServiceAccount { + + public readonly clientId: string; + public readonly clientSecret: string; + public readonly refreshToken: string; + public readonly type: string; + + /* + * Tries to load a ImpersonatedServiceAccount from a path. Throws if the path doesn't exist or the + * data at the path is invalid. + */ + public static fromPath(filePath: string): ImpersonatedServiceAccount { + try { + return new ImpersonatedServiceAccount(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 impersonated service account file: ' + error, + ); + } + } + + constructor(json: object) { + const sourceCredentials = (json as {[key: string]: any})['source_credentials'] + if (sourceCredentials) { + copyAttr(this, sourceCredentials, 'clientId', 'client_id'); + copyAttr(this, sourceCredentials, 'clientSecret', 'client_secret'); + copyAttr(this, sourceCredentials, 'refreshToken', 'refresh_token'); + copyAttr(this, sourceCredentials, 'type', 'type'); + } + + let errorMessage; + if (!util.isNonEmptyString(this.clientId)) { + errorMessage = 'Impersonated Service Account must contain a "source_credentials.client_id" property.'; + } else if (!util.isNonEmptyString(this.clientSecret)) { + errorMessage = 'Impersonated Service Account must contain a "source_credentials.client_secret" property.'; + } else if (!util.isNonEmptyString(this.refreshToken)) { + errorMessage = 'Impersonated Service Account must contain a "source_credentials.refresh_token" property.'; + } else if (!util.isNonEmptyString(this.type)) { + errorMessage = 'Impersonated Service Account must contain a "source_credentials.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) || + (credential instanceof ImpersonatedServiceAccountCredential && credential.implicit); +} + +export function getApplicationDefault(httpAgent?: Agent): Credential { + if (process.env.GOOGLE_APPLICATION_CREDENTIALS) { + return credentialFromFile(process.env.GOOGLE_APPLICATION_CREDENTIALS, httpAgent, false)!; + } + + // It is OK to not have this file. If it is present, it must be valid. + if (GCLOUD_CREDENTIAL_PATH) { + const credential = credentialFromFile(GCLOUD_CREDENTIAL_PATH, httpAgent, true); + if (credential) return credential + } + + 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)); + }); +} + +/** + * Obtain a new OIDC token by making a remote service call. + */ +function requestIDToken(client: HttpClient, request: HttpRequestConfig): Promise { + return client.send(request).then((resp) => { + if (!resp.text) { + throw new FirebaseAppError( + AppErrorCodes.INVALID_CREDENTIAL, + 'Unexpected response while fetching id token: response.text is undefined', + ); + } + return resp.text; + }).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, ignoreMissing?: boolean): Credential | null { + const credentialsFile = readCredentialFile(filePath, ignoreMissing); + if (typeof credentialsFile !== 'object' || credentialsFile === null) { + if (ignoreMissing) { return 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); + } + + if (credentialsFile.type === 'impersonated_service_account') { + return new ImpersonatedServiceAccountCredential(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/app/credential.ts b/src/app/credential.ts new file mode 100644 index 0000000000..b5857903f8 --- /dev/null +++ b/src/app/credential.ts @@ -0,0 +1,47 @@ +/*! + * @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. + */ + +export interface ServiceAccount { + projectId?: string; + clientEmail?: string; + privateKey?: string; +} + +/** + * Interface for Google OAuth 2.0 access tokens. + */ +export interface GoogleOAuthAccessToken { + access_token: string; + expires_in: number; +} + +/** + * 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 the `firebase-admin/app` module. + */ +export interface Credential { + /** + * Returns a Google OAuth2 access token object used to authenticate with + * Firebase services. + * + * @returns A Google OAuth2 access token object. + */ + getAccessToken(): Promise; +} \ No newline at end of file diff --git a/src/app/firebase-app.ts b/src/app/firebase-app.ts new file mode 100644 index 0000000000..4dd97cb33d --- /dev/null +++ b/src/app/firebase-app.ts @@ -0,0 +1,266 @@ +/*! + * @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 { AppOptions, App } from './core'; +import { AppStore } from './lifecycle'; +import { Credential } from './credential'; +import { getApplicationDefault } from './credential-internal'; +import * as validator from '../utils/validator'; +import { deepCopy } from '../utils/deep-copy'; +import { AppErrorCodes, FirebaseAppError } from '../utils/error'; + +const TOKEN_EXPIRY_THRESHOLD_MILLIS = 5 * 60 * 1000; + +/** + * Type representing a Firebase OAuth access token (derived from a Google OAuth2 access token) which + * can be used to authenticate to Firebase services such as the Realtime Database and Auth. + */ +export interface FirebaseAccessToken { + accessToken: string; + expirationTime: number; +} + +/** + * Internals of a FirebaseApp instance. + */ +export class FirebaseAppInternals { + private cachedToken_: FirebaseAccessToken; + private tokenListeners_: Array<(token: string) => void>; + + // eslint-disable-next-line @typescript-eslint/naming-convention + constructor(private credential_: Credential) { + this.tokenListeners_ = []; + } + + public getToken(forceRefresh = false): Promise { + if (forceRefresh || this.shouldRefresh()) { + return this.refreshToken(); + } + + return Promise.resolve(this.cachedToken_); + } + + public getCachedToken(): FirebaseAccessToken | null { + return this.cachedToken_ || null; + } + + 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) { + // Update the cache before firing listeners. Listeners may directly query the + // cached token state. + 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 listener - The listener that will be called with each new token. + */ + public addAuthTokenListener(listener: (token: string) => void): void { + this.tokenListeners_.push(listener); + if (this.cachedToken_) { + listener(this.cachedToken_.accessToken); + } + } + + /** + * Removes a token listener. + * + * @param listener - The listener to remove. + */ + public removeAuthTokenListener(listener: (token: string) => void): void { + this.tokenListeners_ = this.tokenListeners_.filter((other) => other !== listener); + } +} + +/** + * Global context object for a collection of services using a shared authentication state. + * + * @internal + */ +export class FirebaseApp implements App { + + public INTERNAL: FirebaseAppInternals; + + private name_: string; + private options_: AppOptions; + private services_: {[name: string]: unknown} = {}; + private isDeleted_ = false; + + constructor(options: AppOptions, name: string, private readonly appStore?: AppStore) { + this.name_ = name; + 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 ' + + `app named "${this.name_}". Options must be a non-null object.`, + ); + } + + const hasCredential = ('credential' in this.options_); + if (!hasCredential) { + this.options_.credential = getApplicationDefault(this.options_.httpAgent); + } + + const credential = this.options_.credential; + 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 ' + + `app named "${this.name_}". The "credential" property must be an object which implements ` + + 'the Credential interface.', + ); + } + + this.INTERNAL = new FirebaseAppInternals(credential); + } + + /** + * Returns the name of the FirebaseApp instance. + * + * @returns The name of the FirebaseApp instance. + */ + get name(): string { + this.checkDestroyed_(); + return this.name_; + } + + /** + * Returns the options for the FirebaseApp instance. + * + * @returns The options for the FirebaseApp instance. + */ + get options(): AppOptions { + this.checkDestroyed_(); + return deepCopy(this.options_); + } + + /** + * @internal + */ + public getOrInitService(name: string, init: (app: FirebaseApp) => T): T { + return this.ensureService_(name, () => init(this)); + } + + /** + * Deletes the FirebaseApp instance. + * + * @returns An empty Promise fulfilled once the FirebaseApp instance is deleted. + */ + public delete(): Promise { + this.checkDestroyed_(); + + // Also remove the instance from the AppStore. This is needed to support the existing + // app.delete() use case. In the future we can remove this API, and deleteApp() will + // become the only way to tear down an App. + this.appStore?.removeApp(this.name); + + return Promise.all(Object.keys(this.services_).map((serviceName) => { + const service = this.services_[serviceName]; + if (isStateful(service)) { + return service.delete(); + } + return Promise.resolve(); + })).then(() => { + this.services_ = {}; + this.isDeleted_ = true; + }); + } + + // eslint-disable-next-line @typescript-eslint/naming-convention + private ensureService_(serviceName: string, initializer: () => T): T { + this.checkDestroyed_(); + if (!(serviceName in this.services_)) { + this.services_[serviceName] = initializer(); + } + + return this.services_[serviceName] as T; + } + + /** + * Throws an Error if the FirebaseApp instance has already been deleted. + */ + // eslint-disable-next-line @typescript-eslint/naming-convention + private checkDestroyed_(): void { + if (this.isDeleted_) { + throw new FirebaseAppError( + AppErrorCodes.APP_DELETED, + `Firebase app named "${this.name_}" has already been deleted.`, + ); + } + } +} + +interface StatefulFirebaseService { + delete(): Promise; +} + +function isStateful(service: any): service is StatefulFirebaseService { + return typeof service.delete === 'function'; +} diff --git a/src/app/firebase-namespace.ts b/src/app/firebase-namespace.ts new file mode 100644 index 0000000000..ef5e17f0eb --- /dev/null +++ b/src/app/firebase-namespace.ts @@ -0,0 +1,402 @@ +/*! + * @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 as AppCore } from './core'; +import { AppStore, defaultAppStore } from './lifecycle'; +import { + app, appCheck, auth, messaging, machineLearning, storage, firestore, database, + instanceId, installations, projectManagement, securityRules , remoteConfig, AppOptions, +} from '../firebase-namespace-api'; +import { cert, refreshToken, applicationDefault } from './credential-factory'; +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; +import Installations = installations.Installations; +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; + +export interface FirebaseServiceNamespace { + (app?: App): T; + [key: string]: any; +} + +/** + * Internals of a FirebaseNamespace instance. + */ +export class FirebaseNamespaceInternals { + + constructor(private readonly appStore: AppStore) {} + + /** + * Initializes the App 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. + * + * @returns A new App instance. + */ + public initializeApp(options?: AppOptions, appName?: string): App { + const app = this.appStore.initializeApp(options, appName); + return extendApp(app); + } + + /** + * Returns the App instance with the provided name (or the default App instance + * if no name is provided). + * + * @param appName - Optional name of the FirebaseApp instance to return. + * @returns The App instance which has the provided name. + */ + public app(appName?: string): App { + const app = this.appStore.getApp(appName); + return extendApp(app); + } + + /* + * Returns an array of all the non-deleted App instances. + */ + public get apps(): App[] { + return this.appStore.getApps().map((app) => extendApp(app)); + } +} + +const firebaseCredential = { + cert, refreshToken, applicationDefault +}; + +/** + * Global Firebase context object. + */ +export class FirebaseNamespace { + // Hack to prevent Babel from modifying the object returned as the default admin namespace. + /* tslint:disable:variable-name */ + public __esModule = true; + /* tslint:enable:variable-name */ + + public credential = firebaseCredential; + public SDK_VERSION = getSdkVersion(); + public INTERNAL: FirebaseNamespaceInternals; + + /* tslint:disable */ + // TODO(jwenger): Database is the only consumer of firebase.Promise. We should update it to use + // use the native Promise and then remove this. + public Promise: any = Promise; + /* tslint:enable */ + + constructor(appStore?: AppStore) { + this.INTERNAL = new FirebaseNamespaceInternals(appStore ?? new AppStore()); + } + + /** + * Gets the `Auth` service namespace. The returned namespace can be used to get the + * `Auth` service for the default app or an explicitly specified app. + */ + get auth(): FirebaseServiceNamespace { + const fn: FirebaseServiceNamespace = (app?: App) => { + return this.ensureApp(app).auth(); + }; + const auth = require('../auth/auth').Auth; + return Object.assign(fn, { Auth: auth }); + } + + /** + * Gets the `Database` service namespace. The returned namespace can be used to get the + * `Database` service for the default app or an explicitly specified app. + */ + get database(): FirebaseServiceNamespace { + const fn: FirebaseServiceNamespace = (app?: App) => { + return this.ensureApp(app).database(); + }; + + // eslint-disable-next-line @typescript-eslint/no-var-requires + return Object.assign(fn, require('@firebase/database-compat/standalone')); + } + + /** + * Gets the `Messaging` service namespace. The returned namespace can be used to get the + * `Messaging` service for the default app or an explicitly specified app. + */ + get messaging(): FirebaseServiceNamespace { + const fn: FirebaseServiceNamespace = (app?: App) => { + return this.ensureApp(app).messaging(); + }; + const messaging = require('../messaging/messaging').Messaging; + return Object.assign(fn, { Messaging: messaging }); + } + + /** + * Gets the `Storage` service namespace. The returned namespace can be used to get the + * `Storage` service for the default app or an explicitly specified app. + */ + get storage(): FirebaseServiceNamespace { + const fn: FirebaseServiceNamespace = (app?: App) => { + return this.ensureApp(app).storage(); + }; + const storage = require('../storage/storage').Storage; + return Object.assign(fn, { Storage: storage }); + } + + /** + * Gets the `Firestore` service namespace. The returned namespace can be used to get the + * `Firestore` service for the default app or an explicitly specified app. + */ + get firestore(): FirebaseServiceNamespace { + let fn: FirebaseServiceNamespace = (app?: App) => { + return this.ensureApp(app).firestore(); + }; + + // eslint-disable-next-line @typescript-eslint/no-var-requires + const firestore = require('@google-cloud/firestore'); + + fn = Object.assign(fn, firestore.Firestore); + + // `v1beta1` and `v1` are lazy-loaded in the Firestore SDK. We use the same trick here + // to avoid triggering this lazy-loading upon initialization. + Object.defineProperty(fn, 'v1beta1', { + get: () => { + return firestore.v1beta1; + }, + }); + Object.defineProperty(fn, 'v1', { + get: () => { + return firestore.v1; + }, + }); + + return fn; + } + + /** + * Gets the `MachineLearning` service namespace. The returned namespace can be + * used to get the `MachineLearning` service for the default app or an + * explicityly specified app. + */ + get machineLearning(): FirebaseServiceNamespace { + const fn: FirebaseServiceNamespace = + (app?: App) => { + return this.ensureApp(app).machineLearning(); + }; + const machineLearning = + require('../machine-learning/machine-learning').MachineLearning; + 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. + */ + get instanceId(): FirebaseServiceNamespace { + const fn: FirebaseServiceNamespace = (app?: App) => { + return this.ensureApp(app).instanceId(); + }; + const instanceId = require('../instance-id/instance-id').InstanceId; + return Object.assign(fn, { InstanceId: instanceId }); + } + + /** + * Gets the `ProjectManagement` service namespace. The returned namespace can be used to get the + * `ProjectManagement` service for the default app or an explicitly specified app. + */ + get projectManagement(): FirebaseServiceNamespace { + const fn: FirebaseServiceNamespace = (app?: App) => { + return this.ensureApp(app).projectManagement(); + }; + const projectManagement = require('../project-management/project-management').ProjectManagement; + return Object.assign(fn, { ProjectManagement: projectManagement }); + } + + /** + * Gets the `SecurityRules` service namespace. The returned namespace can be used to get the + * `SecurityRules` service for the default app or an explicitly specified app. + */ + get securityRules(): FirebaseServiceNamespace { + const fn: FirebaseServiceNamespace = (app?: App) => { + return this.ensureApp(app).securityRules(); + }; + const securityRules = require('../security-rules/security-rules').SecurityRules; + return Object.assign(fn, { SecurityRules: securityRules }); + } + + /** + * Gets the `RemoteConfig` service namespace. The returned namespace can be used to get the + * `RemoteConfig` service for the default app or an explicitly specified app. + */ + get remoteConfig(): FirebaseServiceNamespace { + const fn: FirebaseServiceNamespace = (app?: App) => { + return this.ensureApp(app).remoteConfig(); + }; + const remoteConfig = require('../remote-config/remote-config').RemoteConfig; + 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. + + /** + * Initializes 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 appName - Optional name of the FirebaseApp instance. + * + * @returns A new FirebaseApp instance. + */ + public initializeApp(options?: AppOptions, appName?: string): App { + return this.INTERNAL.initializeApp(options, appName); + } + + /** + * Returns the FirebaseApp instance with the provided name (or the default FirebaseApp instance + * if no name is provided). + * + * @param appName - Optional name of the FirebaseApp instance to return. + * @returns The FirebaseApp instance which has the provided name. + */ + public app(appName?: string): App { + return this.INTERNAL.app(appName); + } + + /* + * Returns an array of all the non-deleted FirebaseApp instances. + */ + public get apps(): App[] { + return this.INTERNAL.apps; + } + + private ensureApp(app?: App): App { + if (typeof app === 'undefined') { + app = this.app(); + } + return app; + } +} + +/** + * In order to maintain backward compatibility, we instantiate a default namespace instance in + * this module, and delegate all app lifecycle operations to it. In a future implementation where + * the old admin namespace is no longer supported, we should remove this. + * + * @internal + */ +export const defaultNamespace = new FirebaseNamespace(defaultAppStore); + +function extendApp(app: AppCore): App { + const result: App = app as App; + if ((result as any).__extended) { + return result; + } + + result.auth = () => { + const fn = require('../auth/index').getAuth; + return fn(app); + }; + + result.appCheck = () => { + const fn = require('../app-check/index').getAppCheck; + return fn(app); + }; + + result.database = (url?: string) => { + const fn = require('../database/index').getDatabaseWithUrl; + return fn(url, app); + }; + + result.messaging = () => { + const fn = require('../messaging/index').getMessaging; + return fn(app); + }; + + result.storage = () => { + const fn = require('../storage/index').getStorage; + return fn(app); + }; + + result.firestore = () => { + const fn = require('../firestore/index').getFirestore; + return fn(app); + }; + + result.instanceId = () => { + const fn = require('../instance-id/index').getInstanceId; + return fn(app); + } + + result.installations = () => { + const fn = require('../installations/index').getInstallations; + return fn(app); + }; + + result.machineLearning = () => { + const fn = require('../machine-learning/index').getMachineLearning; + return fn(app); + } + + result.projectManagement = () => { + const fn = require('../project-management/index').getProjectManagement; + return fn(app); + }; + + result.securityRules = () => { + const fn = require('../security-rules/index').getSecurityRules; + return fn(app); + }; + + result.remoteConfig = () => { + const fn = require('../remote-config/index').getRemoteConfig; + return fn(app); + }; + + (result as any).__extended = true; + return result; +} diff --git a/src/app/index.ts b/src/app/index.ts new file mode 100644 index 0000000000..5308e414e2 --- /dev/null +++ b/src/app/index.ts @@ -0,0 +1,34 @@ +/*! + * @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 { getSdkVersion } from '../utils'; + +/** + * Firebase App and SDK initialization. + * + * @packageDocumentation + */ + +export { App, AppOptions, FirebaseArrayIndexError, FirebaseError } from './core' +export { initializeApp, getApp, getApps, deleteApp } from './lifecycle'; + +export { Credential, ServiceAccount, GoogleOAuthAccessToken } from './credential'; +export { applicationDefault, cert, refreshToken } from './credential-factory'; + +export { FirebaseAppError, AppErrorCodes } from '../utils/error'; + +export const SDK_VERSION = getSdkVersion(); diff --git a/src/app/lifecycle.ts b/src/app/lifecycle.ts new file mode 100644 index 0000000000..9c7cfdd31d --- /dev/null +++ b/src/app/lifecycle.ts @@ -0,0 +1,186 @@ +/*! + * @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 fs = require('fs'); + +import * as validator from '../utils/validator'; +import { AppErrorCodes, FirebaseAppError } from '../utils/error'; +import { App, AppOptions } from './core'; +import { getApplicationDefault } from './credential-internal'; +import { FirebaseApp } from './firebase-app'; + +const DEFAULT_APP_NAME = '[DEFAULT]'; + +export class AppStore { + + private readonly appStore = new Map(); + + public initializeApp(options?: AppOptions, appName: string = DEFAULT_APP_NAME): App { + if (typeof options === 'undefined') { + options = loadOptionsFromEnvVar(); + options.credential = getApplicationDefault(); + } + + if (typeof appName !== 'string' || appName === '') { + throw new FirebaseAppError( + AppErrorCodes.INVALID_APP_NAME, + `Invalid Firebase app name "${appName}" provided. App name must be a non-empty string.`, + ); + } else if (this.appStore.has(appName)) { + if (appName === DEFAULT_APP_NAME) { + throw new FirebaseAppError( + AppErrorCodes.DUPLICATE_APP, + 'The default Firebase app already exists. This means you called initializeApp() ' + + 'more than once without providing an app name as the second argument. In most cases ' + + 'you only need to call initializeApp() once. But if you do want to initialize ' + + 'multiple apps, pass a second argument to initializeApp() to give each app a unique ' + + 'name.', + ); + } else { + throw new FirebaseAppError( + AppErrorCodes.DUPLICATE_APP, + `Firebase app named "${appName}" already exists. This means you called initializeApp() ` + + 'more than once with the same app name as the second argument. Make sure you provide a ' + + 'unique name every time you call initializeApp().', + ); + } + } + + const app = new FirebaseApp(options, appName, this); + this.appStore.set(app.name, app); + return app; + } + + public getApp(appName: string = DEFAULT_APP_NAME): App { + if (typeof appName !== 'string' || appName === '') { + throw new FirebaseAppError( + AppErrorCodes.INVALID_APP_NAME, + `Invalid Firebase app name "${appName}" provided. App name must be a non-empty string.`, + ); + } else if (!this.appStore.has(appName)) { + let errorMessage: string = (appName === DEFAULT_APP_NAME) + ? 'The default Firebase app does not exist. ' : `Firebase app named "${appName}" does not exist. `; + errorMessage += 'Make sure you call initializeApp() before using any of the Firebase services.'; + + throw new FirebaseAppError(AppErrorCodes.NO_APP, errorMessage); + } + + return this.appStore.get(appName)!; + } + + public getApps(): App[] { + // Return a copy so the caller cannot mutate the array + return Array.from(this.appStore.values()); + } + + public deleteApp(app: App): Promise { + if (typeof app !== 'object' || app === null || !('options' in app)) { + throw new FirebaseAppError(AppErrorCodes.INVALID_ARGUMENT, 'Invalid app argument.'); + } + + // Make sure the given app already exists. + const existingApp = getApp(app.name); + + // Delegate delete operation to the App instance itself. That will also remove the App + // instance from the AppStore. + return (existingApp as FirebaseApp).delete(); + } + + public clearAllApps(): Promise { + const promises: Array> = []; + this.getApps().forEach((app) => { + promises.push(this.deleteApp(app)); + }) + + return Promise.all(promises).then(); + } + + /** + * Removes the specified App instance from the store. This is currently called by the + * {@link FirebaseApp.delete} method. Can be removed once the app deletion is handled + * entirely by the {@link deleteApp} top-level function. + */ + public removeApp(appName: string): void { + this.appStore.delete(appName); + } +} + +export const defaultAppStore = new AppStore(); + +export function initializeApp(options?: AppOptions, appName: string = DEFAULT_APP_NAME): App { + return defaultAppStore.initializeApp(options, appName); +} + +export function getApp(appName: string = DEFAULT_APP_NAME): App { + return defaultAppStore.getApp(appName); +} + +export function getApps(): App[] { + return defaultAppStore.getApps(); +} + +/** + * Renders this given `App` 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 + * deleteApp(app) + * .then(function() { + * console.log("App deleted successfully"); + * }) + * .catch(function(error) { + * console.log("Error deleting app:", error); + * }); + * ``` + */ +export function deleteApp(app: App): Promise { + return defaultAppStore.deleteApp(app); +} + +/** + * Constant holding the environment variable name with the default config. + * 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. + */ +export const FIREBASE_CONFIG_VAR = 'FIREBASE_CONFIG'; + +/** + * Parse the file pointed to by the FIREBASE_CONFIG_VAR, if it exists. + * Or if the FIREBASE_CONFIG_ENV contains a valid JSON object, parse it directly. + * 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. + */ +function 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 AppOptions; + } catch (error) { + // Throw a nicely formed error message if the file contents cannot be parsed + throw new FirebaseAppError( + AppErrorCodes.INVALID_APP_OPTIONS, + 'Failed to parse app options file: ' + error, + ); + } +} diff --git a/src/auth/action-code-settings-builder.ts b/src/auth/action-code-settings-builder.ts new file mode 100644 index 0000000000..4ebc4df19c --- /dev/null +++ b/src/auth/action-code-settings-builder.ts @@ -0,0 +1,248 @@ +/*! + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as validator from '../utils/validator'; +import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; + +/** + * This is the interface that defines the required continue/state URL with + * optional Android and iOS bundle identifiers. + */ +export interface ActionCodeSettings { + + /** + * Defines the link continue/state URL, which has different meanings in + * different contexts: + *
    + *
  • When the link is handled in the web action widgets, this is the deep + * link in the `continueUrl` query parameter.
  • + *
  • When the link is handled in the app directly, this is the `continueUrl` + * query parameter in the deep link of the Dynamic Link.
  • + *
+ */ + url: string; + + /** + * Whether to open the link via a mobile app or a browser. + * The default is false. When set to true, the action code link is sent + * as a Universal Link or Android App Link and is opened by the app if + * installed. In the false case, the code is sent to the web widget first + * and then redirects to the app if installed. + */ + handleCodeInApp?: boolean; + + /** + * Defines the iOS bundle ID. This will try to open the link in an iOS app if it + * is installed. + */ + iOS?: { + + /** + * Defines the required iOS bundle ID of the app where the link should be + * handled if the application is already installed on the device. + */ + bundleId: string; + }; + + /** + * Defines the Android package name. This will try to open the link in an + * android app if it is installed. If `installApp` is passed, it specifies + * whether to install the Android app if the device supports it and the app is + * not already installed. If this field is provided without a `packageName`, an + * error is thrown explaining that the `packageName` must be provided in + * conjunction with this field. If `minimumVersion` is specified, and an older + * version of the app is installed, the user is taken to the Play Store to + * upgrade the app. + */ + android?: { + + /** + * Defines the required Android package name of the app where the link should be + * handled if the Android app is installed. + */ + packageName: string; + + /** + * Whether to install the Android app if the device supports it and the app is + * not already installed. + */ + installApp?: boolean; + + /** + * The Android minimum version if available. If the installed app is an older + * version, the user is taken to the GOogle Play Store to upgrade the app. + */ + minimumVersion?: string; + }; + + /** + * Defines the dynamic link domain to use for the current link if it is to be + * opened using Firebase Dynamic Links, as multiple dynamic link domains can be + * configured per project. This field provides the ability to explicitly choose + * configured per project. This fields provides the ability explicitly choose + * one. If none is provided, the oldest domain is used by default. + */ + dynamicLinkDomain?: string; +} + +/** Defines the email action code server request. */ +interface EmailActionCodeRequest { + continueUrl?: string; + canHandleCodeInApp?: boolean; + dynamicLinkDomain?: string; + androidPackageName?: string; + androidMinimumVersion: string; + androidInstallApp?: boolean; + iOSBundleId?: string; +} + +/** + * Defines the ActionCodeSettings builder class used to convert the + * ActionCodeSettings object to its corresponding server request. + * + * @internal + */ +export class ActionCodeSettingsBuilder { + private continueUrl?: string; + private apn?: string; + private amv?: string; + private installApp?: boolean; + private ibi?: string; + private canHandleCodeInApp?: boolean; + private dynamicLinkDomain?: string; + + /** + * ActionCodeSettingsBuilder constructor. + * + * @param {ActionCodeSettings} actionCodeSettings The ActionCodeSettings + * object used to initiliaze this server request builder. + * @constructor + */ + constructor(actionCodeSettings: ActionCodeSettings) { + if (!validator.isNonNullObject(actionCodeSettings)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"ActionCodeSettings" must be a non-null object.', + ); + } + if (typeof actionCodeSettings.url === 'undefined') { + throw new FirebaseAuthError( + AuthClientErrorCode.MISSING_CONTINUE_URI, + ); + } else if (!validator.isURL(actionCodeSettings.url)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONTINUE_URI, + ); + } + this.continueUrl = actionCodeSettings.url; + + if (typeof actionCodeSettings.handleCodeInApp !== 'undefined' && + !validator.isBoolean(actionCodeSettings.handleCodeInApp)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"ActionCodeSettings.handleCodeInApp" must be a boolean.', + ); + } + this.canHandleCodeInApp = actionCodeSettings.handleCodeInApp || false; + + if (typeof actionCodeSettings.dynamicLinkDomain !== 'undefined' && + !validator.isNonEmptyString(actionCodeSettings.dynamicLinkDomain)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_DYNAMIC_LINK_DOMAIN, + ); + } + this.dynamicLinkDomain = actionCodeSettings.dynamicLinkDomain; + + if (typeof actionCodeSettings.iOS !== 'undefined') { + if (!validator.isNonNullObject(actionCodeSettings.iOS)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"ActionCodeSettings.iOS" must be a valid non-null object.', + ); + } else if (typeof actionCodeSettings.iOS.bundleId === 'undefined') { + throw new FirebaseAuthError( + AuthClientErrorCode.MISSING_IOS_BUNDLE_ID, + ); + } else if (!validator.isNonEmptyString(actionCodeSettings.iOS.bundleId)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"ActionCodeSettings.iOS.bundleId" must be a valid non-empty string.', + ); + } + this.ibi = actionCodeSettings.iOS.bundleId; + } + + if (typeof actionCodeSettings.android !== 'undefined') { + if (!validator.isNonNullObject(actionCodeSettings.android)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"ActionCodeSettings.android" must be a valid non-null object.', + ); + } else if (typeof actionCodeSettings.android.packageName === 'undefined') { + throw new FirebaseAuthError( + AuthClientErrorCode.MISSING_ANDROID_PACKAGE_NAME, + ); + } else if (!validator.isNonEmptyString(actionCodeSettings.android.packageName)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"ActionCodeSettings.android.packageName" must be a valid non-empty string.', + ); + } else if (typeof actionCodeSettings.android.minimumVersion !== 'undefined' && + !validator.isNonEmptyString(actionCodeSettings.android.minimumVersion)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"ActionCodeSettings.android.minimumVersion" must be a valid non-empty string.', + ); + } else if (typeof actionCodeSettings.android.installApp !== 'undefined' && + !validator.isBoolean(actionCodeSettings.android.installApp)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"ActionCodeSettings.android.installApp" must be a valid boolean.', + ); + } + this.apn = actionCodeSettings.android.packageName; + this.amv = actionCodeSettings.android.minimumVersion; + this.installApp = actionCodeSettings.android.installApp || false; + } + } + + /** + * Returns the corresponding constructed server request corresponding to the + * current ActionCodeSettings. + * + * @returns The constructed EmailActionCodeRequest request. + */ + public buildRequest(): EmailActionCodeRequest { + const request: { [key: string]: any } = { + continueUrl: this.continueUrl, + canHandleCodeInApp: this.canHandleCodeInApp, + dynamicLinkDomain: this.dynamicLinkDomain, + androidPackageName: this.apn, + androidMinimumVersion: this.amv, + androidInstallApp: this.installApp, + iOSBundleId: this.ibi, + }; + // Remove all null and undefined fields from request. + for (const key in request) { + if (Object.prototype.hasOwnProperty.call(request, key)) { + if (typeof request[key] === 'undefined' || request[key] === null) { + delete request[key]; + } + } + } + return request as EmailActionCodeRequest; + } +} diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index 3c747bacb4..9fd535777c 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"); @@ -16,28 +17,39 @@ import * as validator from '../utils/validator'; -import {deepCopy} from '../utils/deep-copy'; -import {FirebaseApp} from '../firebase-app'; -import {AuthClientErrorCode, FirebaseAuthError, FirebaseError} from '../utils/error'; +import { App } from '../app/index'; +import { FirebaseApp } from '../app/firebase-app'; +import { deepCopy, deepExtend } from '../utils/deep-copy'; +import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; import { - HttpMethod, SignedApiRequestHandler, ApiSettings, + ApiSettings, AuthorizedHttpClient, HttpRequestConfig, HttpError, } from '../utils/api-request'; -import {CreateRequest, UpdateRequest} from './user-record'; +import * as utils from '../utils/index'; +import { + UserImportOptions, UserImportRecord, UserImportResult, + UserImportBuilder, AuthFactorInfo, convertMultiFactorInfoToServerFormat, +} from './user-import-builder'; +import { ActionCodeSettings, ActionCodeSettingsBuilder } from './action-code-settings-builder'; +import { Tenant, TenantServerResponse, CreateTenantRequest, UpdateTenantRequest } from './tenant'; +import { + isUidIdentifier, isEmailIdentifier, isPhoneIdentifier, isProviderIdentifier, + UserIdentifier, UidIdentifier, EmailIdentifier,PhoneIdentifier, ProviderIdentifier, +} from './identifier'; +import { + SAMLConfig, OIDCConfig, OIDCConfigServerResponse, SAMLConfigServerResponse, + OIDCConfigServerRequest, SAMLConfigServerRequest, CreateRequest, UpdateRequest, + OIDCAuthProviderConfig, SAMLAuthProviderConfig, OIDCUpdateAuthProviderRequest, + SAMLUpdateAuthProviderRequest +} from './auth-config'; +import { ProjectConfig, ProjectConfigServerResponse, UpdateProjectConfigRequest } from './project-config'; -/** Firebase Auth backend host. */ -const FIREBASE_AUTH_HOST = 'www.googleapis.com'; -/** Firebase Auth backend port number. */ -const FIREBASE_AUTH_PORT = 443; -/** Firebase Auth backend path. */ -const FIREBASE_AUTH_PATH = '/identitytoolkit/v3/relyingparty/'; /** Firebase Auth request header. */ const FIREBASE_AUTH_HEADER = { - 'Content-Type': 'application/json', - 'X-Client-Version': 'Node/Admin/', + 'X-Client-Version': `Node/Admin/${utils.getSdkVersion()}`, }; /** Firebase Auth request timeout duration in milliseconds. */ -const FIREBASE_AUTH_TIMEOUT = 10000; +const FIREBASE_AUTH_TIMEOUT = 25000; /** List of reserved claims which cannot be provided when creating a custom token. */ @@ -46,147 +58,556 @@ export const RESERVED_CLAIMS = [ 'iss', 'jti', 'nbf', 'nonce', 'sub', 'firebase', ]; +/** List of supported email action request types. */ +export const EMAIL_ACTION_REQUEST_TYPES = [ + 'PASSWORD_RESET', 'VERIFY_EMAIL', 'EMAIL_SIGNIN', 'VERIFY_AND_CHANGE_EMAIL', +]; + /** Maximum allowed number of characters in the custom claims payload. */ const MAX_CLAIMS_PAYLOAD_SIZE = 1000; /** Maximum allowed number of users to batch download at one time. */ const MAX_DOWNLOAD_ACCOUNT_PAGE_SIZE = 1000; +/** Maximum allowed number of users to batch upload at one time. */ +const MAX_UPLOAD_ACCOUNT_BATCH_SIZE = 1000; + +/** Maximum allowed number of users to batch get at one time. */ +const MAX_GET_ACCOUNTS_BATCH_SIZE = 100; + +/** Maximum allowed number of users to batch delete at one time. */ +const MAX_DELETE_ACCOUNTS_BATCH_SIZE = 1000; + +/** Minimum allowed session cookie duration in seconds (5 minutes). */ +const MIN_SESSION_COOKIE_DURATION_SECS = 5 * 60; + +/** Maximum allowed session cookie duration in seconds (2 weeks). */ +const MAX_SESSION_COOKIE_DURATION_SECS = 14 * 24 * 60 * 60; + +/** Maximum allowed number of provider configurations to batch download at one time. */ +const MAX_LIST_PROVIDER_CONFIGURATION_PAGE_SIZE = 100; + +/** The Firebase Auth backend base URL format. */ +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; + /** - * Validates a create/edit request object. All unsupported parameters + * Enum for the user write operation type. + */ +enum WriteOperationType { + Create = 'create', + Update = 'update', + Upload = 'upload', +} + + +/** Defines a base utility to help with resource URL construction. */ +class AuthResourceUrlBuilder { + + protected urlFormat: string; + private projectId: string; + + /** + * The resource URL builder constructor. + * + * @param projectId - The resource project ID. + * @param version - The endpoint API version. + * @constructor + */ + constructor(protected app: App, protected version: string = 'v1') { + if (useEmulator()) { + this.urlFormat = utils.formatString(FIREBASE_AUTH_EMULATOR_BASE_URL_FORMAT, { + host: emulatorHost() + }); + } else { + this.urlFormat = FIREBASE_AUTH_BASE_URL_FORMAT; + } + } + + /** + * Returns the resource URL corresponding to the provided parameters. + * + * @param api - The backend API name. + * @param params - The optional additional parameters to substitute in the + * URL path. + * @returns The corresponding resource URL. + */ + public getUrl(api?: string, params?: object): Promise { + return this.getProjectId() + .then((projectId) => { + const baseParams = { + version: this.version, + projectId, + api: api || '', + }; + const baseUrl = utils.formatString(this.urlFormat, baseParams); + // Substitute additional api related parameters. + return utils.formatString(baseUrl, params || {}); + }); + } + + private getProjectId(): Promise { + if (this.projectId) { + return Promise.resolve(this.projectId); + } + + return utils.findProjectId(this.app) + .then((projectId) => { + if (!validator.isNonEmptyString(projectId)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CREDENTIAL, + 'Failed to determine project ID for Auth. Initialize the ' + + 'SDK with service account credentials or set project ID as an app option. ' + + 'Alternatively set the GOOGLE_CLOUD_PROJECT environment variable.', + ); + } + + this.projectId = projectId; + return projectId; + }); + } +} + + +/** Tenant aware resource builder utility. */ +class TenantAwareAuthResourceUrlBuilder extends AuthResourceUrlBuilder { + /** + * The tenant aware resource URL builder constructor. + * + * @param projectId - The resource project ID. + * @param version - The endpoint API version. + * @param tenantId - The tenant ID. + * @constructor + */ + constructor(protected app: App, protected version: string, protected tenantId: string) { + super(app, version); + if (useEmulator()) { + this.urlFormat = utils.formatString(FIREBASE_AUTH_EMULATOR_TENANT_URL_FORMAT, { + host: emulatorHost() + }); + } else { + this.urlFormat = FIREBASE_AUTH_TENANT_URL_FORMAT; + } + } + + /** + * Returns the resource URL corresponding to the provided parameters. + * + * @param api - The backend API name. + * @param params - The optional additional parameters to substitute in the + * URL path. + * @returns The corresponding resource URL. + */ + public getUrl(api?: string, params?: object): Promise { + return super.getUrl(api, params) + .then((url) => { + return utils.formatString(url, { tenantId: this.tenantId }); + }); + } +} + +/** + * 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 * are removed from the original request. If an invalid field is passed * an error is thrown. * - * @param {any} request The create/edit request object. + * @param request - The AuthFactorInfo request object. */ -function validateCreateEditRequest(request: any) { - // Hash set of whitelisted parameters. - const validKeys = { - displayName: true, - localId: true, - email: true, - password: true, - rawPassword: true, - emailVerified: true, - photoUrl: true, - disabled: true, - disableUser: true, - deleteAttribute: true, - deleteProvider: true, - sanityCheck: true, - phoneNumber: true, - customAttributes: true, - validSince: true, - }; - // Remove invalid keys from original request. - for (const key in request) { - if (!(key in validKeys)) { - delete request[key]; - } +function validateAuthFactorInfo(request: AuthFactorInfo): void { + const validKeys = { + mfaEnrollmentId: true, + displayName: true, + phoneInfo: true, + enrolledAt: true, + }; + // Remove unsupported keys from the original request. + for (const key in request) { + if (!(key in validKeys)) { + delete request[key]; } - // For any invalid parameter, use the external key name in the error description. - // displayName should be a string. - if (typeof request.displayName !== 'undefined' && - typeof request.displayName !== 'string') { - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_DISPLAY_NAME); + } + // No enrollment ID is available for signupNewUser. Use another identifier. + const authFactorInfoIdentifier = + request.mfaEnrollmentId || request.phoneInfo || JSON.stringify(request); + // 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, + 'The second factor "uid" must be a valid non-empty string.', + ); + } + if (typeof request.displayName !== 'undefined' && + !validator.isString(request.displayName)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_DISPLAY_NAME, + `The second factor "displayName" for "${authFactorInfoIdentifier}" must be a valid string.`, + ); + } + // enrolledAt must be a valid UTC date string. + if (typeof request.enrolledAt !== 'undefined' && + !validator.isISODateString(request.enrolledAt)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ENROLLMENT_TIME, + `The second factor "enrollmentTime" for "${authFactorInfoIdentifier}" must be a valid ` + + 'UTC date string.'); + } + // Validate required fields depending on second factor type. + if (typeof request.phoneInfo !== 'undefined') { + // phoneNumber should be a string and a valid phone number. + if (!validator.isPhoneNumber(request.phoneInfo)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_PHONE_NUMBER, + `The second factor "phoneNumber" for "${authFactorInfoIdentifier}" must be a non-empty ` + + 'E.164 standard compliant identifier string.'); } - if (typeof request.localId !== 'undefined' && !validator.isUid(request.localId)) { - // This is called localId on the backend but the developer specifies this as - // uid externally. So the error message should use the client facing name. - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_UID); + } 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.'); + } +} + + +/** + * Validates a providerUserInfo object. All unsupported parameters + * are removed from the original request. If an invalid field is passed + * an error is thrown. + * + * @param request - The providerUserInfo request object. + */ +function validateProviderUserInfo(request: any): void { + const validKeys = { + rawId: true, + providerId: true, + email: true, + displayName: true, + photoUrl: true, + }; + // Remove invalid keys from original request. + for (const key in request) { + if (!(key in validKeys)) { + delete request[key]; } - // email should be a string and a valid email. - if (typeof request.email !== 'undefined' && !validator.isEmail(request.email)) { - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL); + } + if (!validator.isNonEmptyString(request.providerId)) { + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID); + } + if (typeof request.displayName !== 'undefined' && + typeof request.displayName !== 'string') { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_DISPLAY_NAME, + `The provider "displayName" for "${request.providerId}" must be a valid string.`, + ); + } + if (!validator.isNonEmptyString(request.rawId)) { + // This is called localId on the backend but the developer specifies this as + // uid externally. So the error message should use the client facing name. + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_UID, + `The provider "uid" for "${request.providerId}" must be a valid non-empty string.`, + ); + } + // email should be a string and a valid email. + if (typeof request.email !== 'undefined' && !validator.isEmail(request.email)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_EMAIL, + `The provider "email" for "${request.providerId}" must be a valid email string.`, + ); + } + // photoUrl should be a URL. + if (typeof request.photoUrl !== 'undefined' && + !validator.isURL(request.photoUrl)) { + // This is called photoUrl on the backend but the developer specifies this as + // photoURL externally. So the error message should use the client facing name. + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_PHOTO_URL, + `The provider "photoURL" for "${request.providerId}" must be a valid URL string.`, + ); + } +} + + +/** + * Validates a create/edit request object. All unsupported parameters + * are removed from the original request. If an invalid field is passed + * an error is thrown. + * + * @param request - The create/edit request object. + * @param writeOperationType - The write operation type. + */ +function validateCreateEditRequest(request: any, writeOperationType: WriteOperationType): void { + const uploadAccountRequest = writeOperationType === WriteOperationType.Upload; + // Hash set of whitelisted parameters. + const validKeys = { + displayName: true, + localId: true, + email: true, + password: true, + rawPassword: true, + emailVerified: true, + photoUrl: true, + disabled: true, + disableUser: true, + deleteAttribute: true, + deleteProvider: true, + sanityCheck: true, + 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, + salt: uploadAccountRequest, + createdAt: uploadAccountRequest, + lastLoginAt: uploadAccountRequest, + providerUserInfo: uploadAccountRequest, + mfaInfo: uploadAccountRequest, + // Only for non-uploadAccount requests. + mfa: !uploadAccountRequest, + }; + // Remove invalid keys from original request. + for (const key in request) { + if (!(key in validKeys)) { + delete request[key]; } - // phoneNumber should be a string and a valid phone number. - if (typeof request.phoneNumber !== 'undefined' && - !validator.isPhoneNumber(request.phoneNumber)) { - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER); + } + if (typeof request.tenantId !== 'undefined' && + !validator.isNonEmptyString(request.tenantId)) { + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_TENANT_ID); + } + // For any invalid parameter, use the external key name in the error description. + // displayName should be a string. + if (typeof request.displayName !== 'undefined' && + !validator.isString(request.displayName)) { + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_DISPLAY_NAME); + } + if ((typeof request.localId !== 'undefined' || uploadAccountRequest) && + !validator.isUid(request.localId)) { + // This is called localId on the backend but the developer specifies this as + // uid externally. So the error message should use the client facing name. + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_UID); + } + // email should be a string and a valid email. + if (typeof request.email !== 'undefined' && !validator.isEmail(request.email)) { + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL); + } + // phoneNumber should be a string and a valid phone number. + if (typeof request.phoneNumber !== 'undefined' && + !validator.isPhoneNumber(request.phoneNumber)) { + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER); + } + // password should be a string and a minimum of 6 chars. + if (typeof request.password !== 'undefined' && + !validator.isPassword(request.password)) { + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD); + } + // rawPassword should be a string and a minimum of 6 chars. + if (typeof request.rawPassword !== 'undefined' && + !validator.isPassword(request.rawPassword)) { + // This is called rawPassword on the backend but the developer specifies this as + // password externally. So the error message should use the client facing name. + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD); + } + // emailVerified should be a boolean. + if (typeof request.emailVerified !== 'undefined' && + typeof request.emailVerified !== 'boolean') { + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL_VERIFIED); + } + // photoUrl should be a URL. + if (typeof request.photoUrl !== 'undefined' && + !validator.isURL(request.photoUrl)) { + // This is called photoUrl on the backend but the developer specifies this as + // photoURL externally. So the error message should use the client facing name. + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PHOTO_URL); + } + // disabled should be a boolean. + if (typeof request.disabled !== 'undefined' && + typeof request.disabled !== 'boolean') { + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_DISABLED_FIELD); + } + // validSince should be a number. + if (typeof request.validSince !== 'undefined' && + !validator.isNumber(request.validSince)) { + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_TOKENS_VALID_AFTER_TIME); + } + // createdAt should be a number. + if (typeof request.createdAt !== 'undefined' && + !validator.isNumber(request.createdAt)) { + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_CREATION_TIME); + } + // lastSignInAt should be a number. + if (typeof request.lastLoginAt !== 'undefined' && + !validator.isNumber(request.lastLoginAt)) { + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_LAST_SIGN_IN_TIME); + } + // disableUser should be a boolean. + if (typeof request.disableUser !== 'undefined' && + typeof request.disableUser !== 'boolean') { + // This is called disableUser on the backend but the developer specifies this as + // disabled externally. So the error message should use the client facing name. + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_DISABLED_FIELD); + } + // customAttributes should be stringified JSON with no blacklisted claims. + // The payload should not exceed 1KB. + if (typeof request.customAttributes !== 'undefined') { + let developerClaims: object; + try { + developerClaims = JSON.parse(request.customAttributes); + } catch (error) { + // JSON parsing error. This should never happen as we stringify the claims internally. + // However, we still need to check since setAccountInfo via edit requests could pass + // this field. + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_CLAIMS, error.message); } - // password should be a string and a minimum of 6 chars. - if (typeof request.password !== 'undefined' && - !validator.isPassword(request.password)) { - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD); - } - // rawPassword should be a string and a minimum of 6 chars. - if (typeof request.rawPassword !== 'undefined' && - !validator.isPassword(request.rawPassword)) { - // This is called rawPassword on the backend but the developer specifies this as - // password externally. So the error message should use the client facing name. - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD); - } - // emailVerified should be a boolean. - if (typeof request.emailVerified !== 'undefined' && - typeof request.emailVerified !== 'boolean') { - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL_VERIFIED); - } - // photoUrl should be a URL. - if (typeof request.photoUrl !== 'undefined' && - !validator.isURL(request.photoUrl)) { - // This is called photoUrl on the backend but the developer specifies this as - // photoURL externally. So the error message should use the client facing name. - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PHOTO_URL); - } - // disabled should be a boolean. - if (typeof request.disabled !== 'undefined' && - typeof request.disabled !== 'boolean') { - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_DISABLED_FIELD); - } - // validSince should be a number. - if (typeof request.validSince !== 'undefined' && - !validator.isNumber(request.validSince)) { - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_TOKENS_VALID_AFTER_TIME); - } - // disableUser should be a boolean. - if (typeof request.disableUser !== 'undefined' && - typeof request.disableUser !== 'boolean') { - // This is called disableUser on the backend but the developer specifies this as - // disabled externally. So the error message should use the client facing name. - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_DISABLED_FIELD); - } - // customAttributes should be stringified JSON with no blacklisted claims. - // The payload should not exceed 1KB. - if (typeof request.customAttributes !== 'undefined') { - let developerClaims; - try { - developerClaims = JSON.parse(request.customAttributes); - } catch (error) { - // JSON parsing error. This should never happen as we stringify the claims internally. - // However, we still need to check since setAccountInfo via edit requests could pass - // this field. - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_CLAIMS, error.message); + const invalidClaims: string[] = []; + // Check for any invalid claims. + RESERVED_CLAIMS.forEach((blacklistedClaim) => { + if (Object.prototype.hasOwnProperty.call(developerClaims, blacklistedClaim)) { + invalidClaims.push(blacklistedClaim); } - const invalidClaims = []; - // Check for any invalid claims. - RESERVED_CLAIMS.forEach((blacklistedClaim) => { - if (developerClaims.hasOwnProperty(blacklistedClaim)) { - invalidClaims.push(blacklistedClaim); - } - }); - // Throw an error if an invalid claim is detected. - if (invalidClaims.length > 0) { - throw new FirebaseAuthError( - AuthClientErrorCode.FORBIDDEN_CLAIM, - invalidClaims.length > 1 ? + }); + // Throw an error if an invalid claim is detected. + if (invalidClaims.length > 0) { + throw new FirebaseAuthError( + AuthClientErrorCode.FORBIDDEN_CLAIM, + invalidClaims.length > 1 ? `Developer claims "${invalidClaims.join('", "')}" are reserved and cannot be specified.` : `Developer claim "${invalidClaims[0]}" is reserved and cannot be specified.`, - ); - } - // Check claims payload does not exceed maxmimum size. - if (request.customAttributes.length > MAX_CLAIMS_PAYLOAD_SIZE) { - throw new FirebaseAuthError( - AuthClientErrorCode.CLAIMS_TOO_LARGE, - `Developer claims payload should not exceed ${MAX_CLAIMS_PAYLOAD_SIZE} characters.`, - ); - } + ); + } + // Check claims payload does not exceed maxmimum size. + if (request.customAttributes.length > MAX_CLAIMS_PAYLOAD_SIZE) { + throw new FirebaseAuthError( + AuthClientErrorCode.CLAIMS_TOO_LARGE, + `Developer claims payload should not exceed ${MAX_CLAIMS_PAYLOAD_SIZE} characters.`, + ); + } + } + // passwordHash has to be a base64 encoded string. + if (typeof request.passwordHash !== 'undefined' && + !validator.isString(request.passwordHash)) { + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD_HASH); + } + // salt has to be a base64 encoded string. + if (typeof request.salt !== 'undefined' && + !validator.isString(request.salt)) { + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD_SALT); + } + // providerUserInfo has to be an array of valid UserInfo requests. + if (typeof request.providerUserInfo !== 'undefined' && + !validator.isArray(request.providerUserInfo)) { + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_DATA); + } else if (validator.isArray(request.providerUserInfo)) { + request.providerUserInfo.forEach((providerUserInfoEntry: any) => { + 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. + let enrollments: AuthFactorInfo[] | null = null; + if (request.mfaInfo) { + enrollments = request.mfaInfo; + } else if (request.mfa && request.mfa.enrollments) { + enrollments = request.mfa.enrollments; + } + if (enrollments) { + if (!validator.isArray(enrollments)) { + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_ENROLLED_FACTORS); } + enrollments.forEach((authFactorInfoEntry: AuthFactorInfo) => { + validateAuthFactorInfo(authFactorInfoEntry); + }); + } } -/** Instantiates the downloadAccount endpoint settings. */ -export const FIREBASE_AUTH_DOWNLOAD_ACCOUNT = new ApiSettings('downloadAccount', 'POST') +/** + * Instantiates the createSessionCookie endpoint settings. + * + * @internal + */ +export const FIREBASE_AUTH_CREATE_SESSION_COOKIE = + new ApiSettings(':createSessionCookie', 'POST') + // Set request validator. + .setRequestValidator((request: any) => { + // Validate the ID token is a non-empty string. + if (!validator.isNonEmptyString(request.idToken)) { + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_ID_TOKEN); + } + // Validate the custom session cookie duration. + if (!validator.isNumber(request.validDuration) || + request.validDuration < MIN_SESSION_COOKIE_DURATION_SECS || + request.validDuration > MAX_SESSION_COOKIE_DURATION_SECS) { + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_SESSION_COOKIE_DURATION); + } + }) + // Set response validator. + .setResponseValidator((response: any) => { + // Response should always contain the session cookie. + if (!validator.isNonEmptyString(response.sessionCookie)) { + throw new FirebaseAuthError(AuthClientErrorCode.INTERNAL_ERROR); + } + }); + + +/** + * Instantiates the uploadAccount endpoint settings. + * + * @internal + */ +export const FIREBASE_AUTH_UPLOAD_ACCOUNT = new ApiSettings('/accounts:batchCreate', 'POST'); + + +/** + * Instantiates the downloadAccount endpoint settings. + * + * @internal + */ +export const FIREBASE_AUTH_DOWNLOAD_ACCOUNT = new ApiSettings('/accounts:batchGet', 'GET') // Set request validator. .setRequestValidator((request: any) => { // Validate next page token. @@ -200,18 +621,31 @@ export const FIREBASE_AUTH_DOWNLOAD_ACCOUNT = new ApiSettings('downloadAccount', request.maxResults > MAX_DOWNLOAD_ACCOUNT_PAGE_SIZE) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `Required "maxResults" must be a positive non-zero number that does not exceed ` + - `the allowed ${MAX_DOWNLOAD_ACCOUNT_PAGE_SIZE}.`, + 'Required "maxResults" must be a positive integer that does not exceed ' + + `${MAX_DOWNLOAD_ACCOUNT_PAGE_SIZE}.`, ); } }); +interface GetAccountInfoRequest { + localId?: string[]; + email?: string[]; + phoneNumber?: string[]; + federatedUserId?: Array<{ + providerId: string; + rawId: string; + }>; +} -/** Instantiates the getAccountInfo endpoint settings. */ -export const FIREBASE_AUTH_GET_ACCOUNT_INFO = new ApiSettings('getAccountInfo', 'POST') +/** + * Instantiates the getAccountInfo endpoint settings. + * + * @internal + */ +export const FIREBASE_AUTH_GET_ACCOUNT_INFO = new ApiSettings('/accounts:lookup', 'POST') // Set request validator. - .setRequestValidator((request: any) => { - if (!request.localId && !request.email && !request.phoneNumber) { + .setRequestValidator((request: GetAccountInfoRequest) => { + if (!request.localId && !request.email && !request.phoneNumber && !request.federatedUserId) { throw new FirebaseAuthError( AuthClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Server request is missing user identifier'); @@ -219,13 +653,34 @@ export const FIREBASE_AUTH_GET_ACCOUNT_INFO = new ApiSettings('getAccountInfo', }) // Set response validator. .setResponseValidator((response: any) => { - if (!response.users) { + if (!response.users || !response.users.length) { throw new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); } }); -/** Instantiates the deleteAccount endpoint settings. */ -export const FIREBASE_AUTH_DELETE_ACCOUNT = new ApiSettings('deleteAccount', 'POST') +/** + * Instantiates the getAccountInfo endpoint settings for use when fetching info + * for multiple accounts. + * + * @internal + */ +export const FIREBASE_AUTH_GET_ACCOUNTS_INFO = new ApiSettings('/accounts:lookup', 'POST') + // Set request validator. + .setRequestValidator((request: GetAccountInfoRequest) => { + if (!request.localId && !request.email && !request.phoneNumber && !request.federatedUserId) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Server request is missing user identifier'); + } + }); + + +/** + * Instantiates the deleteAccount endpoint settings. + * + * @internal + */ +export const FIREBASE_AUTH_DELETE_ACCOUNT = new ApiSettings('/accounts:delete', 'POST') // Set request validator. .setRequestValidator((request: any) => { if (!request.localId) { @@ -235,8 +690,60 @@ export const FIREBASE_AUTH_DELETE_ACCOUNT = new ApiSettings('deleteAccount', 'PO } }); -/** Instantiates the setAccountInfo endpoint settings for updating existing accounts. */ -export const FIREBASE_AUTH_SET_ACCOUNT_INFO = new ApiSettings('setAccountInfo', 'POST') +interface BatchDeleteAccountsRequest { + localIds?: string[]; + force?: boolean; +} + +interface BatchDeleteErrorInfo { + index?: number; + localId?: string; + message?: string; +} + +export interface BatchDeleteAccountsResponse { + errors?: BatchDeleteErrorInfo[]; +} + +/** + * @internal + */ +export const FIREBASE_AUTH_BATCH_DELETE_ACCOUNTS = new ApiSettings('/accounts:batchDelete', 'POST') + .setRequestValidator((request: BatchDeleteAccountsRequest) => { + if (!request.localIds) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Server request is missing user identifiers'); + } + if (typeof request.force === 'undefined' || request.force !== true) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Server request is missing force=true field'); + } + }) + .setResponseValidator((response: BatchDeleteAccountsResponse) => { + const errors = response.errors || []; + errors.forEach((batchDeleteErrorInfo) => { + if (typeof batchDeleteErrorInfo.index === 'undefined') { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Server BatchDeleteAccountResponse is missing an errors.index field'); + } + if (!batchDeleteErrorInfo.localId) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Server BatchDeleteAccountResponse is missing an errors.localId field'); + } + // Allow the (error) message to be missing/undef. + }); + }); + +/** + * Instantiates the setAccountInfo endpoint settings for updating existing accounts. + * + * @internal + */ +export const FIREBASE_AUTH_SET_ACCOUNT_INFO = new ApiSettings('/accounts:update', 'POST') // Set request validator. .setRequestValidator((request: any) => { // localId is a required parameter. @@ -245,7 +752,13 @@ export const FIREBASE_AUTH_SET_ACCOUNT_INFO = new ApiSettings('setAccountInfo', AuthClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Server request is missing user identifier'); } - validateCreateEditRequest(request); + // Throw error when tenantId is passed in POST body. + if (typeof request.tenantId !== 'undefined') { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"tenantId" is an invalid "UpdateRequest" property.'); + } + validateCreateEditRequest(request, WriteOperationType.Update); }) // Set response validator. .setResponseValidator((response: any) => { @@ -258,25 +771,33 @@ export const FIREBASE_AUTH_SET_ACCOUNT_INFO = new ApiSettings('setAccountInfo', /** * Instantiates the signupNewUser endpoint settings for creating a new user with or without * uid being specified. The backend will create a new one if not provided and return it. + * + * @internal */ -export const FIREBASE_AUTH_SIGN_UP_NEW_USER = new ApiSettings('signupNewUser', 'POST') +export const FIREBASE_AUTH_SIGN_UP_NEW_USER = new ApiSettings('/accounts', 'POST') // Set request validator. .setRequestValidator((request: any) => { // signupNewUser does not support customAttributes. 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.', ); } - validateCreateEditRequest(request); + // Throw error when tenantId is passed in POST body. + if (typeof request.tenantId !== 'undefined') { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"tenantId" is an invalid "CreateRequest" property.'); + } + validateCreateEditRequest(request, WriteOperationType.Create); }) // Set response validator. .setResponseValidator((response: any) => { @@ -288,38 +809,302 @@ export const FIREBASE_AUTH_SIGN_UP_NEW_USER = new ApiSettings('signupNewUser', ' } }); +const FIREBASE_AUTH_GET_OOB_CODE = new ApiSettings('/accounts:sendOobCode', 'POST') + // Set request validator. + .setRequestValidator((request: any) => { + if (!validator.isEmail(request.email)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_EMAIL, + ); + } + if (typeof request.newEmail !== 'undefined' && !validator.isEmail(request.newEmail)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_NEW_EMAIL, + ); + } + if (EMAIL_ACTION_REQUEST_TYPES.indexOf(request.requestType) === -1) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + `"${request.requestType}" is not a supported email action request type.`, + ); + } + }) + // Set response validator. + .setResponseValidator((response: any) => { + // If the oobLink is not returned, then the request failed. + if (!response.oobLink) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Unable to create the email action link'); + } + }); + /** - * Class that provides mechanism to send requests to the Firebase Auth backend endpoints. + * Instantiates the retrieve OIDC configuration endpoint settings. + * + * @internal */ -export class FirebaseAuthRequestHandler { - private host: string = FIREBASE_AUTH_HOST; - private port: number = FIREBASE_AUTH_PORT; - private path: string = FIREBASE_AUTH_PATH; - private headers: object = FIREBASE_AUTH_HEADER; - private timeout: number = FIREBASE_AUTH_TIMEOUT; - private signedApiRequestHandler: SignedApiRequestHandler; - - /** - * @param {any} response The response to check for errors. - * @return {string|null} The error code if present; null otherwise. - */ - private static getErrorCode(response: any): string | null { - return (validator.isNonNullObject(response) && response.error && (response.error as any).message) || null; - } +const GET_OAUTH_IDP_CONFIG = new ApiSettings('/oauthIdpConfigs/{providerId}', 'GET') + // Set response validator. + .setResponseValidator((response: any) => { + // Response should always contain the OIDC provider resource name. + if (!validator.isNonEmptyString(response.name)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Unable to get OIDC configuration', + ); + } + }); - /** - * @param {FirebaseApp} app The app used to fetch access tokens to sign API requests. - * @constructor - */ - constructor(app: FirebaseApp) { - this.signedApiRequestHandler = new SignedApiRequestHandler(app); - } +/** + * Instantiates the delete OIDC configuration endpoint settings. + * + * @internal + */ +const DELETE_OAUTH_IDP_CONFIG = new ApiSettings('/oauthIdpConfigs/{providerId}', 'DELETE'); + +/** + * Instantiates the create OIDC configuration endpoint settings. + * + * @internal + */ +const CREATE_OAUTH_IDP_CONFIG = new ApiSettings('/oauthIdpConfigs?oauthIdpConfigId={providerId}', 'POST') + // Set response validator. + .setResponseValidator((response: any) => { + // Response should always contain the OIDC provider resource name. + if (!validator.isNonEmptyString(response.name)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Unable to create new OIDC configuration', + ); + } + }); + +/** + * Instantiates the update OIDC configuration endpoint settings. + * + * @internal + */ +const UPDATE_OAUTH_IDP_CONFIG = new ApiSettings('/oauthIdpConfigs/{providerId}?updateMask={updateMask}', 'PATCH') + // Set response validator. + .setResponseValidator((response: any) => { + // Response should always contain the configuration resource name. + if (!validator.isNonEmptyString(response.name)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Unable to update OIDC configuration', + ); + } + }); + +/** + * Instantiates the list OIDC configuration endpoint settings. + * + * @internal + */ +const LIST_OAUTH_IDP_CONFIGS = new ApiSettings('/oauthIdpConfigs', 'GET') + // Set request validator. + .setRequestValidator((request: any) => { + // Validate next page token. + if (typeof request.pageToken !== 'undefined' && + !validator.isNonEmptyString(request.pageToken)) { + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PAGE_TOKEN); + } + // Validate max results. + if (!validator.isNumber(request.pageSize) || + request.pageSize <= 0 || + 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 ' + + `${MAX_LIST_PROVIDER_CONFIGURATION_PAGE_SIZE}.`, + ); + } + }); + +/** + * Instantiates the retrieve SAML configuration endpoint settings. + * + * @internal + */ +const GET_INBOUND_SAML_CONFIG = new ApiSettings('/inboundSamlConfigs/{providerId}', 'GET') + // Set response validator. + .setResponseValidator((response: any) => { + // Response should always contain the SAML provider resource name. + if (!validator.isNonEmptyString(response.name)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Unable to get SAML configuration', + ); + } + }); + +/** + * Instantiates the delete SAML configuration endpoint settings. + * + * @internal + */ +const DELETE_INBOUND_SAML_CONFIG = new ApiSettings('/inboundSamlConfigs/{providerId}', 'DELETE'); + +/** + * Instantiates the create SAML configuration endpoint settings. + * + * @internal + */ +const CREATE_INBOUND_SAML_CONFIG = new ApiSettings('/inboundSamlConfigs?inboundSamlConfigId={providerId}', 'POST') + // Set response validator. + .setResponseValidator((response: any) => { + // Response should always contain the SAML provider resource name. + if (!validator.isNonEmptyString(response.name)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Unable to create new SAML configuration', + ); + } + }); + +/** + * Instantiates the update SAML configuration endpoint settings. + * + * @internal + */ +const UPDATE_INBOUND_SAML_CONFIG = new ApiSettings('/inboundSamlConfigs/{providerId}?updateMask={updateMask}', 'PATCH') + // Set response validator. + .setResponseValidator((response: any) => { + // Response should always contain the configuration resource name. + if (!validator.isNonEmptyString(response.name)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Unable to update SAML configuration', + ); + } + }); + +/** + * Instantiates the list SAML configuration endpoint settings. + * + * @internal + */ +const LIST_INBOUND_SAML_CONFIGS = new ApiSettings('/inboundSamlConfigs', 'GET') + // Set request validator. + .setRequestValidator((request: any) => { + // Validate next page token. + if (typeof request.pageToken !== 'undefined' && + !validator.isNonEmptyString(request.pageToken)) { + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PAGE_TOKEN); + } + // Validate max results. + if (!validator.isNumber(request.pageSize) || + request.pageSize <= 0 || + 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 ' + + `${MAX_LIST_PROVIDER_CONFIGURATION_PAGE_SIZE}.`, + ); + } + }); + +/** + * Class that provides the mechanism to send requests to the Firebase Auth backend endpoints. + * + * @internal + */ +export abstract class AbstractAuthRequestHandler { + + protected readonly httpClient: AuthorizedHttpClient; + private authUrlBuilder: AuthResourceUrlBuilder; + private projectConfigUrlBuilder: AuthResourceUrlBuilder; + + /** + * @param response - The response to check for errors. + * @returns The error code if present; null otherwise. + */ + private static getErrorCode(response: any): string | null { + return (validator.isNonNullObject(response) && response.error && response.error.message) || null; + } + + private static addUidToRequest(id: UidIdentifier, request: GetAccountInfoRequest): GetAccountInfoRequest { + if (!validator.isUid(id.uid)) { + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_UID); + } + request.localId ? request.localId.push(id.uid) : request.localId = [id.uid]; + return request; + } + + private static addEmailToRequest(id: EmailIdentifier, request: GetAccountInfoRequest): GetAccountInfoRequest { + if (!validator.isEmail(id.email)) { + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL); + } + request.email ? request.email.push(id.email) : request.email = [id.email]; + return request; + } + + private static addPhoneToRequest(id: PhoneIdentifier, request: GetAccountInfoRequest): GetAccountInfoRequest { + if (!validator.isPhoneNumber(id.phoneNumber)) { + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER); + } + request.phoneNumber ? request.phoneNumber.push(id.phoneNumber) : request.phoneNumber = [id.phoneNumber]; + return request; + } + + private static addProviderToRequest(id: ProviderIdentifier, request: GetAccountInfoRequest): GetAccountInfoRequest { + if (!validator.isNonEmptyString(id.providerId)) { + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID); + } + if (!validator.isNonEmptyString(id.providerUid)) { + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_UID); + } + const federatedUserId = { + providerId: id.providerId, + rawId: id.providerUid, + }; + request.federatedUserId + ? request.federatedUserId.push(federatedUserId) + : request.federatedUserId = [federatedUserId]; + return request; + } + + /** + * @param app - The app used to fetch access tokens to sign API requests. + * @constructor + */ + constructor(protected readonly app: App) { + if (typeof app !== 'object' || app === null || !('options' in app)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + 'First argument passed to admin.auth() must be a valid Firebase app instance.', + ); + } + + this.httpClient = new AuthHttpClient(app as FirebaseApp); + } + + /** + * Creates a new Firebase session cookie with the specified duration that can be used for + * session management (set as a server side session cookie with custom cookie policy). + * The session cookie JWT will have the same payload claims as the provided ID token. + * + * @param idToken - The Firebase ID token to exchange for a session cookie. + * @param expiresIn - The session cookie duration in milliseconds. + * + * @returns A promise that resolves on success with the created session cookie. + */ + public createSessionCookie(idToken: string, expiresIn: number): Promise { + const request = { + idToken, + // Convert to seconds. + validDuration: expiresIn / 1000, + }; + return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_CREATE_SESSION_COOKIE, request) + .then((response: any) => response.sessionCookie); + } /** * Looks up a user by uid. * - * @param {string} uid The uid of the user to lookup. - * @return {Promise} A promise that resolves with the user information. + * @param uid - The uid of the user to lookup. + * @returns A promise that resolves with the user information. */ public getAccountInfoByUid(uid: string): Promise { if (!validator.isUid(uid)) { @@ -329,14 +1114,14 @@ export class FirebaseAuthRequestHandler { const request = { localId: [uid], }; - return this.invokeRequestHandler(FIREBASE_AUTH_GET_ACCOUNT_INFO, request); + return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_GET_ACCOUNT_INFO, request); } /** * Looks up a user by email. * - * @param {string} email The email of the user to lookup. - * @return {Promise} A promise that resolves with the user information. + * @param email - The email of the user to lookup. + * @returns A promise that resolves with the user information. */ public getAccountInfoByEmail(email: string): Promise { if (!validator.isEmail(email)) { @@ -346,14 +1131,14 @@ export class FirebaseAuthRequestHandler { const request = { email: [email], }; - return this.invokeRequestHandler(FIREBASE_AUTH_GET_ACCOUNT_INFO, request); + return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_GET_ACCOUNT_INFO, request); } /** * Looks up a user by phone number. * - * @param {string} phoneNumber The phone number of the user to lookup. - * @return {Promise} A promise that resolves with the user information. + * @param phoneNumber - The phone number of the user to lookup. + * @returns A promise that resolves with the user information. */ public getAccountInfoByPhoneNumber(phoneNumber: string): Promise { if (!validator.isPhoneNumber(phoneNumber)) { @@ -363,25 +1148,78 @@ export class FirebaseAuthRequestHandler { const request = { phoneNumber: [phoneNumber], }; - return this.invokeRequestHandler(FIREBASE_AUTH_GET_ACCOUNT_INFO, request); + 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). + * + * @param identifiers - The identifiers indicating the users + * to be looked up. Must have <= 100 entries. + * @param A - promise that resolves with the set of successfully + * looked up users. Possibly empty if no users were looked up. + */ + public getAccountInfoByIdentifiers(identifiers: UserIdentifier[]): Promise { + if (identifiers.length === 0) { + return Promise.resolve({ users: [] }); + } else if (identifiers.length > MAX_GET_ACCOUNTS_BATCH_SIZE) { + throw new FirebaseAuthError( + AuthClientErrorCode.MAXIMUM_USER_COUNT_EXCEEDED, + '`identifiers` parameter must have <= ' + MAX_GET_ACCOUNTS_BATCH_SIZE + ' entries.'); + } + + let request: GetAccountInfoRequest = {}; + + for (const id of identifiers) { + if (isUidIdentifier(id)) { + request = AbstractAuthRequestHandler.addUidToRequest(id, request); + } else if (isEmailIdentifier(id)) { + request = AbstractAuthRequestHandler.addEmailToRequest(id, request); + } else if (isPhoneIdentifier(id)) { + request = AbstractAuthRequestHandler.addPhoneToRequest(id, request); + } else if (isProviderIdentifier(id)) { + request = AbstractAuthRequestHandler.addProviderToRequest(id, request); + } else { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + 'Unrecognized identifier: ' + id); + } + } + + return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_GET_ACCOUNTS_INFO, request); } /** * Exports the users (single batch only) with a size of maxResults and starting from * the offset as specified by pageToken. * - * @param {number=} maxResults The page size, 1000 if undefined. This is also the maximum + * @param maxResults - The page size, 1000 if undefined. This is also the maximum * allowed limit. - * @param {string=} pageToken The next page token. If not specified, returns users starting + * @param pageToken - The next page token. If not specified, returns users starting * without any offset. Users are returned in the order they were created from oldest to * newest, relative to the page token offset. - * @return {Promise} A promise that resolves with the current batch of downloaded + * @returns A promise that resolves with the current batch of downloaded * users and the next page token if available. For the last page, an empty list of users * and no page token are returned. */ public downloadAccount( - maxResults: number = MAX_DOWNLOAD_ACCOUNT_PAGE_SIZE, - pageToken?: string): Promise<{users: object[], nextPageToken?: string}> { + maxResults: number = MAX_DOWNLOAD_ACCOUNT_PAGE_SIZE, + pageToken?: string): Promise<{users: object[]; nextPageToken?: string}> { // Construct request. const request = { maxResults, @@ -391,21 +1229,66 @@ export class FirebaseAuthRequestHandler { if (typeof request.nextPageToken === 'undefined') { delete request.nextPageToken; } - return this.invokeRequestHandler(FIREBASE_AUTH_DOWNLOAD_ACCOUNT, request) - .then((response: any) => { - // No more users available. - if (!response.users) { - response.users = []; - } - return response as {users: object[], nextPageToken?: string}; - }); + return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_DOWNLOAD_ACCOUNT, request) + .then((response: any) => { + // No more users available. + if (!response.users) { + response.users = []; + } + return response as {users: object[]; nextPageToken?: string}; + }); + } + + /** + * Imports the list of users provided to Firebase Auth. This is useful when + * migrating from an external authentication system without having to use the Firebase CLI SDK. + * At most, 1000 users are allowed to be imported one at a time. + * When importing a list of password users, UserImportOptions are required to be specified. + * + * @param users - The list of user records to import to Firebase Auth. + * @param options - The user import options, required when the users provided + * include password credentials. + * @returns A promise that resolves when the operation completes + * with the result of the import. This includes the number of successful imports, the number + * of failed uploads and their corresponding errors. + */ + public uploadAccount( + users: UserImportRecord[], options?: UserImportOptions): Promise { + // This will throw if any error is detected in the hash options. + // For errors in the list of users, this will not throw and will report the errors and the + // corresponding user index in the user import generated response below. + // No need to validate raw request or raw response as this is done in UserImportBuilder. + const userImportBuilder = new UserImportBuilder(users, options, (userRequest: any) => { + // Pass true to validate the uploadAccount specific fields. + validateCreateEditRequest(userRequest, WriteOperationType.Upload); + }); + const request = userImportBuilder.buildRequest(); + // Fail quickly if more users than allowed are to be imported. + if (validator.isArray(users) && users.length > MAX_UPLOAD_ACCOUNT_BATCH_SIZE) { + throw new FirebaseAuthError( + AuthClientErrorCode.MAXIMUM_USER_COUNT_EXCEEDED, + `A maximum of ${MAX_UPLOAD_ACCOUNT_BATCH_SIZE} users can be imported at once.`, + ); + } + // If no remaining user in request after client side processing, there is no need + // to send the request to the server. + if (!request.users || request.users.length === 0) { + return Promise.resolve(userImportBuilder.buildResponse([])); + } + return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_UPLOAD_ACCOUNT, request) + .then((response: any) => { + // No error object is returned if no error encountered. + const failedUploads = (response.error || []) as Array<{index: number; message: string}>; + // Rewrite response as UserImportResult and re-insert client previously detected errors. + return userImportBuilder.buildResponse(failedUploads); + }); } /** * Deletes an account identified by a uid. * - * @param {string} uid The uid of the user to delete. - * @return {Promise} A promise that resolves when the user is deleted. + * @param uid - The uid of the user to delete. + * @returns A promise that resolves when the user is deleted. */ public deleteAccount(uid: string): Promise { if (!validator.isUid(uid)) { @@ -415,18 +1298,42 @@ export class FirebaseAuthRequestHandler { const request = { localId: uid, }; - return this.invokeRequestHandler(FIREBASE_AUTH_DELETE_ACCOUNT, request); + return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_DELETE_ACCOUNT, request); + } + + public deleteAccounts(uids: string[], force: boolean): Promise { + if (uids.length === 0) { + return Promise.resolve({}); + } else if (uids.length > MAX_DELETE_ACCOUNTS_BATCH_SIZE) { + throw new FirebaseAuthError( + AuthClientErrorCode.MAXIMUM_USER_COUNT_EXCEEDED, + '`uids` parameter must have <= ' + MAX_DELETE_ACCOUNTS_BATCH_SIZE + ' entries.'); + } + + const request: BatchDeleteAccountsRequest = { + localIds: [], + force, + }; + + uids.forEach((uid) => { + if (!validator.isUid(uid)) { + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_UID); + } + request.localIds!.push(uid); + }); + + return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_BATCH_DELETE_ACCOUNTS, request); } /** * Sets additional developer claims on an existing user identified by provided UID. * - * @param {string} uid The user to edit. - * @param {object} customUserClaims The developer claims to set. - * @return {Promise} A promise that resolves when the operation completes + * @param uid - The user to edit. + * @param customUserClaims - The developer claims to set. + * @returns A promise that resolves when the operation completes * with the user id that was edited. */ - public setCustomUserClaims(uid: string, customUserClaims: object): Promise { + public setCustomUserClaims(uid: string, customUserClaims: object | null): Promise { // Validate user UID. if (!validator.isUid(uid)) { return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_UID)); @@ -447,18 +1354,18 @@ export class FirebaseAuthRequestHandler { localId: uid, customAttributes: JSON.stringify(customUserClaims), }; - return this.invokeRequestHandler(FIREBASE_AUTH_SET_ACCOUNT_INFO, request) - .then((response: any) => { - return response.localId as string; - }); + return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_SET_ACCOUNT_INFO, request) + .then((response: any) => { + return response.localId as string; + }); } /** * Edits an existing user. * - * @param {string} uid The user to edit. - * @param {object} properties The properties to set on the user. - * @return {Promise} A promise that resolves when the operation completes + * @param uid - The user to edit. + * @param properties - The properties to set on the user. + * @returns A promise that resolves when the operation completes * with the user id that was edited. */ public updateExistingAccount(uid: string, properties: UpdateRequest): Promise { @@ -471,6 +1378,33 @@ export class FirebaseAuthRequestHandler { '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. @@ -483,7 +1417,7 @@ export class FirebaseAuthRequestHandler { // Parameters that are deletable and their deleteAttribute names. // Use client facing names, photoURL instead of photoUrl. - const deletableParams = { + const deletableParams: {[key: string]: string} = { displayName: 'DISPLAY_NAME', photoURL: 'PHOTO_URL', }; @@ -505,13 +1439,25 @@ export class FirebaseAuthRequestHandler { // 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. @@ -524,10 +1470,32 @@ export class FirebaseAuthRequestHandler { request.disableUser = request.disabled; delete request.disabled; } - return this.invokeRequestHandler(FIREBASE_AUTH_SET_ACCOUNT_INFO, request) - .then((response: any) => { - return response.localId as string; - }); + // Construct mfa related user data. + if (validator.isNonNullObject(request.multiFactor)) { + if (request.multiFactor.enrolledFactors === null) { + // Remove all second factors. + request.mfa = {}; + } else if (validator.isArray(request.multiFactor.enrolledFactors)) { + request.mfa = { + enrollments: [], + }; + try { + request.multiFactor.enrolledFactors.forEach((multiFactorInfo: any) => { + request.mfa.enrollments.push(convertMultiFactorInfoToServerFormat(multiFactorInfo)); + }); + } catch (e) { + return Promise.reject(e); + } + if (request.mfa.enrollments.length === 0) { + delete request.mfa.enrollments; + } + } + delete request.multiFactor; + } + return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_SET_ACCOUNT_INFO, request) + .then((response: any) => { + return response.localId as string; + }); } /** @@ -539,8 +1507,8 @@ export class FirebaseAuthRequestHandler { * the same second as the revocation will still be valid. If there is a chance that a token * was minted in the last second, delay for 1 second before revoking. * - * @param {string} uid The user whose tokens are to be revoked. - * @return {Promise} A promise that resolves when the operation completes + * @param uid - The user whose tokens are to be revoked. + * @returns A promise that resolves when the operation completes * successfully with the user id of the corresponding user. */ public revokeRefreshTokens(uid: string): Promise { @@ -551,19 +1519,19 @@ export class FirebaseAuthRequestHandler { const request: any = { localId: uid, // validSince is in UTC seconds. - validSince: Math.ceil(new Date().getTime() / 1000), + validSince: Math.floor(new Date().getTime() / 1000), }; - return this.invokeRequestHandler(FIREBASE_AUTH_SET_ACCOUNT_INFO, request) - .then((response: any) => { - return response.localId as string; - }); + return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_SET_ACCOUNT_INFO, request) + .then((response: any) => { + return response.localId as string; + }); } /** * Create a new user with the properties supplied. * - * @param {object} properties The properties to set on the user. - * @return {Promise} A promise that resolves when the operation completes + * @param properties - The properties to set on the user. + * @returns A promise that resolves when the operation completes * with the user id that was created. */ public createNewAccount(properties: CreateRequest): Promise { @@ -577,7 +1545,12 @@ export class FirebaseAuthRequestHandler { } // 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; @@ -588,7 +1561,33 @@ export class FirebaseAuthRequestHandler { request.localId = request.uid; delete request.uid; } - return this.invokeRequestHandler(FIREBASE_AUTH_SIGN_UP_NEW_USER, request) + // Construct mfa related user data. + if (validator.isNonNullObject(request.multiFactor)) { + if (validator.isNonEmptyArray(request.multiFactor.enrolledFactors)) { + const mfaInfo: AuthFactorInfo[] = []; + try { + request.multiFactor.enrolledFactors.forEach((multiFactorInfo) => { + // Enrollment time and uid are not allowed for signupNewUser endpoint. + // They will automatically be provisioned server side. + if ('enrollmentTime' in multiFactorInfo) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"enrollmentTime" is not supported when adding second factors via "createUser()"'); + } else if ('uid' in multiFactorInfo) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"uid" is not supported when adding second factors via "createUser()"'); + } + mfaInfo.push(convertMultiFactorInfoToServerFormat(multiFactorInfo)); + }); + } catch (e) { + return Promise.reject(e); + } + request.mfaInfo = mfaInfo; + } + delete request.multiFactor; + } + return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_SIGN_UP_NEW_USER, request) .then((response: any) => { // Return the user id. return response.localId as string; @@ -596,45 +1595,729 @@ export class FirebaseAuthRequestHandler { } /** - * Invokes the request handler based on the API settings object passed. + * Generates the out of band email action link for the email specified using the action code settings provided. + * Returns a promise that resolves with the generated link. * - * @param {ApiSettings} apiSettings The API endpoint settings to apply to request and response. - * @param {object} requestData The request data. - * @return {Promise} A promise that resolves with the response. + * @param requestType - The request type. This could be either used for password reset, + * email verification, email link sign-in. + * @param email - The email of the user the link is being sent to. + * @param actionCodeSettings - The optional action code setings which defines whether + * the link is to be handled by a mobile app and the additional state information to be passed in the + * deep link, etc. Required when requestType === 'EMAIL_SIGNIN' + * @param newEmail - The email address the account is being updated to. + * Required only for VERIFY_AND_CHANGE_EMAIL requests. + * @returns A promise that resolves with the email action link. */ - private invokeRequestHandler(apiSettings: ApiSettings, requestData: object): Promise { - const path: string = this.path + apiSettings.getEndpoint(); - const httpMethod: HttpMethod = apiSettings.getHttpMethod(); - return Promise.resolve() - .then(() => { - // Validate request. - const requestValidator = apiSettings.getRequestValidator(); - requestValidator(requestData); - // Process request. - return this.signedApiRequestHandler.sendRequest( - this.host, this.port, path, httpMethod, requestData, this.headers, this.timeout); - }) - .then((response) => { - // Check for backend errors in the response. - const errorCode = FirebaseAuthRequestHandler.getErrorCode(response); - if (errorCode) { - throw FirebaseAuthError.fromServerError(errorCode, /* message */ undefined, response); - } - // Validate response. - const responseValidator = apiSettings.getResponseValidator(); - responseValidator(response); - // Return entire response. - return response; - }) - .catch((response) => { - const error = (typeof response === 'object' && 'statusCode' in response) ? - response.error : response; - if (error instanceof FirebaseError) { - throw error; - } - - const errorCode = FirebaseAuthRequestHandler.getErrorCode(error); - throw FirebaseAuthError.fromServerError(errorCode, /* message */ undefined, error); + public getEmailActionLink( + requestType: string, email: string, + actionCodeSettings?: ActionCodeSettings, newEmail?: string): Promise { + let request = { + requestType, + email, + returnOobLink: true, + ...(typeof newEmail !== 'undefined') && { newEmail }, + }; + // ActionCodeSettings required for email link sign-in to determine the url where the sign-in will + // be completed. + if (typeof actionCodeSettings === 'undefined' && requestType === 'EMAIL_SIGNIN') { + return Promise.reject( + new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + "`actionCodeSettings` is required when `requestType` === 'EMAIL_SIGNIN'", + ), + ); + } + if (typeof actionCodeSettings !== 'undefined' || requestType === 'EMAIL_SIGNIN') { + try { + const builder = new ActionCodeSettingsBuilder(actionCodeSettings!); + request = deepExtend(request, builder.buildRequest()); + } catch (e) { + return Promise.reject(e); + } + } + if (requestType === 'VERIFY_AND_CHANGE_EMAIL' && typeof newEmail === 'undefined') { + return Promise.reject( + new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + "`newEmail` is required when `requestType` === 'VERIFY_AND_CHANGE_EMAIL'", + ), + ); + } + return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_GET_OOB_CODE, request) + .then((response: any) => { + // Return the link. + return response.oobLink as string; }); } + + /** + * Looks up an OIDC provider configuration by provider ID. + * + * @param providerId - The provider identifier of the configuration to lookup. + * @returns A promise that resolves with the provider configuration information. + */ + public getOAuthIdpConfig(providerId: string): Promise { + if (!OIDCConfig.isProviderId(providerId)) { + return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID)); + } + return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), GET_OAUTH_IDP_CONFIG, {}, { providerId }); + } + + /** + * Lists the OIDC configurations (single batch only) with a size of maxResults and starting from + * the offset as specified by pageToken. + * + * @param maxResults - The page size, 100 if undefined. This is also the maximum + * allowed limit. + * @param pageToken - The next page token. If not specified, returns OIDC configurations + * without any offset. Configurations are returned in the order they were created from oldest to + * newest, relative to the page token offset. + * @returns A promise that resolves with the current batch of downloaded + * OIDC configurations and the next page token if available. For the last page, an empty list of provider + * configuration and no page token are returned. + */ + public listOAuthIdpConfigs( + maxResults: number = MAX_LIST_PROVIDER_CONFIGURATION_PAGE_SIZE, + pageToken?: string): Promise { + const request: {pageSize: number; pageToken?: string} = { + pageSize: maxResults, + }; + // Add next page token if provided. + if (typeof pageToken !== 'undefined') { + request.pageToken = pageToken; + } + return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), LIST_OAUTH_IDP_CONFIGS, request) + .then((response: any) => { + if (!response.oauthIdpConfigs) { + response.oauthIdpConfigs = []; + delete response.nextPageToken; + } + return response as {oauthIdpConfigs: object[]; nextPageToken?: string}; + }); + } + + /** + * Deletes an OIDC configuration identified by a providerId. + * + * @param providerId - The identifier of the OIDC configuration to delete. + * @returns A promise that resolves when the OIDC provider is deleted. + */ + public deleteOAuthIdpConfig(providerId: string): Promise { + if (!OIDCConfig.isProviderId(providerId)) { + return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID)); + } + return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), DELETE_OAUTH_IDP_CONFIG, {}, { providerId }) + .then(() => { + // Return nothing. + }); + } + + /** + * Creates a new OIDC provider configuration with the properties provided. + * + * @param options - The properties to set on the new OIDC provider configuration to be created. + * @returns A promise that resolves with the newly created OIDC + * configuration. + */ + public createOAuthIdpConfig(options: OIDCAuthProviderConfig): Promise { + // Construct backend request. + let request; + try { + request = OIDCConfig.buildServerRequest(options) || {}; + } catch (e) { + return Promise.reject(e); + } + const providerId = options.providerId; + return this.invokeRequestHandler( + this.getProjectConfigUrlBuilder(), CREATE_OAUTH_IDP_CONFIG, request, { providerId }) + .then((response: any) => { + if (!OIDCConfig.getProviderIdFromResourceName(response.name)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Unable to create new OIDC provider configuration'); + } + return response as OIDCConfigServerResponse; + }); + } + + /** + * Updates an existing OIDC provider configuration with the properties provided. + * + * @param providerId - The provider identifier of the OIDC configuration to update. + * @param options - The properties to update on the existing configuration. + * @returns A promise that resolves with the modified provider + * configuration. + */ + public updateOAuthIdpConfig( + providerId: string, options: OIDCUpdateAuthProviderRequest): Promise { + if (!OIDCConfig.isProviderId(providerId)) { + return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID)); + } + // Construct backend request. + let request: OIDCConfigServerRequest; + try { + request = OIDCConfig.buildServerRequest(options, true) || {}; + } catch (e) { + return Promise.reject(e); + } + const updateMask = utils.generateUpdateMask(request); + return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), UPDATE_OAUTH_IDP_CONFIG, request, + { providerId, updateMask: updateMask.join(',') }) + .then((response: any) => { + if (!OIDCConfig.getProviderIdFromResourceName(response.name)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Unable to update OIDC provider configuration'); + } + return response as OIDCConfigServerResponse; + }); + } + + /** + * Looks up an SAML provider configuration by provider ID. + * + * @param providerId - The provider identifier of the configuration to lookup. + * @returns A promise that resolves with the provider configuration information. + */ + public getInboundSamlConfig(providerId: string): Promise { + if (!SAMLConfig.isProviderId(providerId)) { + return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID)); + } + return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), GET_INBOUND_SAML_CONFIG, {}, { providerId }); + } + + /** + * Lists the SAML configurations (single batch only) with a size of maxResults and starting from + * the offset as specified by pageToken. + * + * @param maxResults - The page size, 100 if undefined. This is also the maximum + * allowed limit. + * @param pageToken - The next page token. If not specified, returns SAML configurations starting + * without any offset. Configurations are returned in the order they were created from oldest to + * newest, relative to the page token offset. + * @returns A promise that resolves with the current batch of downloaded + * SAML configurations and the next page token if available. For the last page, an empty list of provider + * configuration and no page token are returned. + */ + public listInboundSamlConfigs( + maxResults: number = MAX_LIST_PROVIDER_CONFIGURATION_PAGE_SIZE, + pageToken?: string): Promise { + const request: {pageSize: number; pageToken?: string} = { + pageSize: maxResults, + }; + // Add next page token if provided. + if (typeof pageToken !== 'undefined') { + request.pageToken = pageToken; + } + return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), LIST_INBOUND_SAML_CONFIGS, request) + .then((response: any) => { + if (!response.inboundSamlConfigs) { + response.inboundSamlConfigs = []; + delete response.nextPageToken; + } + return response as {inboundSamlConfigs: object[]; nextPageToken?: string}; + }); + } + + /** + * Deletes a SAML configuration identified by a providerId. + * + * @param providerId - The identifier of the SAML configuration to delete. + * @returns A promise that resolves when the SAML provider is deleted. + */ + public deleteInboundSamlConfig(providerId: string): Promise { + if (!SAMLConfig.isProviderId(providerId)) { + return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID)); + } + return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), DELETE_INBOUND_SAML_CONFIG, {}, { providerId }) + .then(() => { + // Return nothing. + }); + } + + /** + * Creates a new SAML provider configuration with the properties provided. + * + * @param options - The properties to set on the new SAML provider configuration to be created. + * @returns A promise that resolves with the newly created SAML + * configuration. + */ + public createInboundSamlConfig(options: SAMLAuthProviderConfig): Promise { + // Construct backend request. + let request; + try { + request = SAMLConfig.buildServerRequest(options) || {}; + } catch (e) { + return Promise.reject(e); + } + const providerId = options.providerId; + return this.invokeRequestHandler( + this.getProjectConfigUrlBuilder(), CREATE_INBOUND_SAML_CONFIG, request, { providerId }) + .then((response: any) => { + if (!SAMLConfig.getProviderIdFromResourceName(response.name)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Unable to create new SAML provider configuration'); + } + return response as SAMLConfigServerResponse; + }); + } + + /** + * Updates an existing SAML provider configuration with the properties provided. + * + * @param providerId - The provider identifier of the SAML configuration to update. + * @param options - The properties to update on the existing configuration. + * @returns A promise that resolves with the modified provider + * configuration. + */ + public updateInboundSamlConfig( + providerId: string, options: SAMLUpdateAuthProviderRequest): Promise { + if (!SAMLConfig.isProviderId(providerId)) { + return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID)); + } + // Construct backend request. + let request: SAMLConfigServerRequest; + try { + request = SAMLConfig.buildServerRequest(options, true) || {}; + } catch (e) { + return Promise.reject(e); + } + const updateMask = utils.generateUpdateMask(request); + return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), UPDATE_INBOUND_SAML_CONFIG, request, + { providerId, updateMask: updateMask.join(',') }) + .then((response: any) => { + if (!SAMLConfig.getProviderIdFromResourceName(response.name)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Unable to update SAML provider configuration'); + } + return response as SAMLConfigServerResponse; + }); + } + + /** + * Invokes the request handler based on the API settings object passed. + * + * @param urlBuilder - The URL builder for Auth endpoints. + * @param apiSettings - The API endpoint settings to apply to request and response. + * @param requestData - The request data. + * @param additionalResourceParams - Additional resource related params if needed. + * @returns A promise that resolves with the response. + */ + protected invokeRequestHandler( + urlBuilder: AuthResourceUrlBuilder, apiSettings: ApiSettings, + requestData: object | undefined, additionalResourceParams?: object): Promise { + return urlBuilder.getUrl(apiSettings.getEndpoint(), additionalResourceParams) + .then((url) => { + // Validate request. + if (requestData) { + const requestValidator = apiSettings.getRequestValidator(); + requestValidator(requestData); + } + // Process request. + const req: HttpRequestConfig = { + method: apiSettings.getHttpMethod(), + url, + headers: FIREBASE_AUTH_HEADER, + data: requestData, + timeout: FIREBASE_AUTH_TIMEOUT, + }; + return this.httpClient.send(req); + }) + .then((response) => { + // Validate response. + const responseValidator = apiSettings.getResponseValidator(); + responseValidator(response.data); + // Return entire response. + return response.data; + }) + .catch((err) => { + if (err instanceof HttpError) { + const error = err.response.data; + const errorCode = AbstractAuthRequestHandler.getErrorCode(error); + if (!errorCode) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'Error returned from server: ' + error + '. Additionally, an ' + + 'internal error occurred while attempting to extract the ' + + 'errorcode from the error.', + ); + } + throw FirebaseAuthError.fromServerError(errorCode, /* message */ undefined, error); + } + throw err; + }); + } + + /** + * @returns A new Auth user management resource URL builder instance. + */ + protected abstract newAuthUrlBuilder(): AuthResourceUrlBuilder; + + /** + * @returns A new project config resource URL builder instance. + */ + protected abstract newProjectConfigUrlBuilder(): AuthResourceUrlBuilder; + + /** + * @returns The current Auth user management resource URL builder. + */ + private getAuthUrlBuilder(): AuthResourceUrlBuilder { + if (!this.authUrlBuilder) { + this.authUrlBuilder = this.newAuthUrlBuilder(); + } + return this.authUrlBuilder; + } + + /** + * @returns The current project config resource URL builder. + */ + private getProjectConfigUrlBuilder(): AuthResourceUrlBuilder { + if (!this.projectConfigUrlBuilder) { + this.projectConfigUrlBuilder = this.newProjectConfigUrlBuilder(); + } + return this.projectConfigUrlBuilder; + } +} + +/** Instantiates the getConfig endpoint settings. */ +const GET_PROJECT_CONFIG = new ApiSettings('/config', 'GET') + .setResponseValidator((response: any) => { + // Response should always contain at least the config name. + if (!validator.isNonEmptyString(response.name)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Unable to get project config', + ); + } + }); + +/** Instantiates the updateConfig endpoint settings. */ +const UPDATE_PROJECT_CONFIG = new ApiSettings('/config?updateMask={updateMask}', 'PATCH') + .setResponseValidator((response: any) => { + // Response should always contain at least the config name. + if (!validator.isNonEmptyString(response.name)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Unable to update project config', + ); + } + }); + +/** Instantiates the getTenant endpoint settings. */ +const GET_TENANT = new ApiSettings('/tenants/{tenantId}', 'GET') +// Set response validator. + .setResponseValidator((response: any) => { + // Response should always contain at least the tenant name. + if (!validator.isNonEmptyString(response.name)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Unable to get tenant', + ); + } + }); + +/** Instantiates the deleteTenant endpoint settings. */ +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. + .setResponseValidator((response: any) => { + // Response should always contain at least the tenant name. + if (!validator.isNonEmptyString(response.name) || + !Tenant.getTenantIdFromResourceName(response.name)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Unable to update tenant', + ); + } + }); + +/** Instantiates the listTenants endpoint settings. */ +const LIST_TENANTS = new ApiSettings('/tenants', 'GET') + // Set request validator. + .setRequestValidator((request: any) => { + // Validate next page token. + if (typeof request.pageToken !== 'undefined' && + !validator.isNonEmptyString(request.pageToken)) { + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PAGE_TOKEN); + } + // Validate max results. + if (!validator.isNumber(request.pageSize) || + request.pageSize <= 0 || + 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 ' + + `the allowed ${MAX_LIST_TENANT_PAGE_SIZE}.`, + ); + } + }); + +/** Instantiates the createTenant endpoint settings. */ +const CREATE_TENANT = new ApiSettings('/tenants', 'POST') +// Set response validator. + .setResponseValidator((response: any) => { + // Response should always contain at least the tenant name. + if (!validator.isNonEmptyString(response.name) || + !Tenant.getTenantIdFromResourceName(response.name)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Unable to create new tenant', + ); + } + }); + + +/** + * Utility for sending requests to Auth server that are Auth instance related. This includes user, tenant, + * and project config management related APIs. This extends the BaseFirebaseAuthRequestHandler class and defines + * additional tenant management related APIs. + */ +export class AuthRequestHandler extends AbstractAuthRequestHandler { + + protected readonly authResourceUrlBuilder: AuthResourceUrlBuilder; + + /** + * The FirebaseAuthRequestHandler constructor used to initialize an instance using a FirebaseApp. + * + * @param app - The app used to fetch access tokens to sign API requests. + * @constructor + */ + constructor(app: App) { + super(app); + this.authResourceUrlBuilder = new AuthResourceUrlBuilder(app, 'v2'); + } + + /** + * @returns A new Auth user management resource URL builder instance. + */ + protected newAuthUrlBuilder(): AuthResourceUrlBuilder { + return new AuthResourceUrlBuilder(this.app, 'v1'); + } + + /** + * @returns A new project config resource URL builder instance. + */ + protected newProjectConfigUrlBuilder(): AuthResourceUrlBuilder { + return new AuthResourceUrlBuilder(this.app, 'v2'); + } + + /** + * Get the current project's config + * @returns A promise that resolves with the project config information. + */ + public getProjectConfig(): Promise { + return this.invokeRequestHandler(this.authResourceUrlBuilder, GET_PROJECT_CONFIG, {}, {}) + .then((response: any) => { + return response as ProjectConfigServerResponse; + }); + } + + /** + * Update the current project's config. + * @returns A promise that resolves with the project config information. + */ + public updateProjectConfig(options: UpdateProjectConfigRequest): Promise { + try { + const request = ProjectConfig.buildServerRequest(options); + const updateMask = utils.generateUpdateMask(request); + return this.invokeRequestHandler( + this.authResourceUrlBuilder, UPDATE_PROJECT_CONFIG, request, { updateMask: updateMask.join(',') }) + .then((response: any) => { + return response as ProjectConfigServerResponse; + }); + } catch (e) { + return Promise.reject(e); + } + } + + /** + * Looks up a tenant by tenant ID. + * + * @param tenantId - The tenant identifier of the tenant to lookup. + * @returns A promise that resolves with the tenant information. + */ + public getTenant(tenantId: string): Promise { + if (!validator.isNonEmptyString(tenantId)) { + return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_TENANT_ID)); + } + return this.invokeRequestHandler(this.authResourceUrlBuilder, GET_TENANT, {}, { tenantId }) + .then((response: any) => { + return response as TenantServerResponse; + }); + } + + /** + * Exports the tenants (single batch only) with a size of maxResults and starting from + * the offset as specified by pageToken. + * + * @param maxResults - The page size, 1000 if undefined. This is also the maximum + * allowed limit. + * @param pageToken - The next page token. If not specified, returns tenants starting + * without any offset. Tenants are returned in the order they were created from oldest to + * newest, relative to the page token offset. + * @returns A promise that resolves with the current batch of downloaded + * tenants and the next page token if available. For the last page, an empty list of tenants + * and no page token are returned. + */ + public listTenants( + maxResults: number = MAX_LIST_TENANT_PAGE_SIZE, + pageToken?: string): Promise<{tenants: TenantServerResponse[]; nextPageToken?: string}> { + const request = { + pageSize: maxResults, + pageToken, + }; + // Remove next page token if not provided. + if (typeof request.pageToken === 'undefined') { + delete request.pageToken; + } + return this.invokeRequestHandler(this.authResourceUrlBuilder, LIST_TENANTS, request) + .then((response: any) => { + if (!response.tenants) { + response.tenants = []; + delete response.nextPageToken; + } + return response as {tenants: TenantServerResponse[]; nextPageToken?: string}; + }); + } + + /** + * Deletes a tenant identified by a tenantId. + * + * @param tenantId - The identifier of the tenant to delete. + * @returns A promise that resolves when the tenant is deleted. + */ + public deleteTenant(tenantId: string): Promise { + if (!validator.isNonEmptyString(tenantId)) { + return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_TENANT_ID)); + } + return this.invokeRequestHandler(this.authResourceUrlBuilder, DELETE_TENANT, undefined, { tenantId }) + .then(() => { + // Return nothing. + }); + } + + /** + * Creates a new tenant with the properties provided. + * + * @param tenantOptions - The properties to set on the new tenant to be created. + * @returns A promise that resolves with the newly created tenant object. + */ + public createTenant(tenantOptions: CreateTenantRequest): Promise { + try { + // Construct backend request. + const request = Tenant.buildServerRequest(tenantOptions, true); + return this.invokeRequestHandler(this.authResourceUrlBuilder, CREATE_TENANT, request) + .then((response: any) => { + return response as TenantServerResponse; + }); + } catch (e) { + return Promise.reject(e); + } + } + + /** + * Updates an existing tenant with the properties provided. + * + * @param tenantId - The tenant identifier of the tenant to update. + * @param tenantOptions - The properties to update on the existing tenant. + * @returns A promise that resolves with the modified tenant object. + */ + public updateTenant(tenantId: string, tenantOptions: UpdateTenantRequest): Promise { + if (!validator.isNonEmptyString(tenantId)) { + return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_TENANT_ID)); + } + try { + // Construct backend request. + const request = Tenant.buildServerRequest(tenantOptions, false); + // Do not traverse deep into testPhoneNumbers. The entire content should be replaced + // and not just specific phone numbers. + const updateMask = utils.generateUpdateMask(request, ['testPhoneNumbers']); + return this.invokeRequestHandler(this.authResourceUrlBuilder, UPDATE_TENANT, request, + { tenantId, updateMask: updateMask.join(',') }) + .then((response: any) => { + return response as TenantServerResponse; + }); + } catch (e) { + return Promise.reject(e); + } + } +} + +/** + * Utility for sending requests to Auth server that are tenant Auth instance related. This includes user + * management related APIs for specified tenants. + * This extends the BaseFirebaseAuthRequestHandler class. + */ +export class TenantAwareAuthRequestHandler extends AbstractAuthRequestHandler { + /** + * The FirebaseTenantRequestHandler constructor used to initialize an instance using a + * FirebaseApp and a tenant ID. + * + * @param app - The app used to fetch access tokens to sign API requests. + * @param tenantId - The request handler's tenant ID. + * @constructor + */ + constructor(app: App, private readonly tenantId: string) { + super(app); + } + + /** + * @returns A new Auth user management resource URL builder instance. + */ + protected newAuthUrlBuilder(): AuthResourceUrlBuilder { + return new TenantAwareAuthResourceUrlBuilder(this.app, 'v1', this.tenantId); + } + + /** + * @returns A new project config resource URL builder instance. + */ + protected newProjectConfigUrlBuilder(): AuthResourceUrlBuilder { + return new TenantAwareAuthResourceUrlBuilder(this.app, 'v2', this.tenantId); + } + + /** + * Imports the list of users provided to Firebase Auth. This is useful when + * migrating from an external authentication system without having to use the Firebase CLI SDK. + * At most, 1000 users are allowed to be imported one at a time. + * When importing a list of password users, UserImportOptions are required to be specified. + * + * Overrides the superclass methods by adding an additional check to match tenant IDs of + * imported user records if present. + * + * @param users - The list of user records to import to Firebase Auth. + * @param options - The user import options, required when the users provided + * include password credentials. + * @returns A promise that resolves when the operation completes + * with the result of the import. This includes the number of successful imports, the number + * of failed uploads and their corresponding errors. + */ + public uploadAccount( + users: UserImportRecord[], options?: UserImportOptions): Promise { + // Add additional check to match tenant ID of imported user records. + users.forEach((user: UserImportRecord, index: number) => { + if (validator.isNonEmptyString(user.tenantId) && + user.tenantId !== this.tenantId) { + throw new FirebaseAuthError( + AuthClientErrorCode.MISMATCHING_TENANT_ID, + `UserRecord of index "${index}" has mismatching tenant ID "${user.tenantId}"`); + } + }); + 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. + */ +export function useEmulator(): boolean { + return !!emulatorHost(); } diff --git a/src/auth/auth-config.ts b/src/auth/auth-config.ts new file mode 100644 index 0000000000..28ee595c46 --- /dev/null +++ b/src/auth/auth-config.ts @@ -0,0 +1,2328 @@ +/*! + * Copyright 2018 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 { deepCopy } from '../utils/deep-copy'; +import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; + +/** + * Interface representing base properties of a user-enrolled second factor for a + * `CreateRequest`. + */ +export interface BaseCreateMultiFactorInfoRequest { + + /** + * The optional display name for an enrolled second factor. + */ + displayName?: string; + + /** + * The type identifier of the second factor. For SMS second factors, this is `phone`. + */ + factorId: string; +} + +/** + * Interface representing a phone specific user-enrolled second factor for a + * `CreateRequest`. + */ +export interface CreatePhoneMultiFactorInfoRequest extends BaseCreateMultiFactorInfoRequest { + + /** + * The phone number associated with a phone second factor. + */ + phoneNumber: string; +} + +/** + * Type representing the properties of a user-enrolled second factor + * for a `CreateRequest`. + */ +export type CreateMultiFactorInfoRequest = | CreatePhoneMultiFactorInfoRequest; + +/** + * Interface representing common properties of a user-enrolled second factor + * for an `UpdateRequest`. + */ +export interface BaseUpdateMultiFactorInfoRequest { + + /** + * The ID of the enrolled second factor. This ID is unique to the user. When not provided, + * a new one is provisioned by the Auth server. + */ + uid?: string; + + /** + * The optional display name for an enrolled second factor. + */ + displayName?: string; + + /** + * The optional date the second factor was enrolled, formatted as a UTC string. + */ + enrollmentTime?: string; + + /** + * The type identifier of the second factor. For SMS second factors, this is `phone`. + */ + factorId: string; +} + +/** + * Interface representing a phone specific user-enrolled second factor + * for an `UpdateRequest`. + */ +export interface UpdatePhoneMultiFactorInfoRequest extends BaseUpdateMultiFactorInfoRequest { + + /** + * The phone number associated with a phone second factor. + */ + phoneNumber: string; +} + +/** + * Type representing the properties of a user-enrolled second factor + * for an `UpdateRequest`. + */ +export type UpdateMultiFactorInfoRequest = | UpdatePhoneMultiFactorInfoRequest; + +/** + * The multi-factor related user settings for create operations. + */ +export interface MultiFactorCreateSettings { + + /** + * The created user's list of enrolled second factors. + */ + enrolledFactors: CreateMultiFactorInfoRequest[]; +} + +/** + * The multi-factor related user settings for update operations. + */ +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: UpdateMultiFactorInfoRequest[] | null; +} + +/** + * Interface representing the properties to update on the provided user. + */ +export interface UpdateRequest { + + /** + * Whether or not the user is disabled: `true` for disabled; + * `false` for enabled. + */ + disabled?: boolean; + + /** + * The user's display name. + */ + displayName?: string | null; + + /** + * The user's primary email. + */ + email?: string; + + /** + * Whether or not the user's primary email is verified. + */ + emailVerified?: boolean; + + /** + * The user's unhashed password. + */ + password?: string; + + /** + * The user's primary phone number. + */ + phoneNumber?: string | null; + + /** + * The user's photo URL. + */ + photoURL?: string | null; + + /** + * 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[]; +} + +/** + * Represents a user identity provider that can be associated with a Firebase user. + */ +export 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 the properties to set on a new user record to be + * created. + */ +export interface CreateRequest extends UpdateRequest { + + /** + * The user's `uid`. + */ + uid?: string; + + /** + * The user's multi-factor related properties. + */ + multiFactor?: MultiFactorCreateSettings; +} + +/** + * The response interface for listing provider configs. This is only available + * when listing all identity providers' configurations via + * {@link BaseAuth.listProviderConfigs}. + */ +export interface ListProviderConfigResults { + + /** + * The list of providers for the specified type in the current page. + */ + providerConfigs: AuthProviderConfig[]; + + /** + * The next page token, if available. + */ + pageToken?: string; +} + +/** + * The filter interface used for listing provider configurations. This is used + * when specifying how to list configured identity providers via + * {@link BaseAuth.listProviderConfigs}. + */ +export interface AuthProviderConfigFilter { + + /** + * The Auth provider configuration filter. This can be either `saml` or `oidc`. + * The former is used to look up SAML providers only, while the latter is used + * for OIDC providers. + */ + type: 'saml' | 'oidc'; + + /** + * The maximum number of results to return per page. The default and maximum is + * 100. + */ + maxResults?: number; + + /** + * The next page token. When not specified, the lookup starts from the beginning + * of the list. + */ + pageToken?: string; +} + +/** + * The request interface for updating a SAML Auth provider. This is used + * when updating a SAML provider's configuration via + * {@link BaseAuth.updateProviderConfig}. + */ +export interface SAMLUpdateAuthProviderRequest { + + /** + * The SAML provider's updated display name. If not provided, the existing + * configuration's value is not modified. + */ + displayName?: string; + + /** + * Whether the SAML provider is enabled or not. If not provided, the existing + * configuration's setting is not modified. + */ + enabled?: boolean; + + /** + * The SAML provider's updated IdP entity ID. If not provided, the existing + * configuration's value is not modified. + */ + idpEntityId?: string; + + /** + * The SAML provider's updated SSO URL. If not provided, the existing + * configuration's value is not modified. + */ + ssoURL?: string; + + /** + * The SAML provider's updated list of X.509 certificated. If not provided, the + * existing configuration list is not modified. + */ + x509Certificates?: string[]; + + /** + * The SAML provider's updated RP entity ID. If not provided, the existing + * configuration's value is not modified. + */ + rpEntityId?: string; + + /** + * The SAML provider's callback URL. If not provided, the existing + * configuration's value is not modified. + */ + callbackURL?: string; +} + +/** + * The request interface for updating an OIDC Auth provider. This is used + * when updating an OIDC provider's configuration via + * {@link BaseAuth.updateProviderConfig}. + */ +export interface OIDCUpdateAuthProviderRequest { + + /** + * The OIDC provider's updated display name. If not provided, the existing + * configuration's value is not modified. + */ + displayName?: string; + + /** + * Whether the OIDC provider is enabled or not. If not provided, the existing + * configuration's setting is not modified. + */ + enabled?: boolean; + + /** + * The OIDC provider's updated client ID. If not provided, the existing + * configuration's value is not modified. + */ + clientId?: string; + + /** + * The OIDC provider's updated issuer. If not provided, the existing + * 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; +} + +export type UpdateAuthProviderRequest = + SAMLUpdateAuthProviderRequest | OIDCUpdateAuthProviderRequest; + +/** A maximum of 10 test phone number / code pairs can be configured. */ +export const MAXIMUM_TEST_PHONE_NUMBERS = 10; + +/** The server side SAML configuration request interface. */ +export interface SAMLConfigServerRequest { + idpConfig?: { + idpEntityId?: string; + ssoUrl?: string; + idpCertificates?: Array<{ + x509Certificate: string; + }>; + signRequest?: boolean; + }; + spConfig?: { + spEntityId?: string; + callbackUri?: string; + }; + displayName?: string; + enabled?: boolean; + [key: string]: any; +} + +/** The server side SAML configuration response interface. */ +export interface SAMLConfigServerResponse { + // Used when getting config. + // projects/${projectId}/inboundSamlConfigs/${providerId} + name?: string; + idpConfig?: { + idpEntityId?: string; + ssoUrl?: string; + idpCertificates?: Array<{ + x509Certificate: string; + }>; + signRequest?: boolean; + }; + spConfig?: { + spEntityId?: string; + callbackUri?: string; + }; + displayName?: string; + enabled?: boolean; +} + +/** The server side OIDC configuration request interface. */ +export interface OIDCConfigServerRequest { + clientId?: string; + issuer?: string; + displayName?: string; + enabled?: boolean; + clientSecret?: string; + responseType?: OAuthResponseType; + [key: string]: any; +} + +/** The server side OIDC configuration response interface. */ +export interface OIDCConfigServerResponse { + // Used when getting config. + // projects/${projectId}/oauthIdpConfigs/${providerId} + name?: string; + clientId?: string; + issuer?: string; + displayName?: string; + enabled?: boolean; + clientSecret?: string; + responseType?: OAuthResponseType; +} + +/** The server side email configuration request interface. */ +export interface EmailSignInConfigServerRequest { + allowPasswordSignup?: boolean; + enableEmailLinkSignin?: boolean; +} + +/** Identifies the server side second factor type. */ +type AuthFactorServerType = 'PHONE_SMS'; + +/** Client Auth factor type to server auth factor type mapping. */ +const AUTH_FACTOR_CLIENT_TO_SERVER_TYPE: {[key: string]: AuthFactorServerType} = { + phone: 'PHONE_SMS', +}; + +/** Server Auth factor type to client auth factor type mapping. */ +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; + }, {}); + +/** Server side multi-factor configuration. */ +export interface MultiFactorAuthServerConfig { + state?: MultiFactorConfigState; + enabledProviders?: AuthFactorServerType[]; + providerConfigs?: MultiFactorProviderConfig[]; +} + +/** + * Identifies a second factor type. + */ +export type AuthFactorType = 'phone'; + +/** + * Identifies a multi-factor configuration state. + */ +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. + */ +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[]; + + /** + * A list of multi-factor provider configurations. + * MFA providers (except phone) indicate whether they're enabled through this field. */ + providerConfigs?: MultiFactorProviderConfig[]; +} + +/** + * Interface representing a multi-factor auth provider configuration. + * This interface is used for second factor auth providers other than SMS. + * Currently, only TOTP is supported. + */export interface MultiFactorProviderConfig { + /** + * Indicates whether this multi-factor provider is enabled or disabled. */ + state: MultiFactorConfigState; + /** + * TOTP multi-factor provider config. */ + totpProviderConfig?: TotpMultiFactorProviderConfig; +} + +/** + * Interface representing configuration settings for TOTP second factor auth. + */ +export interface TotpMultiFactorProviderConfig { + /** + * The allowed number of adjacent intervals that will be used for verification + * to compensate for clock skew. */ + adjacentIntervals?: number; +} + +/** + * Defines the multi-factor config class used to convert client side MultiFactorConfig + * to a format that is understood by the Auth server. + * + * @internal + */ +export class MultiFactorAuthConfig implements MultiFactorConfig { + + /** + * The multi-factor config state. + */ + public readonly state: MultiFactorConfigState; + /** + * The list of identifiers for enabled second factors. + * Currently only ‘phone’ is supported. + */ + public readonly factorIds: AuthFactorType[]; + /** + * A list of multi-factor provider specific config. + * New MFA providers (except phone) will indicate enablement/disablement through this field. + */ + public readonly providerConfigs: MultiFactorProviderConfig[]; + + /** + * 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. + * @returns The resulting server request. + * @internal + */ + 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 = []; + } + } + if (Object.prototype.hasOwnProperty.call(options, 'providerConfigs')) { + request.providerConfigs = options.providerConfigs; + } + return request; + } + + /** + * Validates the MultiFactorConfig options object. Throws an error on failure. + * + * @param options - The options object to validate. + */ + public static validate(options: MultiFactorConfig): void { + const validKeys = { + state: true, + factorIds: true, + providerConfigs: 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".`, + ); + } + }); + } + + if (typeof options.providerConfigs !== 'undefined') { + if (!validator.isArray(options.providerConfigs)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"MultiFactorConfig.providerConfigs" must be an array of valid "MultiFactorProviderConfig."', + ); + } + //Validate content of array. + options.providerConfigs.forEach((multiFactorProviderConfig) => { + if (typeof multiFactorProviderConfig === 'undefined' || !validator.isObject(multiFactorProviderConfig)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + `"${multiFactorProviderConfig}" is not a valid "MultiFactorProviderConfig" type.` + ) + } + const validProviderConfigKeys = { + state: true, + totpProviderConfig: true, + }; + for (const key in multiFactorProviderConfig) { + if (!(key in validProviderConfigKeys)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + `"${key}" is not a valid ProviderConfig parameter.`, + ); + } + } + if (typeof multiFactorProviderConfig.state === 'undefined' || + (multiFactorProviderConfig.state !== 'ENABLED' && + multiFactorProviderConfig.state !== 'DISABLED')) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"MultiFactorConfig.providerConfigs.state" must be either "ENABLED" or "DISABLED".', + ) + } + // Since TOTP is the only provider config available right now, not defining it will lead into an error + if (typeof multiFactorProviderConfig.totpProviderConfig === 'undefined') { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"MultiFactorConfig.providerConfigs.totpProviderConfig" must be defined.' + ) + } + const validTotpProviderConfigKeys = { + adjacentIntervals: true, + }; + for (const key in multiFactorProviderConfig.totpProviderConfig) { + if (!(key in validTotpProviderConfigKeys)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + `"${key}" is not a valid TotpProviderConfig parameter.`, + ); + } + } + const adjIntervals = multiFactorProviderConfig.totpProviderConfig.adjacentIntervals + if (typeof adjIntervals !== 'undefined' && + (!Number.isInteger(adjIntervals) || adjIntervals < 0 || adjIntervals > 10)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"MultiFactorConfig.providerConfigs.totpProviderConfig.adjacentIntervals" must' + + ' be a valid number between 0 and 10 (both inclusive).' + ) + } + }); + } + } + + /** + * The MultiFactorAuthConfig constructor. + * + * @param response - The server side response used to initialize the + * MultiFactorAuthConfig object. + * @constructor + * @internal + */ + 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]); + } + }) + this.providerConfigs = []; + (response.providerConfigs || []).forEach((providerConfig) => { + if (typeof providerConfig !== 'undefined') { + if (typeof providerConfig.state === 'undefined' || + typeof providerConfig.totpProviderConfig === 'undefined' || + (typeof providerConfig.totpProviderConfig.adjacentIntervals !== 'undefined' && + typeof providerConfig.totpProviderConfig.adjacentIntervals !== 'number')) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Invalid multi-factor configuration response'); + } + this.providerConfigs.push(providerConfig); + } + }) + } + + /** Converts MultiFactorConfig to JSON object + * @returns The plain object representation of the multi-factor config instance. */ + public toJSON(): object { + return { + state: this.state, + factorIds: this.factorIds, + providerConfigs: this.providerConfigs, + }; + } +} + + +/** + * 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.` + ); + } + } +} + +/** + * The email sign in provider 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 +} + + +/** + * Defines the email sign-in config class used to convert client side EmailSignInConfig + * to a format that is understood by the Auth server. + * + * @internal + */ +export class EmailSignInConfig implements EmailSignInProviderConfig { + public readonly enabled: boolean; + public readonly passwordRequired?: boolean; + + /** + * Static method to convert a client side request to a EmailSignInConfigServerRequest. + * Throws an error if validation fails. + * + * @param options - The options object to convert to a server request. + * @returns The resulting server request. + * @internal + */ + public static buildServerRequest(options: EmailSignInProviderConfig): EmailSignInConfigServerRequest { + const request: EmailSignInConfigServerRequest = {}; + EmailSignInConfig.validate(options); + if (Object.prototype.hasOwnProperty.call(options, 'enabled')) { + request.allowPasswordSignup = options.enabled; + } + if (Object.prototype.hasOwnProperty.call(options, 'passwordRequired')) { + request.enableEmailLinkSignin = !options.passwordRequired; + } + return request; + } + + /** + * Validates the EmailSignInConfig options object. Throws an error on failure. + * + * @param options - The options object to validate. + */ + private static validate(options: EmailSignInProviderConfig): void { + // TODO: Validate the request. + const validKeys = { + enabled: true, + passwordRequired: true, + }; + if (!validator.isNonNullObject(options)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"EmailSignInConfig" 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_ARGUMENT, + `"${key}" is not a valid EmailSignInConfig parameter.`, + ); + } + } + // Validate content. + if (typeof options.enabled !== 'undefined' && + !validator.isBoolean(options.enabled)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"EmailSignInConfig.enabled" must be a boolean.', + ); + } + if (typeof options.passwordRequired !== 'undefined' && + !validator.isBoolean(options.passwordRequired)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"EmailSignInConfig.passwordRequired" must be a boolean.', + ); + } + } + + /** + * The EmailSignInConfig constructor. + * + * @param response - The server side response used to initialize the + * EmailSignInConfig object. + * @constructor + */ + constructor(response: {[key: string]: any}) { + if (typeof response.allowPasswordSignup === 'undefined') { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Invalid email sign-in configuration response'); + } + this.enabled = response.allowPasswordSignup; + this.passwordRequired = !response.enableEmailLinkSignin; + } + + /** @returns The plain object representation of the email sign-in config. */ + public toJSON(): object { + return { + enabled: this.enabled, + passwordRequired: this.passwordRequired, + }; + } +} + +/** + * The base Auth provider configuration interface. + */ +export interface BaseAuthProviderConfig { + + /** + * The provider ID defined by the developer. + * For a SAML provider, this is always prefixed by `saml.`. + * For an OIDC provider, this is always prefixed by `oidc.`. + */ + providerId: string; + + /** + * The user-friendly display name to the current configuration. This name is + * also used as the provider label in the Cloud Console. + */ + displayName?: string; + + /** + * Whether the provider configuration is enabled or disabled. A user + * cannot sign in using a disabled provider. + */ + enabled: boolean; +} + +/** + * 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 BaseAuth.createProviderConfig}. + */ +export interface SAMLAuthProviderConfig extends BaseAuthProviderConfig { + + /** + * The SAML IdP entity identifier. + */ + idpEntityId: string; + + /** + * The SAML IdP SSO URL. This must be a valid URL. + */ + ssoURL: string; + + /** + * The list of SAML IdP X.509 certificates issued by CA for this provider. + * Multiple certificates are accepted to prevent outages during + * IdP key rotation (for example ADFS rotates every 10 days). When the Auth + * server receives a SAML response, it will match the SAML response with the + * certificate on record. Otherwise the response is rejected. + * Developers are expected to manage the certificate updates as keys are + * rotated. + */ + x509Certificates: string[]; + + /** + * The SAML relying party (service provider) entity ID. + * This is defined by the developer but needs to be provided to the SAML IdP. + */ + rpEntityId: string; + + /** + * This is fixed and must always be the same as the OAuth redirect URL + * provisioned by Firebase Auth, + * `https://project-id.firebaseapp.com/__/auth/handler` unless a custom + * `authDomain` is used. + * The callback URL should also be provided to the SAML IdP during + * configuration. + */ + callbackURL?: string; +} + +/** + * The interface representing OIDC provider's response object for OAuth + * authorization 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 { + /** + * 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 + * {@link BaseAuth.createProviderConfig}. + */ +export interface OIDCAuthProviderConfig extends BaseAuthProviderConfig { + + /** + * This is the required client ID used to confirm the audience of an OIDC + * provider's + * [ID token](https://openid.net/specs/openid-connect-core-1_0-final.html#IDToken). + */ + clientId: string; + + /** + * This is the required provider issuer used to match the provider issuer of + * the ID token and to determine the corresponding OIDC discovery document, eg. + * [`/.well-known/openid-configuration`](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig). + * This is needed for the following: + *
    + *
  • To verify the provided issuer.
  • + *
  • Determine the authentication/authorization endpoint during the OAuth + * `id_token` authentication flow.
  • + *
  • To retrieve the public signing keys via `jwks_uri` to verify the OIDC + * provider's ID token's signature.
  • + *
  • To determine the claims_supported to construct the user attributes to be + * returned in the additional user info response.
  • + *
+ * ID token validation will be performed as defined in the + * [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; +} + +/** + * The Auth provider configuration type. + * {@link BaseAuth.createProviderConfig}. + */ +export type AuthProviderConfig = SAMLAuthProviderConfig | OIDCAuthProviderConfig; + +/** + * Defines the SAMLConfig class used to convert a client side configuration to its + * server side representation. + * + * @internal + */ +export class SAMLConfig implements SAMLAuthProviderConfig { + public readonly enabled: boolean; + public readonly displayName?: string; + public readonly providerId: string; + public readonly idpEntityId: string; + public readonly ssoURL: string; + public readonly x509Certificates: string[]; + public readonly rpEntityId: string; + public readonly callbackURL?: string; + public readonly enableRequestSigning?: boolean; + + /** + * Converts a client side request to a SAMLConfigServerRequest which is the format + * accepted by the backend server. + * Throws an error if validation fails. If the request is not a SAMLConfig request, + * returns null. + * + * @param options - The options object to convert to a server request. + * @param ignoreMissingFields - Whether to ignore missing fields. + * @returns The resulting server request or null if not valid. + */ + public static buildServerRequest( + options: Partial, + ignoreMissingFields = false): SAMLConfigServerRequest | null { + const makeRequest = validator.isNonNullObject(options) && + (options.providerId || ignoreMissingFields); + if (!makeRequest) { + return null; + } + const request: SAMLConfigServerRequest = {}; + // Validate options. + SAMLConfig.validate(options, ignoreMissingFields); + request.enabled = options.enabled; + request.displayName = options.displayName; + // IdP config. + if (options.idpEntityId || options.ssoURL || options.x509Certificates) { + request.idpConfig = { + idpEntityId: options.idpEntityId, + ssoUrl: options.ssoURL, + signRequest: (options as any).enableRequestSigning, + idpCertificates: typeof options.x509Certificates === 'undefined' ? undefined : [], + }; + if (options.x509Certificates) { + for (const cert of (options.x509Certificates || [])) { + request.idpConfig!.idpCertificates!.push({ x509Certificate: cert }); + } + } + } + // RP config. + if (options.callbackURL || options.rpEntityId) { + request.spConfig = { + spEntityId: options.rpEntityId, + callbackUri: options.callbackURL, + }; + } + return request; + } + + /** + * Returns the provider ID corresponding to the resource name if available. + * + * @param resourceName - The server side resource name. + * @returns The provider ID corresponding to the resource, null otherwise. + */ + public static getProviderIdFromResourceName(resourceName: string): string | null { + // name is of form projects/project1/inboundSamlConfigs/providerId1 + const matchProviderRes = resourceName.match(/\/inboundSamlConfigs\/(saml\..*)$/); + if (!matchProviderRes || matchProviderRes.length < 2) { + return null; + } + return matchProviderRes[1]; + } + + /** + * @param providerId - The provider ID to check. + * @returns Whether the provider ID corresponds to a SAML provider. + */ + public static isProviderId(providerId: any): providerId is string { + return validator.isNonEmptyString(providerId) && providerId.indexOf('saml.') === 0; + } + + /** + * Validates the SAMLConfig options object. Throws an error on failure. + * + * @param options - The options object to validate. + * @param ignoreMissingFields - Whether to ignore missing fields. + */ + public static validate(options: Partial, ignoreMissingFields = false): void { + const validKeys = { + enabled: true, + displayName: true, + providerId: true, + idpEntityId: true, + ssoURL: true, + x509Certificates: true, + rpEntityId: true, + callbackURL: true, + enableRequestSigning: true, + }; + if (!validator.isNonNullObject(options)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"SAMLAuthProviderConfig" must be a valid 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 SAML config parameter.`, + ); + } + } + // Required fields. + if (validator.isNonEmptyString(options.providerId)) { + if (options.providerId.indexOf('saml.') !== 0) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_PROVIDER_ID, + '"SAMLAuthProviderConfig.providerId" must be a valid non-empty string prefixed with "saml.".', + ); + } + } else if (!ignoreMissingFields) { + // providerId is required and not provided correctly. + throw new FirebaseAuthError( + !options.providerId ? AuthClientErrorCode.MISSING_PROVIDER_ID : AuthClientErrorCode.INVALID_PROVIDER_ID, + '"SAMLAuthProviderConfig.providerId" must be a valid non-empty string prefixed with "saml.".', + ); + } + if (!(ignoreMissingFields && typeof options.idpEntityId === 'undefined') && + !validator.isNonEmptyString(options.idpEntityId)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"SAMLAuthProviderConfig.idpEntityId" must be a valid non-empty string.', + ); + } + if (!(ignoreMissingFields && typeof options.ssoURL === 'undefined') && + !validator.isURL(options.ssoURL)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"SAMLAuthProviderConfig.ssoURL" must be a valid URL string.', + ); + } + if (!(ignoreMissingFields && typeof options.rpEntityId === 'undefined') && + !validator.isNonEmptyString(options.rpEntityId)) { + throw new FirebaseAuthError( + !options.rpEntityId ? AuthClientErrorCode.MISSING_SAML_RELYING_PARTY_CONFIG : + AuthClientErrorCode.INVALID_CONFIG, + '"SAMLAuthProviderConfig.rpEntityId" must be a valid non-empty string.', + ); + } + if (!(ignoreMissingFields && typeof options.callbackURL === 'undefined') && + !validator.isURL(options.callbackURL)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"SAMLAuthProviderConfig.callbackURL" must be a valid URL string.', + ); + } + if (!(ignoreMissingFields && typeof options.x509Certificates === 'undefined') && + !validator.isArray(options.x509Certificates)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"SAMLAuthProviderConfig.x509Certificates" must be a valid array of X509 certificate strings.', + ); + } + (options.x509Certificates || []).forEach((cert: string) => { + if (!validator.isNonEmptyString(cert)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"SAMLAuthProviderConfig.x509Certificates" must be a valid array of X509 certificate strings.', + ); + } + }); + 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.', + ); + } + if (typeof options.enabled !== 'undefined' && + !validator.isBoolean(options.enabled)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"SAMLAuthProviderConfig.enabled" must be a boolean.', + ); + } + if (typeof options.displayName !== 'undefined' && + !validator.isString(options.displayName)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"SAMLAuthProviderConfig.displayName" must be a valid string.', + ); + } + } + + /** + * The SAMLConfig constructor. + * + * @param response - The server side response used to initialize the SAMLConfig object. + * @constructor + */ + constructor(response: SAMLConfigServerResponse) { + if (!response || + !response.idpConfig || + !response.idpConfig.idpEntityId || + !response.idpConfig.ssoUrl || + !response.spConfig || + !response.spConfig.spEntityId || + !response.name || + !(validator.isString(response.name) && + SAMLConfig.getProviderIdFromResourceName(response.name))) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Invalid SAML configuration response'); + } + + const providerId = SAMLConfig.getProviderIdFromResourceName(response.name); + if (!providerId) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Invalid SAML configuration response'); + } + this.providerId = providerId; + + // RP config. + this.rpEntityId = response.spConfig.spEntityId; + this.callbackURL = response.spConfig.callbackUri; + // IdP config. + this.idpEntityId = response.idpConfig.idpEntityId; + this.ssoURL = response.idpConfig.ssoUrl; + this.enableRequestSigning = !!response.idpConfig.signRequest; + const x509Certificates: string[] = []; + for (const cert of (response.idpConfig.idpCertificates || [])) { + if (cert.x509Certificate) { + x509Certificates.push(cert.x509Certificate); + } + } + this.x509Certificates = x509Certificates; + // When enabled is undefined, it takes its default value of false. + this.enabled = !!response.enabled; + this.displayName = response.displayName; + } + + /** @returns The plain object representation of the SAMLConfig. */ + public toJSON(): object { + return { + enabled: this.enabled, + displayName: this.displayName, + providerId: this.providerId, + idpEntityId: this.idpEntityId, + ssoURL: this.ssoURL, + x509Certificates: deepCopy(this.x509Certificates), + rpEntityId: this.rpEntityId, + callbackURL: this.callbackURL, + enableRequestSigning: this.enableRequestSigning, + }; + } +} + +/** + * Defines the OIDCConfig class used to convert a client side configuration to its + * server side representation. + * + * @internal + */ +export class OIDCConfig implements OIDCAuthProviderConfig { + public readonly enabled: boolean; + public readonly displayName?: string; + 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 + * accepted by the backend server. + * Throws an error if validation fails. If the request is not a OIDCConfig request, + * returns null. + * + * @param options - The options object to convert to a server request. + * @param ignoreMissingFields - Whether to ignore missing fields. + * @returns The resulting server request or null if not valid. + */ + public static buildServerRequest( + options: Partial, + ignoreMissingFields = false): OIDCConfigServerRequest | null { + const makeRequest = validator.isNonNullObject(options) && + (options.providerId || ignoreMissingFields); + if (!makeRequest) { + return null; + } + const request: OIDCConfigServerRequest = {}; + // Validate options. + OIDCConfig.validate(options, ignoreMissingFields); + request.enabled = options.enabled; + 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; + } + + /** + * Returns the provider ID corresponding to the resource name if available. + * + * @param resourceName - The server side resource name + * @returns The provider ID corresponding to the resource, null otherwise. + */ + public static getProviderIdFromResourceName(resourceName: string): string | null { + // name is of form projects/project1/oauthIdpConfigs/providerId1 + const matchProviderRes = resourceName.match(/\/oauthIdpConfigs\/(oidc\..*)$/); + if (!matchProviderRes || matchProviderRes.length < 2) { + return null; + } + return matchProviderRes[1]; + } + + /** + * @param providerId - The provider ID to check. + * @returns Whether the provider ID corresponds to an OIDC provider. + */ + public static isProviderId(providerId: any): providerId is string { + return validator.isNonEmptyString(providerId) && providerId.indexOf('oidc.') === 0; + } + + /** + * Validates the OIDCConfig options object. Throws an error on failure. + * + * @param options - The options object to validate. + * @param ignoreMissingFields - Whether to ignore missing fields. + */ + public static validate(options: Partial, ignoreMissingFields = false): void { + const validKeys = { + enabled: true, + displayName: true, + providerId: true, + clientId: true, + issuer: true, + clientSecret: true, + responseType: true, + }; + const validResponseTypes = { + idToken: true, + code: true, + }; + if (!validator.isNonNullObject(options)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"OIDCAuthProviderConfig" must be a valid 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 OIDC config parameter.`, + ); + } + } + // Required fields. + if (validator.isNonEmptyString(options.providerId)) { + if (options.providerId.indexOf('oidc.') !== 0) { + throw new FirebaseAuthError( + !options.providerId ? AuthClientErrorCode.MISSING_PROVIDER_ID : AuthClientErrorCode.INVALID_PROVIDER_ID, + '"OIDCAuthProviderConfig.providerId" must be a valid non-empty string prefixed with "oidc.".', + ); + } + } else if (!ignoreMissingFields) { + throw new FirebaseAuthError( + !options.providerId ? AuthClientErrorCode.MISSING_PROVIDER_ID : AuthClientErrorCode.INVALID_PROVIDER_ID, + '"OIDCAuthProviderConfig.providerId" must be a valid non-empty string prefixed with "oidc.".', + ); + } + if (!(ignoreMissingFields && typeof options.clientId === 'undefined') && + !validator.isNonEmptyString(options.clientId)) { + throw new FirebaseAuthError( + !options.clientId ? AuthClientErrorCode.MISSING_OAUTH_CLIENT_ID : AuthClientErrorCode.INVALID_OAUTH_CLIENT_ID, + '"OIDCAuthProviderConfig.clientId" must be a valid non-empty string.', + ); + } + if (!(ignoreMissingFields && typeof options.issuer === 'undefined') && + !validator.isURL(options.issuer)) { + throw new FirebaseAuthError( + !options.issuer ? AuthClientErrorCode.MISSING_ISSUER : AuthClientErrorCode.INVALID_CONFIG, + '"OIDCAuthProviderConfig.issuer" must be a valid URL string.', + ); + } + if (typeof options.enabled !== 'undefined' && + !validator.isBoolean(options.enabled)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"OIDCAuthProviderConfig.enabled" must be a boolean.', + ); + } + if (typeof options.displayName !== 'undefined' && + !validator.isString(options.displayName)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"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.', + ); + } + } + } + + /** + * The OIDCConfig constructor. + * + * @param response - The server side response used to initialize the OIDCConfig object. + * @constructor + */ + constructor(response: OIDCConfigServerResponse) { + if (!response || + !response.issuer || + !response.clientId || + !response.name || + !(validator.isString(response.name) && + OIDCConfig.getProviderIdFromResourceName(response.name))) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Invalid OIDC configuration response'); + } + + const providerId = OIDCConfig.getProviderIdFromResourceName(response.name); + if (!providerId) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Invalid SAML configuration response'); + } + this.providerId = providerId; + + this.clientId = response.clientId; + this.issuer = response.issuer; + // When enabled is undefined, it takes its default value of false. + 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; + } + } + + /** @returns The plain object representation of the OIDCConfig. */ + public toJSON(): OIDCAuthProviderConfig { + return { + enabled: this.enabled, + displayName: this.displayName, + providerId: this.providerId, + issuer: this.issuer, + clientId: this.clientId, + clientSecret: deepCopy(this.clientSecret), + responseType: deepCopy(this.responseType), + }; + } +} + +/** + * The request interface for updating a SMS Region Config. + * Configures the regions where users are allowed to send verification SMS. + * This is based on the calling code of the destination phone number. + */ +export type SmsRegionConfig = AllowByDefaultWrap | AllowlistOnlyWrap; + +/** + * Mutual exclusive SMS Region Config of AllowByDefault interface + */ +export interface AllowByDefaultWrap { + /** + * Allow every region by default. + */ + allowByDefault: AllowByDefault; + /** @alpha */ + allowlistOnly?: never; +} + +/** + * Mutually exclusive SMS Region Config of AllowlistOnly interface + */ +export interface AllowlistOnlyWrap { + /** + * Only allowing regions by explicitly adding them to an + * allowlist. + */ + allowlistOnly: AllowlistOnly; + /** @alpha */ + allowByDefault?: never; +} + +/** + * Defines a policy of allowing every region by default and adding disallowed + * regions to a disallow list. + */ +export interface AllowByDefault { + /** + * Two letter unicode region codes to disallow as defined by + * https://cldr.unicode.org/ + * The full list of these region codes is here: + * https://github.com/unicode-cldr/cldr-localenames-full/blob/master/main/en/territories.json + */ + disallowedRegions: string[]; +} + +/** + * Defines a policy of only allowing regions by explicitly adding them to an + * allowlist. + */ +export interface AllowlistOnly { + /** + * Two letter unicode region codes to allow as defined by + * https://cldr.unicode.org/ + * The full list of these region codes is here: + * https://github.com/unicode-cldr/cldr-localenames-full/blob/master/main/en/territories.json + */ + allowedRegions: string[]; +} + +/** + * Defines the SMSRegionConfig class used for validation. + * + * @internal + */ +export class SmsRegionsAuthConfig { + public static validate(options: SmsRegionConfig): void { + if (!validator.isNonNullObject(options)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"SmsRegionConfig" must be a non-null object.', + ); + } + + const validKeys = { + allowlistOnly: true, + allowByDefault: true, + }; + + for (const key in options) { + if (!(key in validKeys)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + `"${key}" is not a valid SmsRegionConfig parameter.`, + ); + } + } + + // validate mutual exclusiveness of allowByDefault and allowlistOnly + if (typeof options.allowByDefault !== 'undefined' && typeof options.allowlistOnly !== 'undefined') { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + 'SmsRegionConfig cannot have both "allowByDefault" and "allowlistOnly" parameters.', + ); + } + // validation for allowByDefault type + if (typeof options.allowByDefault !== 'undefined') { + const allowByDefaultValidKeys = { + disallowedRegions: true, + } + for (const key in options.allowByDefault) { + if (!(key in allowByDefaultValidKeys)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + `"${key}" is not a valid SmsRegionConfig.allowByDefault parameter.`, + ); + } + } + // disallowedRegion can be empty. + if (typeof options.allowByDefault.disallowedRegions !== 'undefined' + && !validator.isArray(options.allowByDefault.disallowedRegions)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"SmsRegionConfig.allowByDefault.disallowedRegions" must be a valid string array.', + ); + } + } + + if (typeof options.allowlistOnly !== 'undefined') { + const allowListOnlyValidKeys = { + allowedRegions: true, + } + for (const key in options.allowlistOnly) { + if (!(key in allowListOnlyValidKeys)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + `"${key}" is not a valid SmsRegionConfig.allowlistOnly parameter.`, + ); + } + } + + // allowedRegions can be empty + if (typeof options.allowlistOnly.allowedRegions !== 'undefined' + && !validator.isArray(options.allowlistOnly.allowedRegions)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"SmsRegionConfig.allowlistOnly.allowedRegions" must be a valid string array.', + ); + } + } + } +} +/** +* Enforcement state of reCAPTCHA protection. +* - 'OFF': Unenforced. +* - 'AUDIT': Create assessment but don't enforce the result. +* - 'ENFORCE': Create assessment and enforce the result. +*/ +export type RecaptchaProviderEnforcementState = 'OFF' | 'AUDIT' | 'ENFORCE'; + +/** +* The actions to take for reCAPTCHA-protected requests. +* - 'BLOCK': The reCAPTCHA-protected request will be blocked. +*/ +export type RecaptchaAction = 'BLOCK'; + +/** + * The config for a reCAPTCHA action rule. + */ +export interface RecaptchaManagedRule { + /** + * The action will be enforced if the reCAPTCHA score of a request is larger than endScore. + */ + endScore: number; + /** + * The action for reCAPTCHA-protected requests. + */ + action?: RecaptchaAction; +} + +/** + * The key's platform type. + */ +export type RecaptchaKeyClientType = 'WEB' | 'IOS' | 'ANDROID'; + +/** + * The reCAPTCHA key config. + */ +export interface RecaptchaKey { + /** + * The key's client platform type. + */ + type?: RecaptchaKeyClientType; + + /** + * The reCAPTCHA site key. + */ + key: string; +} + +/** + * The request interface for updating a reCAPTCHA Config. + * By enabling reCAPTCHA Enterprise Integration you are + * agreeing to reCAPTCHA Enterprise + * {@link https://cloud.google.com/terms/service-terms | Term of Service}. + */ +export interface RecaptchaConfig { + /** + * The enforcement state of the email password provider. + */ + emailPasswordEnforcementState?: RecaptchaProviderEnforcementState; + /** + * The reCAPTCHA managed rules. + */ + managedRules?: RecaptchaManagedRule[]; + + /** + * The reCAPTCHA keys. + */ + recaptchaKeys?: RecaptchaKey[]; + + /** + * Whether to use account defender for reCAPTCHA assessment. + * The default value is false. + */ + useAccountDefender?: boolean; +} + +export class RecaptchaAuthConfig implements RecaptchaConfig { + public readonly emailPasswordEnforcementState?: RecaptchaProviderEnforcementState; + public readonly managedRules?: RecaptchaManagedRule[]; + public readonly recaptchaKeys?: RecaptchaKey[]; + public readonly useAccountDefender?: boolean; + + constructor(recaptchaConfig: RecaptchaConfig) { + this.emailPasswordEnforcementState = recaptchaConfig.emailPasswordEnforcementState; + this.managedRules = recaptchaConfig.managedRules; + this.recaptchaKeys = recaptchaConfig.recaptchaKeys; + this.useAccountDefender = recaptchaConfig.useAccountDefender; + } + + /** + * Validates the RecaptchaConfig options object. Throws an error on failure. + * @param options - The options object to validate. + */ + public static validate(options: RecaptchaConfig): void { + const validKeys = { + emailPasswordEnforcementState: true, + managedRules: true, + recaptchaKeys: true, + useAccountDefender: true, + }; + + if (!validator.isNonNullObject(options)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"RecaptchaConfig" must be a non-null object.', + ); + } + + for (const key in options) { + if (!(key in validKeys)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + `"${key}" is not a valid RecaptchaConfig parameter.`, + ); + } + } + + // Validation + if (typeof options.emailPasswordEnforcementState !== undefined) { + if (!validator.isNonEmptyString(options.emailPasswordEnforcementState)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"RecaptchaConfig.emailPasswordEnforcementState" must be a valid non-empty string.', + ); + } + + if (options.emailPasswordEnforcementState !== 'OFF' && + options.emailPasswordEnforcementState !== 'AUDIT' && + options.emailPasswordEnforcementState !== 'ENFORCE') { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"RecaptchaConfig.emailPasswordEnforcementState" must be either "OFF", "AUDIT" or "ENFORCE".', + ); + } + } + + if (typeof options.managedRules !== 'undefined') { + // Validate array + if (!validator.isArray(options.managedRules)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"RecaptchaConfig.managedRules" must be an array of valid "RecaptchaManagedRule".', + ); + } + // Validate each rule of the array + options.managedRules.forEach((managedRule) => { + RecaptchaAuthConfig.validateManagedRule(managedRule); + }); + } + + if (typeof options.useAccountDefender !== 'undefined') { + if (!validator.isBoolean(options.useAccountDefender)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"RecaptchaConfig.useAccountDefender" must be a boolean value".', + ); + } + } + } + + /** + * Validate each element in ManagedRule array + * @param options - The options object to validate. + */ + private static validateManagedRule(options: RecaptchaManagedRule): void { + const validKeys = { + endScore: true, + action: true, + } + if (!validator.isNonNullObject(options)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"RecaptchaManagedRule" 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 RecaptchaManagedRule parameter.`, + ); + } + } + + // Validate content. + if (typeof options.action !== 'undefined' && + options.action !== 'BLOCK') { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"RecaptchaManagedRule.action" must be "BLOCK".', + ); + } + } + + /** + * Returns a JSON-serializable representation of this object. + * @returns The JSON-serializable object representation of the ReCaptcha config instance + */ + public toJSON(): object { + const json: any = { + emailPasswordEnforcementState: this.emailPasswordEnforcementState, + managedRules: deepCopy(this.managedRules), + recaptchaKeys: deepCopy(this.recaptchaKeys), + useAccountDefender: this.useAccountDefender, + } + + if (typeof json.emailPasswordEnforcementState === 'undefined') { + delete json.emailPasswordEnforcementState; + } + if (typeof json.managedRules === 'undefined') { + delete json.managedRules; + } + if (typeof json.recaptchaKeys === 'undefined') { + delete json.recaptchaKeys; + } + + if (typeof json.useAccountDefender === 'undefined') { + delete json.useAccountDefender; + } + + return json; + } +} + +/** + * A password policy configuration for a project or tenant +*/ +export interface PasswordPolicyConfig { + /** + * Enforcement state of the password policy + */ + enforcementState?: PasswordPolicyEnforcementState; + /** + * Require users to have a policy-compliant password to sign in + */ + forceUpgradeOnSignin?: boolean; + /** + * The constraints that make up the password strength policy + */ + constraints?: CustomStrengthOptionsConfig; +} + +/** + * A password policy's enforcement state. + */ +export type PasswordPolicyEnforcementState = 'ENFORCE' | 'OFF'; + +/** + * Constraints to be enforced on the password policy + */ +export interface CustomStrengthOptionsConfig { + /** + * The password must contain an upper case character + */ + requireUppercase?: boolean; + /** + * The password must contain a lower case character + */ + requireLowercase?: boolean; + /** + * The password must contain a non-alphanumeric character + */ + requireNonAlphanumeric?: boolean; + /** + * The password must contain a number + */ + requireNumeric?: boolean; + /** + * Minimum password length. Valid values are from 6 to 30 + */ + minLength?: number; + /** + * Maximum password length. No default max length + */ + maxLength?: number; +} + +/** + * Defines the password policy config class used to convert client side PasswordPolicyConfig + * to a format that is understood by the Auth server. + * + * @internal + */ +export class PasswordPolicyAuthConfig implements PasswordPolicyConfig { + + /** + * Identifies a password policy configuration state. + */ + public readonly enforcementState: PasswordPolicyEnforcementState; + /** + * Users must have a password compliant with the password policy to sign-in + */ + public readonly forceUpgradeOnSignin: boolean; + /** + * Must be of length 1. Contains the strength attributes for the password policy + */ + public readonly constraints?: CustomStrengthOptionsConfig; + + /** + * Static method to convert a client side request to a PasswordPolicyAuthServerConfig. + * Throws an error if validation fails. + * + * @param options - The options object to convert to a server request. + * @returns The resulting server request. + * @internal + */ + public static buildServerRequest(options: PasswordPolicyConfig): PasswordPolicyAuthServerConfig { + const request: PasswordPolicyAuthServerConfig = {}; + PasswordPolicyAuthConfig.validate(options); + if (Object.prototype.hasOwnProperty.call(options, 'enforcementState')) { + request.passwordPolicyEnforcementState = options.enforcementState; + } + request.forceUpgradeOnSignin = false; + if (Object.prototype.hasOwnProperty.call(options, 'forceUpgradeOnSignin')) { + request.forceUpgradeOnSignin = options.forceUpgradeOnSignin; + } + const constraintsRequest: CustomStrengthOptionsAuthServerConfig = { + containsUppercaseCharacter: false, + containsLowercaseCharacter: false, + containsNonAlphanumericCharacter: false, + containsNumericCharacter: false, + minPasswordLength: 6, + maxPasswordLength: 4096, + }; + request.passwordPolicyVersions = []; + if (Object.prototype.hasOwnProperty.call(options, 'constraints')) { + if (options) { + if (options.constraints?.requireUppercase !== undefined) { + constraintsRequest.containsUppercaseCharacter = options.constraints.requireUppercase; + } + if (options.constraints?.requireLowercase !== undefined) { + constraintsRequest.containsLowercaseCharacter = options.constraints.requireLowercase; + } + if (options.constraints?.requireNonAlphanumeric !== undefined) { + constraintsRequest.containsNonAlphanumericCharacter = options.constraints.requireNonAlphanumeric; + } + if (options.constraints?.requireNumeric !== undefined) { + constraintsRequest.containsNumericCharacter = options.constraints.requireNumeric; + } + if (options.constraints?.minLength !== undefined) { + constraintsRequest.minPasswordLength = options.constraints.minLength; + } + if (options.constraints?.maxLength !== undefined) { + constraintsRequest.maxPasswordLength = options.constraints.maxLength; + } + } + } + request.passwordPolicyVersions.push({ customStrengthOptions: constraintsRequest }); + return request; + } + + /** + * Validates the PasswordPolicyConfig options object. Throws an error on failure. + * + * @param options - The options object to validate. + * @internal + */ + public static validate(options: PasswordPolicyConfig): void { + const validKeys = { + enforcementState: true, + forceUpgradeOnSignin: true, + constraints: true, + }; + if (!validator.isNonNullObject(options)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"PasswordPolicyConfig" 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 PasswordPolicyConfig parameter.`, + ); + } + } + // Validate content. + if (typeof options.enforcementState === 'undefined' || + !(options.enforcementState === 'ENFORCE' || + options.enforcementState === 'OFF')) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"PasswordPolicyConfig.enforcementState" must be either "ENFORCE" or "OFF".', + ); + } + + if (typeof options.forceUpgradeOnSignin !== 'undefined') { + if (!validator.isBoolean(options.forceUpgradeOnSignin)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"PasswordPolicyConfig.forceUpgradeOnSignin" must be a boolean.', + ); + } + } + + if (typeof options.constraints !== 'undefined') { + if (options.enforcementState === 'ENFORCE' && !validator.isNonNullObject(options.constraints)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"PasswordPolicyConfig.constraints" must be a non-empty object.', + ); + } + + const validCharKeys = { + requireUppercase: true, + requireLowercase: true, + requireNumeric: true, + requireNonAlphanumeric: true, + minLength: true, + maxLength: true, + }; + + // Check for unsupported attributes. + for (const key in options.constraints) { + if (!(key in validCharKeys)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + `"${key}" is not a valid PasswordPolicyConfig.constraints parameter.`, + ); + } + } + if (typeof options.constraints.requireUppercase !== 'undefined' && + !validator.isBoolean(options.constraints.requireUppercase)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"PasswordPolicyConfig.constraints.requireUppercase" must be a boolean.', + ); + } + if (typeof options.constraints.requireLowercase !== 'undefined' && + !validator.isBoolean(options.constraints.requireLowercase)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"PasswordPolicyConfig.constraints.requireLowercase" must be a boolean.', + ); + } + if (typeof options.constraints.requireNonAlphanumeric !== 'undefined' && + !validator.isBoolean(options.constraints.requireNonAlphanumeric)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"PasswordPolicyConfig.constraints.requireNonAlphanumeric"' + + ' must be a boolean.', + ); + } + if (typeof options.constraints.requireNumeric !== 'undefined' && + !validator.isBoolean(options.constraints.requireNumeric)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"PasswordPolicyConfig.constraints.requireNumeric" must be a boolean.', + ); + } + if (typeof options.constraints.minLength === 'undefined') { + options.constraints.minLength = 6; + } else if (!validator.isNumber(options.constraints.minLength)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"PasswordPolicyConfig.constraints.minLength" must be a number.', + ); + } else { + if (!(options.constraints.minLength >= 6 + && options.constraints.minLength <= 30)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"PasswordPolicyConfig.constraints.minLength"' + + ' must be an integer between 6 and 30, inclusive.', + ); + } + } + if (typeof options.constraints.maxLength === 'undefined') { + options.constraints.maxLength = 4096; + } else if (!validator.isNumber(options.constraints.maxLength)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"PasswordPolicyConfig.constraints.maxLength" must be a number.', + ); + } else { + if (!(options.constraints.maxLength >= options.constraints.minLength && + options.constraints.maxLength <= 4096)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"PasswordPolicyConfig.constraints.maxLength"' + + ' must be greater than or equal to minLength and at max 4096.', + ); + } + } + } else { + if (options.enforcementState === 'ENFORCE') { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"PasswordPolicyConfig.constraints" must be defined.', + ); + } + } + } + + /** + * The PasswordPolicyAuthConfig constructor. + * + * @param response - The server side response used to initialize the + * PasswordPolicyAuthConfig object. + * @constructor + * @internal + */ + constructor(response: PasswordPolicyAuthServerConfig) { + if (typeof response.passwordPolicyEnforcementState === 'undefined') { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Invalid password policy configuration response'); + } + this.enforcementState = response.passwordPolicyEnforcementState; + let constraintsResponse: CustomStrengthOptionsConfig = {}; + if (typeof response.passwordPolicyVersions !== 'undefined') { + (response.passwordPolicyVersions || []).forEach((policyVersion) => { + constraintsResponse = { + requireLowercase: policyVersion.customStrengthOptions?.containsLowercaseCharacter, + requireUppercase: policyVersion.customStrengthOptions?.containsUppercaseCharacter, + requireNonAlphanumeric: policyVersion.customStrengthOptions?.containsNonAlphanumericCharacter, + requireNumeric: policyVersion.customStrengthOptions?.containsNumericCharacter, + minLength: policyVersion.customStrengthOptions?.minPasswordLength, + maxLength: policyVersion.customStrengthOptions?.maxPasswordLength, + }; + }); + } + this.constraints = constraintsResponse; + this.forceUpgradeOnSignin = response.forceUpgradeOnSignin?true:false; + } +} + +/** + * Server side password policy configuration. + */ +export interface PasswordPolicyAuthServerConfig { + passwordPolicyEnforcementState?: PasswordPolicyEnforcementState; + passwordPolicyVersions?: PasswordPolicyVersionsAuthServerConfig[]; + forceUpgradeOnSignin?: boolean; +} + +/** + * Server side password policy versions configuration. + */ +export interface PasswordPolicyVersionsAuthServerConfig { + customStrengthOptions?: CustomStrengthOptionsAuthServerConfig; +} + +/** + * Server side password policy constraints configuration. + */ +export interface CustomStrengthOptionsAuthServerConfig { + containsLowercaseCharacter?: boolean; + containsUppercaseCharacter?: boolean; + containsNumericCharacter?: boolean; + containsNonAlphanumericCharacter?: boolean; + minPasswordLength?: number; + maxPasswordLength?: number; +} + +/** + * The email privacy configuration of a project or tenant. + */ +export interface EmailPrivacyConfig { + /** + * Whether enhanced email privacy is enabled. + */ + enableImprovedEmailPrivacy?: boolean; +} + +/** + * Defines the EmailPrivacyAuthConfig class used for validation. + * + * @internal + */ +export class EmailPrivacyAuthConfig { + public static validate(options: EmailPrivacyConfig): void { + if (!validator.isNonNullObject(options)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"EmailPrivacyConfig" must be a non-null object.', + ); + } + + const validKeys = { + enableImprovedEmailPrivacy: true, + }; + + for (const key in options) { + if (!(key in validKeys)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + `"${key}" is not a valid "EmailPrivacyConfig" parameter.`, + ); + } + } + + if (typeof options.enableImprovedEmailPrivacy !== 'undefined' + && !validator.isBoolean(options.enableImprovedEmailPrivacy)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"EmailPrivacyConfig.enableImprovedEmailPrivacy" must be a valid boolean value.', + ); + } + } +} diff --git a/src/auth/auth-namespace.ts b/src/auth/auth-namespace.ts new file mode 100644 index 0000000000..486c0b488a --- /dev/null +++ b/src/auth/auth-namespace.ts @@ -0,0 +1,381 @@ +/*! + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { App } from '../app/index'; + +// Import all public types with aliases, and re-export from the auth namespace. + +import { ActionCodeSettings as TActionCodeSettings } from './action-code-settings-builder'; + +import { Auth as TAuth } from './auth'; + +import { + AuthFactorType as TAuthFactorType, + AuthProviderConfig as TAuthProviderConfig, + AuthProviderConfigFilter as TAuthProviderConfigFilter, + CreateRequest as TCreateRequest, + CreateMultiFactorInfoRequest as TCreateMultiFactorInfoRequest, + CreatePhoneMultiFactorInfoRequest as TCreatePhoneMultiFactorInfoRequest, + EmailSignInProviderConfig as TEmailSignInProviderConfig, + ListProviderConfigResults as TListProviderConfigResults, + MultiFactorCreateSettings as TMultiFactorCreateSettings, + MultiFactorConfig as TMultiFactorConfig, + MultiFactorConfigState as TMultiFactorConfigState, + MultiFactorUpdateSettings as TMultiFactorUpdateSettings, + OIDCAuthProviderConfig as TOIDCAuthProviderConfig, + OIDCUpdateAuthProviderRequest as TOIDCUpdateAuthProviderRequest, + SAMLAuthProviderConfig as TSAMLAuthProviderConfig, + SAMLUpdateAuthProviderRequest as TSAMLUpdateAuthProviderRequest, + UpdateAuthProviderRequest as TUpdateAuthProviderRequest, + UpdateMultiFactorInfoRequest as TUpdateMultiFactorInfoRequest, + UpdatePhoneMultiFactorInfoRequest as TUpdatePhoneMultiFactorInfoRequest, + UpdateRequest as TUpdateRequest, +} from './auth-config'; + +import { + BaseAuth as TBaseAuth, + DeleteUsersResult as TDeleteUsersResult, + GetUsersResult as TGetUsersResult, + ListUsersResult as TListUsersResult, + SessionCookieOptions as TSessionCookieOptions, +} from './base-auth'; + +import { + EmailIdentifier as TEmailIdentifier, + PhoneIdentifier as TPhoneIdentifier, + ProviderIdentifier as TProviderIdentifier, + UserIdentifier as TUserIdentifier, + UidIdentifier as TUidIdentifier, +} from './identifier'; + +import { + CreateTenantRequest as TCreateTenantRequest, + Tenant as TTenant, + UpdateTenantRequest as TUpdateTenantRequest, +} from './tenant'; + +import { + ListTenantsResult as TListTenantsResult, + TenantAwareAuth as TTenantAwareAuth, + TenantManager as TTenantManager, +} from './tenant-manager'; + +import { + DecodedIdToken as TDecodedIdToken, + DecodedAuthBlockingToken as TDecodedAuthBlockingToken, +} from './token-verifier'; + +import { + HashAlgorithmType as THashAlgorithmType, + UserImportOptions as TUserImportOptions, + UserImportRecord as TUserImportRecord, + UserImportResult as TUserImportResult, + UserMetadataRequest as TUserMetadataRequest, + UserProviderRequest as TUserProviderRequest, +} from './user-import-builder'; + +import { + MultiFactorInfo as TMultiFactorInfo, + MultiFactorSettings as TMultiFactorSettings, + PhoneMultiFactorInfo as TPhoneMultiFactorInfo, + UserInfo as TUserInfo, + UserMetadata as TUserMetadata, + UserRecord as TUserRecord, +} from './user-record'; + +/** + * Gets the {@link firebase-admin.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 firebase-admin.auth#Auth} service or as `admin.auth(app)` to access the + * {@link firebase-admin.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): auth.Auth; + +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace auth { + /** + * Type alias to {@link firebase-admin.auth#ActionCodeSettings}. + */ + export type ActionCodeSettings = TActionCodeSettings; + + /** + * Type alias to {@link firebase-admin.auth#Auth}. + */ + export type Auth = TAuth; + + /** + * Type alias to {@link firebase-admin.auth#AuthFactorType}. + */ + export type AuthFactorType = TAuthFactorType; + + /** + * Type alias to {@link firebase-admin.auth#AuthProviderConfig}. + */ + export type AuthProviderConfig = TAuthProviderConfig; + + /** + * Type alias to {@link firebase-admin.auth#AuthProviderConfigFilter}. + */ + export type AuthProviderConfigFilter = TAuthProviderConfigFilter; + + /** + * Type alias to {@link firebase-admin.auth#BaseAuth}. + */ + export type BaseAuth = TBaseAuth; + + /** + * Type alias to {@link firebase-admin.auth#CreateMultiFactorInfoRequest}. + */ + export type CreateMultiFactorInfoRequest = TCreateMultiFactorInfoRequest; + + /** + * Type alias to {@link firebase-admin.auth#CreatePhoneMultiFactorInfoRequest}. + */ + export type CreatePhoneMultiFactorInfoRequest = TCreatePhoneMultiFactorInfoRequest; + + /** + * Type alias to {@link firebase-admin.auth#CreateRequest}. + */ + export type CreateRequest = TCreateRequest; + + /** + * Type alias to {@link firebase-admin.auth#CreateTenantRequest}. + */ + export type CreateTenantRequest = TCreateTenantRequest; + + /** + * Type alias to {@link firebase-admin.auth#DecodedIdToken}. + */ + export type DecodedIdToken = TDecodedIdToken; + + /** @alpha */ + export type DecodedAuthBlockingToken = TDecodedAuthBlockingToken; + + /** + * Type alias to {@link firebase-admin.auth#DeleteUsersResult}. + */ + export type DeleteUsersResult = TDeleteUsersResult; + + /** + * Type alias to {@link firebase-admin.auth#EmailIdentifier}. + */ + export type EmailIdentifier = TEmailIdentifier; + + /** + * Type alias to {@link firebase-admin.auth#EmailSignInProviderConfig}. + */ + export type EmailSignInProviderConfig = TEmailSignInProviderConfig; + + /** + * Type alias to {@link firebase-admin.auth#GetUsersResult}. + */ + export type GetUsersResult = TGetUsersResult; + + /** + * Type alias to {@link firebase-admin.auth#HashAlgorithmType}. + */ + export type HashAlgorithmType = THashAlgorithmType; + + /** + * Type alias to {@link firebase-admin.auth#ListProviderConfigResults}. + */ + export type ListProviderConfigResults = TListProviderConfigResults; + + /** + * Type alias to {@link firebase-admin.auth#ListTenantsResult}. + */ + export type ListTenantsResult = TListTenantsResult; + + /** + * Type alias to {@link firebase-admin.auth#ListUsersResult}. + */ + export type ListUsersResult = TListUsersResult; + + /** + * Type alias to {@link firebase-admin.auth#MultiFactorCreateSettings}. + */ + export type MultiFactorCreateSettings = TMultiFactorCreateSettings; + + /** + * Type alias to {@link firebase-admin.auth#MultiFactorConfig}. + */ + export type MultiFactorConfig = TMultiFactorConfig; + + /** + * Type alias to {@link firebase-admin.auth#MultiFactorConfigState}. + */ + export type MultiFactorConfigState = TMultiFactorConfigState; + + /** + * Type alias to {@link firebase-admin.auth#MultiFactorInfo}. + */ + export type MultiFactorInfo = TMultiFactorInfo; + + /** + * Type alias to {@link firebase-admin.auth#MultiFactorUpdateSettings}. + */ + export type MultiFactorUpdateSettings = TMultiFactorUpdateSettings; + + /** + * Type alias to {@link firebase-admin.auth#MultiFactorSettings}. + */ + export type MultiFactorSettings = TMultiFactorSettings; + + /** + * Type alias to {@link firebase-admin.auth#OIDCAuthProviderConfig}. + */ + export type OIDCAuthProviderConfig = TOIDCAuthProviderConfig; + + /** + * Type alias to {@link firebase-admin.auth#OIDCUpdateAuthProviderRequest}. + */ + export type OIDCUpdateAuthProviderRequest = TOIDCUpdateAuthProviderRequest; + + /** + * Type alias to {@link firebase-admin.auth#PhoneIdentifier}. + */ + export type PhoneIdentifier = TPhoneIdentifier; + + /** + * Type alias to {@link firebase-admin.auth#PhoneMultiFactorInfo}. + */ + export type PhoneMultiFactorInfo = TPhoneMultiFactorInfo; + + /** + * Type alias to {@link firebase-admin.auth#ProviderIdentifier}. + */ + export type ProviderIdentifier = TProviderIdentifier; + + /** + * Type alias to {@link firebase-admin.auth#SAMLAuthProviderConfig}. + */ + export type SAMLAuthProviderConfig = TSAMLAuthProviderConfig; + + /** + * Type alias to {@link firebase-admin.auth#SAMLUpdateAuthProviderRequest}. + */ + export type SAMLUpdateAuthProviderRequest = TSAMLUpdateAuthProviderRequest; + + /** + * Type alias to {@link firebase-admin.auth#SessionCookieOptions}. + */ + export type SessionCookieOptions = TSessionCookieOptions; + + /** + * Type alias to {@link firebase-admin.auth#Tenant}. + */ + export type Tenant = TTenant; + + /** + * Type alias to {@link firebase-admin.auth#TenantAwareAuth}. + */ + export type TenantAwareAuth = TTenantAwareAuth; + + /** + * Type alias to {@link firebase-admin.auth#TenantManager}. + */ + export type TenantManager = TTenantManager; + + /** + * Type alias to {@link firebase-admin.auth#UidIdentifier}. + */ + export type UidIdentifier = TUidIdentifier; + + /** + * Type alias to {@link firebase-admin.auth#UpdateAuthProviderRequest}. + */ + export type UpdateAuthProviderRequest = TUpdateAuthProviderRequest; + + /** + * Type alias to {@link firebase-admin.auth#UpdateMultiFactorInfoRequest}. + */ + export type UpdateMultiFactorInfoRequest = TUpdateMultiFactorInfoRequest; + + /** + * Type alias to {@link firebase-admin.auth#UpdatePhoneMultiFactorInfoRequest}. + */ + export type UpdatePhoneMultiFactorInfoRequest = TUpdatePhoneMultiFactorInfoRequest; + + /** + * Type alias to {@link firebase-admin.auth#UpdateRequest}. + */ + export type UpdateRequest = TUpdateRequest; + + /** + * Type alias to {@link firebase-admin.auth#UpdateTenantRequest}. + */ + export type UpdateTenantRequest = TUpdateTenantRequest; + + /** + * Type alias to {@link firebase-admin.auth#UserIdentifier}. + */ + export type UserIdentifier = TUserIdentifier; + + /** + * Type alias to {@link firebase-admin.auth#UserImportOptions}. + */ + export type UserImportOptions = TUserImportOptions; + + /** + * Type alias to {@link firebase-admin.auth#UserImportRecord}. + */ + export type UserImportRecord = TUserImportRecord; + + /** + * Type alias to {@link firebase-admin.auth#UserImportResult}. + */ + export type UserImportResult = TUserImportResult; + + /** + * Type alias to {@link firebase-admin.auth#UserInfo}. + */ + export type UserInfo = TUserInfo; + + /** + * Type alias to {@link firebase-admin.auth#UserMetadata}. + */ + export type UserMetadata = TUserMetadata; + + /** + * Type alias to {@link firebase-admin.auth#UserMetadataRequest}. + */ + export type UserMetadataRequest = TUserMetadataRequest; + + /** + * Type alias to {@link firebase-admin.auth#UserProviderRequest}. + */ + export type UserProviderRequest = TUserProviderRequest; + + /** + * Type alias to {@link firebase-admin.auth#UserRecord}. + */ + export type UserRecord = TUserRecord; +} diff --git a/src/auth/auth.ts b/src/auth/auth.ts index 805a461157..4808fbbdc0 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"); @@ -14,340 +15,58 @@ * limitations under the License. */ -import {UserRecord, CreateRequest, UpdateRequest} from './user-record'; -import {Certificate} from './credential'; -import {FirebaseApp} from '../firebase-app'; -import {FirebaseTokenGenerator} from './token-generator'; -import {FirebaseAuthRequestHandler} from './auth-api-request'; -import {AuthClientErrorCode, FirebaseAuthError} from '../utils/error'; -import {FirebaseServiceInterface, FirebaseServiceInternalsInterface} from '../firebase-service'; - -import * as validator from '../utils/validator'; - - -/** - * 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); - } -} - - -/** Response object for a listUsers operation. */ -export interface ListUsersResult { - users: UserRecord[]; - pageToken?: string; -} - - -/** Inteface representing a decoded ID token. */ -export interface DecodedIdToken { - aud: string; - auth_time: number; - exp: number; - firebase: { - identities: { - [key: string]: any; - }; - sign_in_provider: string; - [key: string]: any; - }; - iat: number; - iss: string; - sub: string; - [key: string]: any; -} - +import { App } from '../app/index'; +import { AuthRequestHandler } from './auth-api-request'; +import { TenantManager } from './tenant-manager'; +import { BaseAuth } from './base-auth'; +import { ProjectConfigManager } from './project-config-manager'; /** * Auth service bound to the provided app. + * An Auth instance can have multiple tenants. */ -export class Auth implements FirebaseServiceInterface { - public INTERNAL: AuthInternals = new AuthInternals(); +export class Auth extends BaseAuth { - private app_: FirebaseApp; - private tokenGenerator_: FirebaseTokenGenerator; - private authRequestHandler: FirebaseAuthRequestHandler; + private readonly tenantManager_: TenantManager; + private readonly projectConfigManager_: ProjectConfigManager; + private readonly app_: App; /** - * @param {object} app The app for this Auth service. + * @param app - The app for this Auth service. * @constructor + * @internal */ - constructor(app: FirebaseApp) { - if (typeof app !== 'object' || app === null || !('options' in app)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, - 'First argument passed to admin.auth() must be a valid Firebase app instance.', - ); - } - + constructor(app: App) { + super(app, new AuthRequestHandler(app)); this.app_ = app; - - // TODO (inlined): plumb this into a factory method for tokenGenerator_ once we - // can generate custom tokens from access tokens. - let serviceAccount; - if (typeof app.options.credential.getCertificate === 'function') { - serviceAccount = app.options.credential.getCertificate(); - } - if (serviceAccount) { - // Cert credentials and Application Default Credentials created from a service account file - // provide a certificate we can use to mint custom tokens and verify ID tokens. - this.tokenGenerator_ = new FirebaseTokenGenerator(serviceAccount); - } else if (validator.isNonEmptyString(process.env.GCLOUD_PROJECT)) { - // Google infrastructure like GAE, GCE, and GCF store the GCP / Firebase project ID in an - // environment variable that we can use to get verifyIdToken() to work. createCustomToken() - // still won't work since it requires a private key and client email which we do not have. - const cert: any = { - projectId: process.env.GCLOUD_PROJECT, - }; - this.tokenGenerator_ = new FirebaseTokenGenerator(cert); - } - // Initialize auth request handler with the app. - this.authRequestHandler = new FirebaseAuthRequestHandler(app); + this.tenantManager_ = new TenantManager(app); + this.projectConfigManager_ = new ProjectConfigManager(app); } /** * Returns the app associated with this Auth instance. * - * @return {FirebaseApp} The app associated with this Auth instance. + * @returns The app associated with this Auth instance. */ - get app(): FirebaseApp { + get app(): App { return this.app_; } /** - * Creates a new custom token that can be sent back to a client to use with - * signInWithCustomToken(). - * - * @param {string} uid The uid to use as the JWT subject. - * @param {object=} developerClaims Optional additional claims to include in the JWT payload. - * - * @return {Promise} A JWT for the provided payload. - */ - public createCustomToken(uid: string, developerClaims?: object): Promise { - if (typeof this.tokenGenerator_ === 'undefined') { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CREDENTIAL, - 'Must initialize app with a cert credential to call auth().createCustomToken().', - ); - } - return this.tokenGenerator_.createCustomToken(uid, developerClaims); - } - - /** - * Verifies a JWT auth token. Returns a Promise with the tokens claims. Rejects - * the promise if the token could not be verified. If checkRevoked is set to true, - * verifies if the session corresponding to the ID token was revoked. If the corresponding - * user's session was invalidated, an auth/id-token-revoked error is thrown. If not specified - * the check is not applied. - * - * @param {string} idToken The JWT to verify. - * @param {boolean=} checkRevoked Whether to check if the ID token is revoked. - * @return {Promise} A Promise that will be fulfilled after a successful - * verification. - */ - public verifyIdToken(idToken: string, checkRevoked: boolean = false): Promise { - if (typeof this.tokenGenerator_ === 'undefined') { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CREDENTIAL, - 'Must initialize app with a cert credential or set your Firebase project ID as the ' + - 'GCLOUD_PROJECT environment variable to call auth().verifyIdToken().', - ); - } - return this.tokenGenerator_.verifyIdToken(idToken) - .then((decodedIdToken: DecodedIdToken) => { - // Whether to check if the token was revoked. - if (!checkRevoked) { - return decodedIdToken; - } - // Get tokens valid after time for the corresponding user. - return this.getUser(decodedIdToken.sub) - .then((user: UserRecord) => { - // If no tokens valid after time available, token is not revoked. - if (user.tokensValidAfterTime) { - // Get the ID token authentication time and convert to milliseconds UTC. - const authTimeUtc = decodedIdToken.auth_time * 1000; - // Get user tokens valid after time in milliseconds UTC. - const validSinceUtc = new Date(user.tokensValidAfterTime).getTime(); - // Check if authentication time is older than valid since time. - if (authTimeUtc < validSinceUtc) { - throw new FirebaseAuthError(AuthClientErrorCode.ID_TOKEN_REVOKED); - } - } - // All checks above passed. Return the decoded token. - return decodedIdToken; - }); - }); - } - - /** - * Looks up the user identified by the provided user id and returns a promise that is - * fulfilled with a user record for the given user if that user is found. - * - * @param {string} uid The uid of the user to look up. - * @return {Promise} A promise that resolves with the corresponding user record. - */ - public getUser(uid: string): Promise { - return this.authRequestHandler.getAccountInfoByUid(uid) - .then((response: any) => { - // Returns the user record populated with server response. - return new UserRecord(response.users[0]); - }); - } - - /** - * Looks up the user identified by the provided email and returns a promise that is - * fulfilled with a user record for the given user if that user is found. - * - * @param {string} email The email of the user to look up. - * @return {Promise} A promise that resolves with the corresponding user record. - */ - public getUserByEmail(email: string): Promise { - return this.authRequestHandler.getAccountInfoByEmail(email) - .then((response: any) => { - // Returns the user record populated with server response. - return new UserRecord(response.users[0]); - }); - } - - /** - * Looks up the user identified by the provided phone number and returns a promise that is - * fulfilled with a user record for the given user if that user is found. - * - * @param {string} phoneNumber The phone number of the user to look up. - * @return {Promise} A promise that resolves with the corresponding user record. - */ - public getUserByPhoneNumber(phoneNumber: string): Promise { - return this.authRequestHandler.getAccountInfoByPhoneNumber(phoneNumber) - .then((response: any) => { - // Returns the user record populated with server response. - return new UserRecord(response.users[0]); - }); - } - - /** - * Exports a batch of user accounts. Batch size is determined by the maxResults argument. - * Starting point of the batch is determined by the pageToken argument. - * - * @param {number=} maxResults The page size, 1000 if undefined. This is also the maximum - * allowed limit. - * @param {string=} pageToken The next page token. If not specified, returns users starting - * without any offset. - * @return {Promise<{users: UserRecord[], pageToken?: string}>} A promise that resolves with - * the current batch of downloaded users and the next page token. For the last page, an - * empty list of users and no page token are returned. - */ - public listUsers(maxResults?: number, pageToken?: string): Promise { - return this.authRequestHandler.downloadAccount(maxResults, pageToken) - .then((response: any) => { - // List of users to return. - const users: UserRecord[] = []; - // Convert each user response to a UserRecord. - response.users.forEach((userResponse) => { - users.push(new UserRecord(userResponse)); - }); - // Return list of user records and the next page token if available. - const result = { - users, - pageToken: response.nextPageToken, - }; - // Delete result.pageToken if undefined. - if (typeof result.pageToken === 'undefined') { - delete result.pageToken; - } - return result; - }); - } - - /** - * Creates a new user with the properties provided. - * - * @param {CreateRequest} properties The properties to set on the new user record to be created. - * @return {Promise} A promise that resolves with the newly created user record. - */ - public createUser(properties: CreateRequest): Promise { - return this.authRequestHandler.createNewAccount(properties) - .then((uid) => { - // Return the corresponding user record. - return this.getUser(uid); - }) - .catch((error) => { - if (error.code === 'auth/user-not-found') { - // Something must have happened after creating the user and then retrieving it. - throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, - 'Unable to create the user record provided.'); - } - throw error; - }); - } - - /** - * Deletes the user identified by the provided user id and returns a promise that is - * fulfilled when the user is found and successfully deleted. - * - * @param {string} uid The uid of the user to delete. - * @return {Promise} A promise that resolves when the user is successfully deleted. - */ - public deleteUser(uid: string): Promise { - return this.authRequestHandler.deleteAccount(uid) - .then((response) => { - // Return nothing on success. - }); - } - - /** - * Updates an existing user with the properties provided. - * - * @param {string} uid The uid identifier of the user to update. - * @param {UpdateRequest} properties The properties to update on the existing user. - * @return {Promise} A promise that resolves with the modified user record. - */ - public updateUser(uid: string, properties: UpdateRequest): Promise { - return this.authRequestHandler.updateExistingAccount(uid, properties) - .then((existingUid) => { - // Return the corresponding user record. - return this.getUser(existingUid); - }); - } - - /** - * Sets additional developer claims on an existing user identified by the provided UID. + * Returns the tenant manager instance associated with the current project. * - * @param {string} uid The user to edit. - * @param {object} customUserClaims The developer claims to set. - * @return {Promise} A promise that resolves when the operation completes - * successfully. + * @returns The tenant manager instance associated with the current project. */ - public setCustomUserClaims(uid: string, customUserClaims: object): Promise { - return this.authRequestHandler.setCustomUserClaims(uid, customUserClaims) - .then((existingUid) => { - // Return nothing on success. - }); + public tenantManager(): TenantManager { + return this.tenantManager_; } /** - * Revokes all refresh tokens for the specified user identified by the provided UID. - * In addition to revoking all refresh tokens for a user, all ID tokens issued before - * revocation will also be revoked on the Auth backend. Any request with an ID token - * generated before revocation will be rejected with a token expired error. + * Returns the project config manager instance associated with the current project. * - * @param {string} uid The user whose tokens are to be revoked. - * @return {Promise} A promise that resolves when the operation completes - * successfully. + * @returns The project config manager instance associated with the current project. */ - public revokeRefreshTokens(uid: string): Promise { - return this.authRequestHandler.revokeRefreshTokens(uid) - .then((existingUid) => { - // Return nothing on success. - }); + public projectConfigManager(): ProjectConfigManager { + return this.projectConfigManager_; } } diff --git a/src/auth/base-auth.ts b/src/auth/base-auth.ts new file mode 100644 index 0000000000..6f77e088f8 --- /dev/null +++ b/src/auth/base-auth.ts @@ -0,0 +1,1142 @@ +/*! + * 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, FirebaseArrayIndexError } from '../app'; +import { AuthClientErrorCode, ErrorInfo, FirebaseAuthError } from '../utils/error'; +import { deepCopy } from '../utils/deep-copy'; +import * as validator from '../utils/validator'; + +import { AbstractAuthRequestHandler, useEmulator } from './auth-api-request'; +import { FirebaseTokenGenerator, EmulatedSigner, handleCryptoSignerError } from './token-generator'; +import { + FirebaseTokenVerifier, + createSessionCookieVerifier, + createIdTokenVerifier, + createAuthBlockingTokenVerifier, + DecodedIdToken, + DecodedAuthBlockingToken, +} from './token-verifier'; +import { + AuthProviderConfig, SAMLAuthProviderConfig, AuthProviderConfigFilter, ListProviderConfigResults, + SAMLConfig, OIDCConfig, OIDCConfigServerResponse, SAMLConfigServerResponse, + UpdateAuthProviderRequest, OIDCAuthProviderConfig, CreateRequest, UpdateRequest, +} from './auth-config'; +import { UserRecord } from './user-record'; +import { + UserIdentifier, isUidIdentifier, isEmailIdentifier, isPhoneIdentifier, isProviderIdentifier, +} from './identifier'; +import { UserImportOptions, UserImportRecord, UserImportResult } from './user-import-builder'; +import { ActionCodeSettings } from './action-code-settings-builder'; +import { cryptoSignerFromApp } from '../utils/crypto-signer'; + +/** Represents the result of the {@link BaseAuth.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[]; +} + +/** + * Interface representing the object returned from a + * {@link BaseAuth.listUsers} operation. Contains the list + * of users for the current batch and the next page token if available. + */ +export interface ListUsersResult { + + /** + * The list of {@link UserRecord} objects for the + * current downloaded batch. + */ + users: UserRecord[]; + + /** + * The next page token if available. This is needed for the next batch download. + */ + pageToken?: string; +} + +/** + * Represents the result of the {@link BaseAuth.deleteUsers}. + * API. + */ +export interface DeleteUsersResult { + /** + * The number of user records that failed to be deleted (possibly zero). + */ + failureCount: number; + + /** + * The number of users that were deleted successfully (possibly zero). + * Users that did not exist prior to calling `deleteUsers()` are + * considered to be successfully deleted. + */ + successCount: number; + + /** + * A list of `FirebaseArrayIndexError` instances describing the errors that + * were encountered during the deletion. Length of this list is equal to + * the return value of {@link DeleteUsersResult.failureCount}. + */ + errors: FirebaseArrayIndexError[]; +} + +/** + * Interface representing the session cookie options needed for the + * {@link BaseAuth.createSessionCookie} method. + */ +export interface SessionCookieOptions { + + /** + * The session cookie custom expiration in milliseconds. The minimum allowed is + * 5 minutes and the maxium allowed is 2 weeks. + */ + expiresIn: number; +} + +/** + * @internal + */ +export function createFirebaseTokenGenerator(app: App, + tenantId?: string): FirebaseTokenGenerator { + try { + const signer = useEmulator() ? new EmulatedSigner() : cryptoSignerFromApp(app); + return new FirebaseTokenGenerator(signer, tenantId); + } catch (err) { + throw handleCryptoSignerError(err); + } +} + +/** + * Common parent interface for both `Auth` and `TenantAwareAuth` APIs. + */ +export abstract class BaseAuth { + + /** @internal */ + protected readonly tokenGenerator: FirebaseTokenGenerator; + /** @internal */ + protected readonly idTokenVerifier: FirebaseTokenVerifier; + /** @internal */ + protected readonly authBlockingTokenVerifier: FirebaseTokenVerifier; + /** @internal */ + protected readonly sessionCookieVerifier: FirebaseTokenVerifier; + + /** + * The BaseAuth class constructor. + * + * @param app - The FirebaseApp to associate with this Auth instance. + * @param authRequestHandler - The RPC request handler for this instance. + * @param tokenGenerator - Optional token generator. If not specified, a + * (non-tenant-aware) instance will be created. Use this paramter to + * specify a tenant-aware tokenGenerator. + * @constructor + * @internal + */ + constructor( + app: App, + /** @internal */ protected readonly authRequestHandler: AbstractAuthRequestHandler, + tokenGenerator?: FirebaseTokenGenerator) { + if (tokenGenerator) { + this.tokenGenerator = tokenGenerator; + } else { + this.tokenGenerator = createFirebaseTokenGenerator(app); + } + + this.sessionCookieVerifier = createSessionCookieVerifier(app); + this.idTokenVerifier = createIdTokenVerifier(app); + this.authBlockingTokenVerifier = createAuthBlockingTokenVerifier(app); + } + + /** + * Creates a new Firebase custom token (JWT) that can be sent back to a client + * device to use to sign in with the client SDKs' `signInWithCustomToken()` + * methods. (Tenant-aware instances will also embed the tenant ID in the + * token.) + * + * See {@link https://firebase.google.com/docs/auth/admin/create-custom-tokens | Create Custom Tokens} + * for code samples and detailed documentation. + * + * @param uid - The `uid` to use as the custom token's subject. + * @param developerClaims - Optional additional claims to include + * in the custom token's payload. + * + * @returns A promise fulfilled with a custom token for the + * provided `uid` and payload. + */ + public createCustomToken(uid: string, developerClaims?: object): Promise { + return this.tokenGenerator.createCustomToken(uid, developerClaims); + } + + /** + * Verifies a Firebase ID token (JWT). If the token is valid, the promise is + * fulfilled with the token's decoded claims; otherwise, the promise is + * rejected. + * + * If `checkRevoked` is set to true, first verifies whether the corresponding + * user is disabled. If yes, an `auth/user-disabled` error is thrown. If no, + * verifies if the session corresponding to the ID token was revoked. If the + * corresponding user's session was invalidated, an `auth/id-token-revoked` + * error is thrown. If not specified the check is not applied. + * + * See {@link https://firebase.google.com/docs/auth/admin/verify-id-tokens | Verify ID Tokens} + * for code samples and detailed documentation. + * + * @param idToken - The ID token to verify. + * @param checkRevoked - Whether to check if the ID token was revoked. + * This requires an extra request to the Firebase Auth backend to check + * the `tokensValidAfterTime` time for the corresponding user. + * When not specified, this additional check is not applied. + * + * @returns A promise fulfilled with the + * token's decoded claims if the ID token is valid; otherwise, a rejected + * promise. + */ + public verifyIdToken(idToken: string, checkRevoked = false): Promise { + const isEmulator = useEmulator(); + return this.idTokenVerifier.verifyJWT(idToken, isEmulator) + .then((decodedIdToken: DecodedIdToken) => { + // Whether to check if the token was revoked. + if (checkRevoked || isEmulator) { + return this.verifyDecodedJWTNotRevokedOrDisabled( + decodedIdToken, + AuthClientErrorCode.ID_TOKEN_REVOKED); + } + return decodedIdToken; + }); + } + + /** + * Gets the user data for the user corresponding to a given `uid`. + * + * See {@link https://firebase.google.com/docs/auth/admin/manage-users#retrieve_user_data | Retrieve user data} + * for code samples and detailed documentation. + * + * @param uid - The `uid` corresponding to the user whose data to fetch. + * + * @returns A promise fulfilled with the user + * data corresponding to the provided `uid`. + */ + public getUser(uid: string): Promise { + return this.authRequestHandler.getAccountInfoByUid(uid) + .then((response: any) => { + // Returns the user record populated with server response. + return new UserRecord(response.users[0]); + }); + } + + /** + * Gets the user data for the user corresponding to a given email. + * + * See {@link https://firebase.google.com/docs/auth/admin/manage-users#retrieve_user_data | Retrieve user data} + * for code samples and detailed documentation. + * + * @param email - The email corresponding to the user whose data to + * fetch. + * + * @returns A promise fulfilled with the user + * data corresponding to the provided email. + */ + public getUserByEmail(email: string): Promise { + return this.authRequestHandler.getAccountInfoByEmail(email) + .then((response: any) => { + // Returns the user record populated with server response. + return new UserRecord(response.users[0]); + }); + } + + /** + * Gets the user data for the user corresponding to a given phone number. The + * phone number has to conform to the E.164 specification. + * + * See {@link https://firebase.google.com/docs/auth/admin/manage-users#retrieve_user_data | Retrieve user data} + * for code samples and detailed documentation. + * + * @param phoneNumber - The phone number corresponding to the user whose + * data to fetch. + * + * @returns A promise fulfilled with the user + * data corresponding to the provided phone number. + */ + public getUserByPhoneNumber(phoneNumber: string): Promise { + return this.authRequestHandler.getAccountInfoByPhoneNumber(phoneNumber) + .then((response: any) => { + // Returns the user record populated with server response. + return new UserRecord(response.users[0]); + }); + } + + /** + * Gets the user data for the user corresponding to a given provider id. + * + * See {@link https://firebase.google.com/docs/auth/admin/manage-users#retrieve_user_data | 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. + * + * @returns 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. + * + * There are no ordering guarantees; in particular, the nth entry in the result list is not + * guaranteed to correspond to the nth entry in the input parameters list. + * + * Only a maximum of 100 identifiers may be supplied. If more than 100 identifiers are supplied, + * this method throws a FirebaseAuthError. + * + * @param identifiers - The identifiers used to indicate which user records should be returned. + * Must not have more than 100 entries. + * @returns A promise that resolves to the corresponding user records. + * @throws FirebaseAuthError If any of the identifiers are invalid or if more than 100 + * identifiers are specified. + */ + public getUsers(identifiers: UserIdentifier[]): Promise { + if (!validator.isArray(identifiers)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, '`identifiers` parameter must be an array'); + } + return this.authRequestHandler + .getAccountInfoByIdentifiers(identifiers) + .then((response: any) => { + /** + * Checks if the specified identifier is within the list of + * UserRecords. + */ + const isUserFound = ((id: UserIdentifier, userRecords: UserRecord[]): boolean => { + return !!userRecords.find((userRecord) => { + if (isUidIdentifier(id)) { + return id.uid === userRecord.uid; + } else if (isEmailIdentifier(id)) { + return id.email === userRecord.email; + } else if (isPhoneIdentifier(id)) { + return id.phoneNumber === userRecord.phoneNumber; + } else if (isProviderIdentifier(id)) { + const matchingUserInfo = userRecord.providerData.find((userInfo) => { + return id.providerId === userInfo.providerId; + }); + return !!matchingUserInfo && id.providerUid === matchingUserInfo.uid; + } else { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'Unhandled identifier type'); + } + }); + }); + + const users = response.users ? response.users.map((user: any) => new UserRecord(user)) : []; + const notFound = identifiers.filter((id) => !isUserFound(id, users)); + + return { users, notFound }; + }); + } + + /** + * Retrieves a list of users (single batch only) with a size of `maxResults` + * starting from the offset as specified by `pageToken`. This is used to + * retrieve all the users of a specified project in batches. + * + * See {@link https://firebase.google.com/docs/auth/admin/manage-users#list_all_users | List all users} + * for code samples and detailed documentation. + * + * @param maxResults - The page size, 1000 if undefined. This is also + * the maximum allowed limit. + * @param pageToken - The next page token. If not specified, returns + * users starting without any offset. + * @returns A promise that resolves with + * the current batch of downloaded users and the next page token. + */ + public listUsers(maxResults?: number, pageToken?: string): Promise { + return this.authRequestHandler.downloadAccount(maxResults, pageToken) + .then((response: any) => { + // List of users to return. + const users: UserRecord[] = []; + // Convert each user response to a UserRecord. + response.users.forEach((userResponse: any) => { + users.push(new UserRecord(userResponse)); + }); + // Return list of user records and the next page token if available. + const result = { + users, + pageToken: response.nextPageToken, + }; + // Delete result.pageToken if undefined. + if (typeof result.pageToken === 'undefined') { + delete result.pageToken; + } + return result; + }); + } + + /** + * Creates a new user. + * + * See {@link https://firebase.google.com/docs/auth/admin/manage-users#create_a_user | Create a user} + * for code samples and detailed documentation. + * + * @param properties - The properties to set on the + * new user record to be created. + * + * @returns A promise fulfilled with the user + * data corresponding to the newly created user. + */ + public createUser(properties: CreateRequest): Promise { + return this.authRequestHandler.createNewAccount(properties) + .then((uid) => { + // Return the corresponding user record. + return this.getUser(uid); + }) + .catch((error) => { + if (error.code === 'auth/user-not-found') { + // Something must have happened after creating the user and then retrieving it. + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'Unable to create the user record provided.'); + } + throw error; + }); + } + + /** + * Deletes an existing user. + * + * See {@link https://firebase.google.com/docs/auth/admin/manage-users#delete_a_user | Delete a user} + * for code samples and detailed documentation. + * + * @param uid - The `uid` corresponding to the user to delete. + * + * @returns An empty promise fulfilled once the user has been + * deleted. + */ + public deleteUser(uid: string): Promise { + return this.authRequestHandler.deleteAccount(uid) + .then(() => { + // Return nothing on success. + }); + } + + /** + * Deletes the users specified by the given uids. + * + * Deleting a non-existing user won't generate an error (i.e. this method + * is idempotent.) Non-existing users are considered to be successfully + * deleted, and are therefore counted in the + * `DeleteUsersResult.successCount` value. + * + * Only a maximum of 1000 identifiers may be supplied. If more than 1000 + * identifiers are supplied, this method throws a FirebaseAuthError. + * + * This API is currently rate limited at the server to 1 QPS. If you exceed + * this, you may get a quota exceeded error. Therefore, if you want to + * delete more than 1000 users, you may need to add a delay to ensure you + * don't go over this limit. + * + * @param uids - The `uids` corresponding to the users to delete. + * + * @returns A Promise that resolves to the total number of successful/failed + * deletions, as well as the array of errors that corresponds to the + * failed deletions. + */ + public deleteUsers(uids: string[]): Promise { + if (!validator.isArray(uids)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, '`uids` parameter must be an array'); + } + return this.authRequestHandler.deleteAccounts(uids, /*force=*/true) + .then((batchDeleteAccountsResponse) => { + const result: DeleteUsersResult = { + failureCount: 0, + successCount: uids.length, + errors: [], + }; + + if (!validator.isNonEmptyArray(batchDeleteAccountsResponse.errors)) { + return result; + } + + result.failureCount = batchDeleteAccountsResponse.errors.length; + result.successCount = uids.length - batchDeleteAccountsResponse.errors.length; + result.errors = batchDeleteAccountsResponse.errors.map((batchDeleteErrorInfo) => { + if (batchDeleteErrorInfo.index === undefined) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'Corrupt BatchDeleteAccountsResponse detected'); + } + + const errMsgToError = (msg?: string): FirebaseAuthError => { + // We unconditionally set force=true, so the 'NOT_DISABLED' error + // should not be possible. + const code = msg && msg.startsWith('NOT_DISABLED') ? + AuthClientErrorCode.USER_NOT_DISABLED : AuthClientErrorCode.INTERNAL_ERROR; + return new FirebaseAuthError(code, batchDeleteErrorInfo.message); + }; + + return { + index: batchDeleteErrorInfo.index, + error: errMsgToError(batchDeleteErrorInfo.message), + }; + }); + + return result; + }); + } + + /** + * Updates an existing user. + * + * See {@link https://firebase.google.com/docs/auth/admin/manage-users#update_a_user | Update a user} + * for code samples and detailed documentation. + * + * @param uid - The `uid` corresponding to the user to update. + * @param properties - The properties to update on + * the provided user. + * + * @returns A promise fulfilled with the + * updated user data. + */ + 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. + return this.getUser(existingUid); + }); + } + + /** + * Sets additional developer claims on an existing user identified by the + * provided `uid`, typically used to define user roles and levels of + * access. These claims should propagate to all devices where the user is + * already signed in (after token expiration or when token refresh is forced) + * and the next time the user signs in. If a reserved OIDC claim name + * is used (sub, iat, iss, etc), an error is thrown. They are set on the + * authenticated user's ID token JWT. + * + * See {@link https://firebase.google.com/docs/auth/admin/custom-claims | + * Defining user roles and access levels} + * for code samples and detailed documentation. + * + * @param uid - The `uid` of the user to edit. + * @param customUserClaims - The developer claims to set. If null is + * passed, existing custom claims are deleted. Passing a custom claims payload + * larger than 1000 bytes will throw an error. Custom claims are added to the + * user's ID token which is transmitted on every authenticated request. + * For profile non-access related user attributes, use database or other + * separate storage systems. + * @returns A promise that resolves when the operation completes + * successfully. + */ + public setCustomUserClaims(uid: string, customUserClaims: object | null): Promise { + return this.authRequestHandler.setCustomUserClaims(uid, customUserClaims) + .then(() => { + // Return nothing on success. + }); + } + + /** + * Revokes all refresh tokens for an existing user. + * + * This API will update the user's {@link UserRecord.tokensValidAfterTime} to + * the current UTC. It is important that the server on which this is called has + * its clock set correctly and synchronized. + * + * While this will revoke all sessions for a specified user and disable any + * 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 BaseAuth.verifyIdToken} + * where `checkRevoked` is set to true. + * + * @param uid - The `uid` corresponding to the user whose refresh tokens + * are to be revoked. + * + * @returns An empty promise fulfilled once the user's refresh + * tokens have been revoked. + */ + public revokeRefreshTokens(uid: string): Promise { + return this.authRequestHandler.revokeRefreshTokens(uid) + .then(() => { + // Return nothing on success. + }); + } + + /** + * 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 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. + * + * @param users - The list of user records to import to Firebase Auth. + * @param options - The user import options, required when the users provided include + * password credentials. + * @returns A promise that resolves when + * the operation completes with the result of the import. This includes the + * number of successful imports, the number of failed imports and their + * corresponding errors. + */ + public importUsers( + users: UserImportRecord[], options?: UserImportOptions): Promise { + return this.authRequestHandler.uploadAccount(users, options); + } + + /** + * Creates a new Firebase session cookie with the specified options. The created + * JWT string can be set as a server-side session cookie with a custom cookie + * policy, and be used for session management. The session cookie JWT will have + * the same payload claims as the provided ID token. + * + * See {@link https://firebase.google.com/docs/auth/admin/manage-cookies | Manage Session Cookies} + * for code samples and detailed documentation. + * + * @param idToken - The Firebase ID token to exchange for a session + * cookie. + * @param sessionCookieOptions - The session + * cookie options which includes custom session duration. + * + * @returns A promise that resolves on success with the + * created session cookie. + */ + public createSessionCookie( + idToken: string, sessionCookieOptions: SessionCookieOptions): Promise { + // Return rejected promise if expiresIn is not available. + if (!validator.isNonNullObject(sessionCookieOptions) || + !validator.isNumber(sessionCookieOptions.expiresIn)) { + return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_SESSION_COOKIE_DURATION)); + } + return this.authRequestHandler.createSessionCookie( + idToken, sessionCookieOptions.expiresIn); + } + + /** + * Verifies a Firebase session cookie. Returns a Promise with the cookie claims. + * Rejects the promise if the cookie could not be verified. + * + * If `checkRevoked` is set to true, first verifies whether the corresponding + * user is disabled: If yes, an `auth/user-disabled` error is thrown. If no, + * verifies if the session corresponding to the session cookie was revoked. + * If the corresponding user's session was invalidated, an + * `auth/session-cookie-revoked` error is thrown. If not specified the check + * is not performed. + * + * See {@link https://firebase.google.com/docs/auth/admin/manage-cookies#verify_session_cookie_and_check_permissions | + * Verify Session Cookies} + * for code samples and detailed documentation + * + * @param sessionCookie - The session cookie to verify. + * @param checkForRevocation - Whether to check if the session cookie was + * revoked. This requires an extra request to the Firebase Auth backend to + * check the `tokensValidAfterTime` time for the corresponding user. + * When not specified, this additional check is not performed. + * + * @returns A promise fulfilled with the + * session cookie's decoded claims if the session cookie is valid; otherwise, + * a rejected promise. + */ + public verifySessionCookie( + sessionCookie: string, checkRevoked = false): Promise { + const isEmulator = useEmulator(); + return this.sessionCookieVerifier.verifyJWT(sessionCookie, isEmulator) + .then((decodedIdToken: DecodedIdToken) => { + // Whether to check if the token was revoked. + if (checkRevoked || isEmulator) { + return this.verifyDecodedJWTNotRevokedOrDisabled( + decodedIdToken, + AuthClientErrorCode.SESSION_COOKIE_REVOKED); + } + return decodedIdToken; + }); + } + + /** + * 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 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. + * + * @example + * ```javascript + * var actionCodeSettings = { + * url: 'https://www.example.com/?email=user@example.com', + * iOS: { + * bundleId: 'com.example.ios' + * }, + * android: { + * packageName: 'com.example.android', + * installApp: true, + * minimumVersion: '12' + * }, + * handleCodeInApp: true, + * dynamicLinkDomain: 'custom.page.link' + * }; + * admin.auth() + * .generatePasswordResetLink('user@example.com', actionCodeSettings) + * .then(function(link) { + * // The link was successfully generated. + * }) + * .catch(function(error) { + * // Some error occurred, you can inspect the code: error.code + * }); + * ``` + * + * @param email - The email address of the user whose password is to be + * reset. + * @param actionCodeSettings - The action + * code settings. If specified, the state/continue URL is set as the + * "continueUrl" parameter in the password reset link. The default password + * reset landing page will use this to display a link to go back to the app + * if it is installed. + * If the actionCodeSettings is not specified, no URL is appended to the + * action URL. + * The state URL provided must belong to a domain that is whitelisted by the + * developer in the console. Otherwise an error is thrown. + * Mobile app redirects are only applicable if the developer configures + * and accepts the Firebase Dynamic Links terms of service. + * The Android package name and iOS bundle ID are respected only if they + * are configured in the same Firebase Auth project. + * @returns A promise that resolves with the generated link. + */ + public generatePasswordResetLink(email: string, actionCodeSettings?: ActionCodeSettings): Promise { + return this.authRequestHandler.getEmailActionLink('PASSWORD_RESET', email, actionCodeSettings); + } + + /** + * Generates the out of band email action link to verify the user's ownership + * of the specified email. The {@link ActionCodeSettings} object provided + * as an argument to this method defines whether the link is to be handled by a + * mobile app or browser along with additional state information to be passed in + * the deep link, etc. + * + * @example + * ```javascript + * var actionCodeSettings = { + * url: 'https://www.example.com/cart?email=user@example.com&cartId=123', + * iOS: { + * bundleId: 'com.example.ios' + * }, + * android: { + * packageName: 'com.example.android', + * installApp: true, + * minimumVersion: '12' + * }, + * handleCodeInApp: true, + * dynamicLinkDomain: 'custom.page.link' + * }; + * admin.auth() + * .generateEmailVerificationLink('user@example.com', actionCodeSettings) + * .then(function(link) { + * // The link was successfully generated. + * }) + * .catch(function(error) { + * // Some error occurred, you can inspect the code: error.code + * }); + * ``` + * + * @param email - The email account to verify. + * @param actionCodeSettings - The action + * code settings. If specified, the state/continue URL is set as the + * "continueUrl" parameter in the email verification link. The default email + * verification landing page will use this to display a link to go back to + * the app if it is installed. + * If the actionCodeSettings is not specified, no URL is appended to the + * action URL. + * The state URL provided must belong to a domain that is whitelisted by the + * developer in the console. Otherwise an error is thrown. + * Mobile app redirects are only applicable if the developer configures + * and accepts the Firebase Dynamic Links terms of service. + * The Android package name and iOS bundle ID are respected only if they + * are configured in the same Firebase Auth project. + * @returns A promise that resolves with the generated link. + */ + public generateEmailVerificationLink(email: string, actionCodeSettings?: ActionCodeSettings): Promise { + return this.authRequestHandler.getEmailActionLink('VERIFY_EMAIL', email, actionCodeSettings); + } + + /** + * Generates an out-of-band email action link to verify the user's ownership + * of the specified email. The {@link ActionCodeSettings} object provided + * as an argument to this method defines whether the link is to be handled by a + * mobile app or browser along with additional state information to be passed in + * the deep link, etc. + * + * @param email - The current email account. + * @param newEmail - The email address the account is being updated to. + * @param actionCodeSettings - The action + * code settings. If specified, the state/continue URL is set as the + * "continueUrl" parameter in the email verification link. The default email + * verification landing page will use this to display a link to go back to + * the app if it is installed. + * If the actionCodeSettings is not specified, no URL is appended to the + * action URL. + * The state URL provided must belong to a domain that is authorized + * in the console, or an error will be thrown. + * Mobile app redirects are only applicable if the developer configures + * and accepts the Firebase Dynamic Links terms of service. + * The Android package name and iOS bundle ID are respected only if they + * are configured in the same Firebase Auth project. + * @returns A promise that resolves with the generated link. + */ + public generateVerifyAndChangeEmailLink(email: string, newEmail: string, + actionCodeSettings?: ActionCodeSettings): Promise { + return this.authRequestHandler.getEmailActionLink('VERIFY_AND_CHANGE_EMAIL', email, actionCodeSettings, newEmail); + } + + /** + * Generates the out of band email action link to verify the user's ownership + * of the specified email. The {@link ActionCodeSettings} object provided + * 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. + * + * @example + * ```javascript + * var actionCodeSettings = { + * url: 'https://www.example.com/cart?email=user@example.com&cartId=123', + * iOS: { + * bundleId: 'com.example.ios' + * }, + * android: { + * packageName: 'com.example.android', + * installApp: true, + * minimumVersion: '12' + * }, + * handleCodeInApp: true, + * dynamicLinkDomain: 'custom.page.link' + * }; + * admin.auth() + * .generateEmailVerificationLink('user@example.com', actionCodeSettings) + * .then(function(link) { + * // The link was successfully generated. + * }) + * .catch(function(error) { + * // Some error occurred, you can inspect the code: error.code + * }); + * ``` + * + * @param email - The email account to verify. + * @param actionCodeSettings - The action + * code settings. If specified, the state/continue URL is set as the + * "continueUrl" parameter in the email verification link. The default email + * verification landing page will use this to display a link to go back to + * the app if it is installed. + * If the actionCodeSettings is not specified, no URL is appended to the + * action URL. + * The state URL provided must belong to a domain that is whitelisted by the + * developer in the console. Otherwise an error is thrown. + * Mobile app redirects are only applicable if the developer configures + * and accepts the Firebase Dynamic Links terms of service. + * The Android package name and iOS bundle ID are respected only if they + * are configured in the same Firebase Auth project. + * @returns A promise that resolves with the generated link. + */ + public generateSignInWithEmailLink(email: string, actionCodeSettings: ActionCodeSettings): Promise { + return this.authRequestHandler.getEmailActionLink('EMAIL_SIGNIN', email, actionCodeSettings); + } + + /** + * Returns the list of existing provider configurations matching the filter + * provided. At most, 100 provider configs can be listed at a time. + * + * SAML and OIDC provider support requires Google Cloud's Identity Platform + * (GCIP). To learn more about GCIP, including pricing and features, + * see the {@link https://cloud.google.com/identity-platform | GCIP documentation}. + * + * @param options - The provider config filter to apply. + * @returns A promise that resolves with the list of provider configs meeting the + * filter requirements. + */ + public listProviderConfigs(options: AuthProviderConfigFilter): Promise { + const processResponse = (response: any, providerConfigs: AuthProviderConfig[]): ListProviderConfigResults => { + // Return list of provider configuration and the next page token if available. + const result: ListProviderConfigResults = { + providerConfigs, + }; + // Delete result.pageToken if undefined. + if (Object.prototype.hasOwnProperty.call(response, 'nextPageToken')) { + result.pageToken = response.nextPageToken; + } + return result; + }; + if (options && options.type === 'oidc') { + return this.authRequestHandler.listOAuthIdpConfigs(options.maxResults, options.pageToken) + .then((response: any) => { + // List of provider configurations to return. + const providerConfigs: OIDCConfig[] = []; + // Convert each provider config response to a OIDCConfig. + response.oauthIdpConfigs.forEach((configResponse: any) => { + providerConfigs.push(new OIDCConfig(configResponse)); + }); + // Return list of provider configuration and the next page token if available. + return processResponse(response, providerConfigs); + }); + } else if (options && options.type === 'saml') { + return this.authRequestHandler.listInboundSamlConfigs(options.maxResults, options.pageToken) + .then((response: any) => { + // List of provider configurations to return. + const providerConfigs: SAMLConfig[] = []; + // Convert each provider config response to a SAMLConfig. + response.inboundSamlConfigs.forEach((configResponse: any) => { + providerConfigs.push(new SAMLConfig(configResponse)); + }); + // Return list of provider configuration and the next page token if available. + return processResponse(response, providerConfigs); + }); + } + return Promise.reject( + new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"AuthProviderConfigFilter.type" must be either "saml" or "oidc"')); + } + + /** + * Looks up an Auth provider configuration by the provided ID. + * Returns a promise that resolves with the provider configuration + * corresponding to the provider ID specified. If the specified ID does not + * exist, an `auth/configuration-not-found` error is thrown. + * + * SAML and OIDC provider support requires Google Cloud's Identity Platform + * (GCIP). To learn more about GCIP, including pricing and features, + * see the {@link https://cloud.google.com/identity-platform | GCIP documentation}. + * + * @param providerId - The provider ID corresponding to the provider + * config to return. + * @returns A promise that resolves + * with the configuration corresponding to the provided ID. + */ + public getProviderConfig(providerId: string): Promise { + if (OIDCConfig.isProviderId(providerId)) { + return this.authRequestHandler.getOAuthIdpConfig(providerId) + .then((response: OIDCConfigServerResponse) => { + return new OIDCConfig(response); + }); + } else if (SAMLConfig.isProviderId(providerId)) { + return this.authRequestHandler.getInboundSamlConfig(providerId) + .then((response: SAMLConfigServerResponse) => { + return new SAMLConfig(response); + }); + } + return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID)); + } + + /** + * Deletes the provider configuration corresponding to the provider ID passed. + * If the specified ID does not exist, an `auth/configuration-not-found` error + * is thrown. + * + * SAML and OIDC provider support requires Google Cloud's Identity Platform + * (GCIP). To learn more about GCIP, including pricing and features, + * see the {@link https://cloud.google.com/identity-platform | GCIP documentation}. + * + * @param providerId - The provider ID corresponding to the provider + * config to delete. + * @returns A promise that resolves on completion. + */ + public deleteProviderConfig(providerId: string): Promise { + if (OIDCConfig.isProviderId(providerId)) { + return this.authRequestHandler.deleteOAuthIdpConfig(providerId); + } else if (SAMLConfig.isProviderId(providerId)) { + return this.authRequestHandler.deleteInboundSamlConfig(providerId); + } + return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID)); + } + + /** + * Returns a promise that resolves with the updated `AuthProviderConfig` + * corresponding to the provider ID specified. + * If the specified ID does not exist, an `auth/configuration-not-found` error + * is thrown. + * + * SAML and OIDC provider support requires Google Cloud's Identity Platform + * (GCIP). To learn more about GCIP, including pricing and features, + * see the {@link https://cloud.google.com/identity-platform | GCIP documentation}. + * + * @param providerId - The provider ID corresponding to the provider + * config to update. + * @param updatedConfig - The updated configuration. + * @returns A promise that resolves with the updated provider configuration. + */ + public updateProviderConfig( + providerId: string, updatedConfig: UpdateAuthProviderRequest): Promise { + if (!validator.isNonNullObject(updatedConfig)) { + return Promise.reject(new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + 'Request is missing "UpdateAuthProviderRequest" configuration.', + )); + } + if (OIDCConfig.isProviderId(providerId)) { + return this.authRequestHandler.updateOAuthIdpConfig(providerId, updatedConfig) + .then((response) => { + return new OIDCConfig(response); + }); + } else if (SAMLConfig.isProviderId(providerId)) { + return this.authRequestHandler.updateInboundSamlConfig(providerId, updatedConfig) + .then((response) => { + return new SAMLConfig(response); + }); + } + return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID)); + } + + /** + * Returns a promise that resolves with the newly created `AuthProviderConfig` + * when the new provider configuration is created. + * + * SAML and OIDC provider support requires Google Cloud's Identity Platform + * (GCIP). To learn more about GCIP, including pricing and features, + * see the {@link https://cloud.google.com/identity-platform | GCIP documentation}. + * + * @param config - The provider configuration to create. + * @returns A promise that resolves with the created provider configuration. + */ + public createProviderConfig(config: AuthProviderConfig): Promise { + if (!validator.isNonNullObject(config)) { + return Promise.reject(new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + 'Request is missing "AuthProviderConfig" configuration.', + )); + } + if (OIDCConfig.isProviderId(config.providerId)) { + return this.authRequestHandler.createOAuthIdpConfig(config as OIDCAuthProviderConfig) + .then((response) => { + return new OIDCConfig(response); + }); + } else if (SAMLConfig.isProviderId(config.providerId)) { + return this.authRequestHandler.createInboundSamlConfig(config as SAMLAuthProviderConfig) + .then((response) => { + return new SAMLConfig(response); + }); + } + return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID)); + } + + /** @alpha */ + // eslint-disable-next-line @typescript-eslint/naming-convention + public _verifyAuthBlockingToken( + token: string, + audience?: string + ): Promise { + const isEmulator = useEmulator(); + return this.authBlockingTokenVerifier._verifyAuthBlockingToken(token, isEmulator, audience) + .then((decodedAuthBlockingToken: DecodedAuthBlockingToken) => { + return decodedAuthBlockingToken; + }); + } + + /** + * Verifies the decoded Firebase issued JWT is not revoked or disabled. Returns a promise that + * resolves with the decoded claims on success. Rejects the promise with revocation error if revoked + * or user disabled. + * + * @param decodedIdToken - The JWT's decoded claims. + * @param revocationErrorInfo - The revocation error info to throw on revocation + * detection. + * @returns A promise that will be fulfilled after a successful verification. + */ + private verifyDecodedJWTNotRevokedOrDisabled( + decodedIdToken: DecodedIdToken, revocationErrorInfo: ErrorInfo): Promise { + // Get tokens valid after time for the corresponding user. + return this.getUser(decodedIdToken.sub) + .then((user: UserRecord) => { + if (user.disabled) { + throw new FirebaseAuthError( + AuthClientErrorCode.USER_DISABLED, + 'The user record is disabled.'); + } + // If no tokens valid after time available, token is not revoked. + if (user.tokensValidAfterTime) { + // Get the ID token authentication time and convert to milliseconds UTC. + const authTimeUtc = decodedIdToken.auth_time * 1000; + // Get user tokens valid after time in milliseconds UTC. + const validSinceUtc = new Date(user.tokensValidAfterTime).getTime(); + // Check if authentication time is older than valid since time. + if (authTimeUtc < validSinceUtc) { + throw new FirebaseAuthError(revocationErrorInfo); + } + } + // All checks above passed. Return the decoded token. + return decodedIdToken; + }); + } +} diff --git a/src/auth/credential.ts b/src/auth/credential.ts deleted file mode 100644 index b526c338c1..0000000000 --- a/src/auth/credential.ts +++ /dev/null @@ -1,393 +0,0 @@ -/*! - * 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 * as jwt from 'jsonwebtoken'; -import * as forge from 'node-forge'; - -// Use untyped import syntax for Node built-ins -import fs = require('fs'); -import os = require('os'); -import http = require('http'); -import path = require('path'); -import https = require('https'); - -import {AppErrorCodes, FirebaseAppError} from '../utils/error'; - - -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'; -const GOOGLE_AUTH_TOKEN_PORT = 443; - -// NOTE: the Google Metadata Service uses HTTP over a vlan -const GOOGLE_METADATA_SERVICE_HOST = 'metadata.google.internal'; -const GOOGLE_METADATA_SERVICE_PATH = '/computeMetadata/v1beta1/instance/service-accounts/default/token'; - -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_PORT = 443; -const REFRESH_TOKEN_PATH = '/oauth2/v4/token'; - -const ONE_HOUR_IN_SECONDS = 60 * 60; -const JWT_ALGORITHM = 'RS256'; - - -function copyAttr(to: object, from: object, key: string, alt: string) { - const tmp = from[key] || from[alt]; - if (typeof tmp !== 'undefined') { - to[key] = tmp; - } -} - -export class RefreshToken { - public clientId: string; - public clientSecret: string; - public refreshToken: string; - public type: string; - - /* - * Tries to load a RefreshToken from a path. If the path is not present, returns null. - * Throws if data at the path is invalid. - */ - public static fromPath(filePath: string): RefreshToken { - let jsonString: string; - - try { - jsonString = fs.readFileSync(filePath, 'utf8'); - } catch (ignored) { - // Ignore errors if the file is not present, as this is sometimes an expected condition - return null; - } - - try { - return new RefreshToken(JSON.parse(jsonString)); - } 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 (typeof this.clientId !== 'string' || !this.clientId) { - errorMessage = 'Refresh token must contain a "client_id" property.'; - } else if (typeof this.clientSecret !== 'string' || !this.clientSecret) { - errorMessage = 'Refresh token must contain a "client_secret" property.'; - } else if (typeof this.refreshToken !== 'string' || !this.refreshToken) { - errorMessage = 'Refresh token must contain a "refresh_token" property.'; - } else if (typeof this.type !== 'string' || !this.type) { - errorMessage = 'Refresh token must contain a "type" property.'; - } - - if (typeof errorMessage !== 'undefined') { - throw new FirebaseAppError(AppErrorCodes.INVALID_CREDENTIAL, errorMessage); - } - } -} - -/** - * A struct containing the properties necessary to use service-account JSON credentials. - */ -export class Certificate { - public projectId: string; - public privateKey: string; - public clientEmail: string; - - public static fromPath(filePath: string): Certificate { - // Node bug encountered in v6.x. fs.readFileSync hangs when path is a 0 or 1. - if (typeof filePath !== 'string') { - throw new FirebaseAppError( - AppErrorCodes.INVALID_CREDENTIAL, - 'Failed to parse certificate key file: TypeError: path must be a string', - ); - } - try { - return new Certificate(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 certificate key file: ' + error, - ); - } - } - - constructor(json: object) { - if (typeof json !== 'object' || json === null) { - throw new FirebaseAppError( - AppErrorCodes.INVALID_CREDENTIAL, - 'Certificate object 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 (typeof this.privateKey !== 'string' || !this.privateKey) { - errorMessage = 'Certificate object must contain a string "private_key" property.'; - } else if (typeof this.clientEmail !== 'string' || !this.clientEmail) { - errorMessage = 'Certificate object must contain a string "client_email" property.'; - } - - if (typeof errorMessage !== 'undefined') { - throw new FirebaseAppError(AppErrorCodes.INVALID_CREDENTIAL, errorMessage); - } - - try { - forge.pki.privateKeyFromPem(this.privateKey); - } catch (error) { - throw new FirebaseAppError( - AppErrorCodes.INVALID_CREDENTIAL, - 'Failed to parse private key: ' + error); - } - } -} - -/** - * 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 */ -} - -/** - * A wrapper around the http and https request libraries to simplify & promisify JSON requests. - * TODO(inlined): Create a type for "transit". - */ -function requestAccessToken(transit, options: object, data?: any): Promise { - return new Promise((resolve, reject) => { - const req = transit.request(options, (res) => { - const buffers: Buffer[] = []; - res.on('data', (buffer) => buffers.push(buffer)); - res.on('end', () => { - try { - const json = JSON.parse(Buffer.concat(buffers).toString()); - if (json.error) { - let errorMessage = 'Error fetching access token: ' + json.error; - if (json.error_description) { - errorMessage += ' (' + json.error_description + ')'; - } - reject(new FirebaseAppError(AppErrorCodes.INVALID_CREDENTIAL, errorMessage)); - } else if (!json.access_token || !json.expires_in) { - reject(new FirebaseAppError( - AppErrorCodes.INVALID_CREDENTIAL, - `Unexpected response while fetching access token: ${ JSON.stringify(json) }`, - )); - } else { - resolve(json); - } - } catch (err) { - reject(new FirebaseAppError( - AppErrorCodes.INVALID_CREDENTIAL, - `Failed to parse access token response: ${err.toString()}`, - )); - } - }); - }); - req.on('error', reject); - if (data) { - req.write(data); - } - req.end(); - }); -} - -/** - * Implementation of Credential that uses a service account certificate. - */ -export class CertCredential implements Credential { - private certificate_: Certificate; - - constructor(serviceAccountPathOrObject: string | object) { - this.certificate_ = (typeof serviceAccountPathOrObject === 'string') ? - Certificate.fromPath(serviceAccountPathOrObject) : new Certificate(serviceAccountPathOrObject); - } - - public getAccessToken(): Promise { - const token = this.createAuthJwt_(); - const postData = 'grant_type=urn%3Aietf%3Aparams%3Aoauth%3A' + - 'grant-type%3Ajwt-bearer&assertion=' + - token; - const options = { - method: 'POST', - host: GOOGLE_AUTH_TOKEN_HOST, - port: GOOGLE_AUTH_TOKEN_PORT, - path: GOOGLE_AUTH_TOKEN_PATH, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Content-Length': postData.length, - }, - }; - return requestAccessToken(https, options, postData); - } - - public getCertificate(): Certificate { - return this.certificate_; - } - - 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(' '), - }; - - // This method is actually synchronous so we can capture and return the buffer. - return jwt.sign(claims, this.certificate_.privateKey, { - audience: GOOGLE_TOKEN_AUDIENCE, - expiresIn: ONE_HOUR_IN_SECONDS, - issuer: this.certificate_.clientEmail, - algorithm: JWT_ALGORITHM, - }); - } -} - -/** - * Interface for things that generate access tokens. - */ -export interface Credential { - getAccessToken(): Promise; - getCertificate(): Certificate; -} - -/** - * Implementation of Credential that gets access tokens from refresh tokens. - */ -export class RefreshTokenCredential implements Credential { - private refreshToken_: RefreshToken; - - constructor(refreshTokenPathOrObject: string | object) { - this.refreshToken_ = (typeof refreshTokenPathOrObject === 'string') ? - RefreshToken.fromPath(refreshTokenPathOrObject) : new RefreshToken(refreshTokenPathOrObject); - } - - 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 options = { - method: 'POST', - host: REFRESH_TOKEN_HOST, - port: REFRESH_TOKEN_PORT, - path: REFRESH_TOKEN_PATH, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Content-Length': postData.length, - }, - }; - return requestAccessToken(https, options, postData); - } - - public getCertificate(): Certificate { - return null; - } -} - - -/** - * 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 MetadataServiceCredential implements Credential { - public getAccessToken(): Promise { - const options = { - method: 'GET', - host: GOOGLE_METADATA_SERVICE_HOST, - path: GOOGLE_METADATA_SERVICE_PATH, - headers: { - 'Content-Length': 0, - }, - }; - return requestAccessToken(http, options); - } - - public getCertificate(): Certificate { - return null; - } -} - - -/** - * ApplicationDefaultCredential implements the process for loading credentials as - * described in https://developers.google.com/identity/protocols/application-default-credentials - */ -export class ApplicationDefaultCredential implements Credential { - private credential_: Credential; - - constructor() { - if (process.env.GOOGLE_APPLICATION_CREDENTIALS) { - const serviceAccount = Certificate.fromPath(process.env.GOOGLE_APPLICATION_CREDENTIALS); - this.credential_ = new CertCredential(serviceAccount); - return; - } - - // It is OK to not have this file. If it is present, it must be valid. - const refreshToken = RefreshToken.fromPath(GCLOUD_CREDENTIAL_PATH); - if (refreshToken) { - this.credential_ = new RefreshTokenCredential(refreshToken); - return; - } - - this.credential_ = new MetadataServiceCredential(); - } - - public getAccessToken(): Promise { - return this.credential_.getAccessToken(); - } - - public getCertificate(): Certificate { - return this.credential_.getCertificate(); - } - - // Used in testing to verify we are delegating to the correct implementation. - public getCredential(): Credential { - return this.credential_; - } -} diff --git a/src/auth/identifier.ts b/src/auth/identifier.ts new file mode 100644 index 0000000000..8bae483d1d --- /dev/null +++ b/src/auth/identifier.ts @@ -0,0 +1,80 @@ +/*! + * 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. + */ + +/** + * Used for looking up an account by uid. + * + * See {@link BaseAuth.getUsers}. + */ +export interface UidIdentifier { + uid: string; +} + +/** + * Used for looking up an account by email. + * + * See {@link BaseAuth.getUsers}. + */ +export interface EmailIdentifier { + email: string; +} + +/** + * Used for looking up an account by phone number. + * + * See {@link BaseAuth.getUsers}. + */ +export interface PhoneIdentifier { + phoneNumber: string; +} + +/** + * Used for looking up an account by federated provider. + * + * See {@link BaseAuth.getUsers}. + */ +export interface ProviderIdentifier { + providerId: string; + providerUid: string; +} + +/** + * Identifies a user to be looked up. + */ +export type UserIdentifier = + UidIdentifier | EmailIdentifier | PhoneIdentifier | ProviderIdentifier; + +/* + * User defined type guards. See + * https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards + */ + +export function isUidIdentifier(id: UserIdentifier): id is UidIdentifier { + return (id as UidIdentifier).uid !== undefined; +} + +export function isEmailIdentifier(id: UserIdentifier): id is EmailIdentifier { + return (id as EmailIdentifier).email !== undefined; +} + +export function isPhoneIdentifier(id: UserIdentifier): id is PhoneIdentifier { + return (id as PhoneIdentifier).phoneNumber !== undefined; +} + +export function isProviderIdentifier(id: ProviderIdentifier): id is ProviderIdentifier { + const pid = id as ProviderIdentifier; + return pid.providerId !== undefined && pid.providerUid !== undefined; +} diff --git a/src/auth/index.ts b/src/auth/index.ts new file mode 100644 index 0000000000..f350b28837 --- /dev/null +++ b/src/auth/index.ts @@ -0,0 +1,171 @@ +/*! + * 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. + */ + +/** + * Firebase Authentication. + * + * @packageDocumentation + */ + +import { App, getApp } from '../app/index'; +import { FirebaseApp } from '../app/firebase-app'; +import { Auth } from './auth'; + +/** + * Gets the {@link Auth} service for the default app or a + * given app. + * + * `getAuth()` can be called with no arguments to access the default app's + * {@link Auth} service or as `getAuth(app)` to access the + * {@link Auth} service associated with a specific app. + * + * @example + * ```javascript + * // Get the Auth service for the default app + * const defaultAuth = getAuth(); + * ``` + * + * @example + * ```javascript + * // Get the Auth service for a given app + * const otherAuth = getAuth(otherApp); + * ``` + * + */ +export function getAuth(app?: App): Auth { + if (typeof app === 'undefined') { + app = getApp(); + } + + const firebaseApp: FirebaseApp = app as FirebaseApp; + return firebaseApp.getOrInitService('auth', (app) => new Auth(app)); +} + +export { ActionCodeSettings } from './action-code-settings-builder'; + +export { + Auth, +} from './auth'; + +export { + AllowByDefault, + AllowByDefaultWrap, + AllowlistOnly, + AllowlistOnlyWrap, + AuthFactorType, + AuthProviderConfig, + AuthProviderConfigFilter, + BaseAuthProviderConfig, + BaseCreateMultiFactorInfoRequest, + BaseUpdateMultiFactorInfoRequest, + CreateMultiFactorInfoRequest, + CreatePhoneMultiFactorInfoRequest, + CreateRequest, + EmailSignInProviderConfig, + ListProviderConfigResults, + MultiFactorConfig, + MultiFactorConfigState, + MultiFactorCreateSettings, + MultiFactorUpdateSettings, + MultiFactorProviderConfig, + OAuthResponseType, + OIDCAuthProviderConfig, + OIDCUpdateAuthProviderRequest, + RecaptchaAction, + RecaptchaConfig, + RecaptchaKey, + RecaptchaKeyClientType, + RecaptchaManagedRule, + RecaptchaProviderEnforcementState, + SAMLAuthProviderConfig, + SAMLUpdateAuthProviderRequest, + SmsRegionConfig, + UserProvider, + UpdateAuthProviderRequest, + UpdateMultiFactorInfoRequest, + UpdatePhoneMultiFactorInfoRequest, + UpdateRequest, + TotpMultiFactorProviderConfig, + PasswordPolicyConfig, + PasswordPolicyEnforcementState, + CustomStrengthOptionsConfig, + EmailPrivacyConfig, +} from './auth-config'; + +export { + BaseAuth, + DeleteUsersResult, + GetUsersResult, + ListUsersResult, + SessionCookieOptions, +} from './base-auth'; + +export { + EmailIdentifier, + PhoneIdentifier, + ProviderIdentifier, + UidIdentifier, + UserIdentifier, +} from './identifier'; + +export { + CreateTenantRequest, + Tenant, + UpdateTenantRequest, +} from './tenant'; + +export { + ListTenantsResult, + TenantAwareAuth, + TenantManager, +} from './tenant-manager'; + +export { + UpdateProjectConfigRequest, + ProjectConfig, +} from './project-config'; + +export { + ProjectConfigManager, +} from './project-config-manager'; + +export { + DecodedIdToken, + DecodedAuthBlockingToken +} from './token-verifier'; + +export { + HashAlgorithmType, + UserImportOptions, + UserImportRecord, + UserImportResult, + UserMetadataRequest, + UserProviderRequest, +} from './user-import-builder'; + +export { + MultiFactorInfo, + MultiFactorSettings, + PhoneMultiFactorInfo, + UserInfo, + UserMetadata, + UserRecord, +} from './user-record'; + +export { + FirebaseAuthError, + AuthClientErrorCode, +} from '../utils/error'; diff --git a/src/auth/project-config-manager.ts b/src/auth/project-config-manager.ts new file mode 100644 index 0000000000..847aa7d982 --- /dev/null +++ b/src/auth/project-config-manager.ts @@ -0,0 +1,63 @@ +/*! + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { App } from '../app'; +import { ProjectConfig, ProjectConfigServerResponse, UpdateProjectConfigRequest } from './project-config'; +import { + AuthRequestHandler, +} from './auth-api-request'; + +/** + * Manages (gets and updates) the current project config. + */ +export class ProjectConfigManager { + private readonly authRequestHandler: AuthRequestHandler; + /** + * Initializes a ProjectConfigManager instance for a specified FirebaseApp. + * + * @param app - The app for this ProjectConfigManager instance. + * + * @constructor + * @internal + */ + constructor(app: App) { + this.authRequestHandler = new AuthRequestHandler(app); + } + + /** + * Get the project configuration. + * + * @returns A promise fulfilled with the project configuration. + */ + public getProjectConfig(): Promise { + return this.authRequestHandler.getProjectConfig() + .then((response: ProjectConfigServerResponse) => { + return new ProjectConfig(response); + }) + } + /** + * Updates an existing project configuration. + * + * @param projectConfigOptions - The properties to update on the project. + * + * @returns A promise fulfilled with the updated project config. + */ + public updateProjectConfig(projectConfigOptions: UpdateProjectConfigRequest): Promise { + return this.authRequestHandler.updateProjectConfig(projectConfigOptions) + .then((response: ProjectConfigServerResponse) => { + return new ProjectConfig(response); + }) + } +} diff --git a/src/auth/project-config.ts b/src/auth/project-config.ts new file mode 100644 index 0000000000..250d6549ac --- /dev/null +++ b/src/auth/project-config.ts @@ -0,0 +1,270 @@ +/*! + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as validator from '../utils/validator'; +import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; +import { + SmsRegionsAuthConfig, + SmsRegionConfig, + MultiFactorConfig, + MultiFactorAuthConfig, + MultiFactorAuthServerConfig, + RecaptchaConfig, + RecaptchaAuthConfig, + PasswordPolicyAuthConfig, + PasswordPolicyAuthServerConfig, + PasswordPolicyConfig, + EmailPrivacyConfig, + EmailPrivacyAuthConfig, +} from './auth-config'; +import { deepCopy } from '../utils/deep-copy'; + +/** + * Interface representing the properties to update on the provided project config. + */ +export interface UpdateProjectConfigRequest { + /** + * The SMS configuration to update on the project. + */ + smsRegionConfig?: SmsRegionConfig; + /** + * The multi-factor auth configuration to update on the project. + */ + multiFactorConfig?: MultiFactorConfig; + + /** + * The reCAPTCHA configuration to update on the project. + * By enabling reCAPTCHA Enterprise integration, you are + * agreeing to the reCAPTCHA Enterprise + * {@link https://cloud.google.com/terms/service-terms | Term of Service}. + */ + recaptchaConfig?: RecaptchaConfig; + /** + * The password policy configuration to update on the project + */ + passwordPolicyConfig?: PasswordPolicyConfig; + /** + * The email privacy configuration to update on the project + */ + emailPrivacyConfig?: EmailPrivacyConfig; +} + +/** + * Response received when getting or updating the project config. + */ +export interface ProjectConfigServerResponse { + smsRegionConfig?: SmsRegionConfig; + mfa?: MultiFactorAuthServerConfig; + recaptchaConfig?: RecaptchaConfig; + passwordPolicyConfig?: PasswordPolicyAuthServerConfig; + emailPrivacyConfig?: EmailPrivacyConfig; +} + +/** + * Request to update the project config. + */ +export interface ProjectConfigClientRequest { + smsRegionConfig?: SmsRegionConfig; + mfa?: MultiFactorAuthServerConfig; + recaptchaConfig?: RecaptchaConfig; + passwordPolicyConfig?: PasswordPolicyAuthServerConfig; + emailPrivacyConfig?: EmailPrivacyConfig; +} + +/** +* Represents a project configuration. +*/ +export class ProjectConfig { + /** + * The SMS Regions Config for the project. + * Configures the regions where users are allowed to send verification SMS. + * This is based on the calling code of the destination phone number. + */ + public readonly smsRegionConfig?: SmsRegionConfig; + + /** + * The project's multi-factor auth configuration. + * Supports only phone and TOTP. + */ + private readonly multiFactorConfig_?: MultiFactorConfig; + /** + * The multi-factor auth configuration. + */ + get multiFactorConfig(): MultiFactorConfig | undefined { + return this.multiFactorConfig_; + } + /** + * The reCAPTCHA configuration to update on the project. + * By enabling reCAPTCHA Enterprise integration, you are + * agreeing to the reCAPTCHA Enterprise + * {@link https://cloud.google.com/terms/service-terms | Term of Service}. + */ + private readonly recaptchaConfig_?: RecaptchaAuthConfig; + + /** + * The password policy configuration for the project + */ + public readonly passwordPolicyConfig?: PasswordPolicyConfig; + /** + * The email privacy configuration for the project + */ + public readonly emailPrivacyConfig?: EmailPrivacyConfig; + + /** + * Validates a project config options object. Throws an error on failure. + * + * @param request - The project config options object to validate. + */ + private static validate(request: UpdateProjectConfigRequest): void { + if (!validator.isNonNullObject(request)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"UpdateProjectConfigRequest" must be a valid non-null object.', + ); + } + const validKeys = { + smsRegionConfig: true, + multiFactorConfig: true, + recaptchaConfig: true, + passwordPolicyConfig: true, + emailPrivacyConfig: true, + } + // Check for unsupported top level attributes. + for (const key in request) { + if (!(key in validKeys)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + `"${key}" is not a valid UpdateProjectConfigRequest parameter.`, + ); + } + } + // Validate SMS Regions Config if provided. + if (typeof request.smsRegionConfig !== 'undefined') { + SmsRegionsAuthConfig.validate(request.smsRegionConfig); + } + + // Validate Multi Factor Config if provided + if (typeof request.multiFactorConfig !== 'undefined') { + MultiFactorAuthConfig.validate(request.multiFactorConfig); + } + // Validate reCAPTCHA config attribute. + if (typeof request.recaptchaConfig !== 'undefined') { + RecaptchaAuthConfig.validate(request.recaptchaConfig); + } + + // Validate Password policy Config if provided + if (typeof request.passwordPolicyConfig !== 'undefined') { + PasswordPolicyAuthConfig.validate(request.passwordPolicyConfig); + } + + // Validate Email Privacy Config if provided. + if (typeof request.emailPrivacyConfig !== 'undefined') { + EmailPrivacyAuthConfig.validate(request.emailPrivacyConfig); + } + } + + /** + * Build the corresponding server request for a UpdateProjectConfigRequest object. + * @param configOptions - The properties to convert to a server request. + * @returns The equivalent server request. + * + * @internal + */ + public static buildServerRequest(configOptions: UpdateProjectConfigRequest): ProjectConfigClientRequest { + ProjectConfig.validate(configOptions); + const request: ProjectConfigClientRequest = {}; + if (typeof configOptions.smsRegionConfig !== 'undefined') { + request.smsRegionConfig = configOptions.smsRegionConfig; + } + if (typeof configOptions.multiFactorConfig !== 'undefined') { + request.mfa = MultiFactorAuthConfig.buildServerRequest(configOptions.multiFactorConfig); + } + if (typeof configOptions.recaptchaConfig !== 'undefined') { + request.recaptchaConfig = configOptions.recaptchaConfig; + } + if (typeof configOptions.passwordPolicyConfig !== 'undefined') { + request.passwordPolicyConfig = PasswordPolicyAuthConfig.buildServerRequest(configOptions.passwordPolicyConfig); + } + if (typeof configOptions.emailPrivacyConfig !== 'undefined') { + request.emailPrivacyConfig = configOptions.emailPrivacyConfig; + } + return request; + } + + /** + * The reCAPTCHA configuration. + */ + get recaptchaConfig(): RecaptchaConfig | undefined { + return this.recaptchaConfig_; + } + /** + * The Project Config object constructor. + * + * @param response - The server side response used to initialize the Project Config object. + * @constructor + * @internal + */ + constructor(response: ProjectConfigServerResponse) { + if (typeof response.smsRegionConfig !== 'undefined') { + this.smsRegionConfig = response.smsRegionConfig; + } + //Backend API returns "mfa" in case of project config and "mfaConfig" in case of tenant config. + //The SDK exposes it as multiFactorConfig always. + if (typeof response.mfa !== 'undefined') { + this.multiFactorConfig_ = new MultiFactorAuthConfig(response.mfa); + } + if (typeof response.recaptchaConfig !== 'undefined') { + this.recaptchaConfig_ = new RecaptchaAuthConfig(response.recaptchaConfig); + } + if (typeof response.passwordPolicyConfig !== 'undefined') { + this.passwordPolicyConfig = new PasswordPolicyAuthConfig(response.passwordPolicyConfig); + } + if (typeof response.emailPrivacyConfig !== 'undefined') { + this.emailPrivacyConfig = response.emailPrivacyConfig; + } + } + /** + * Returns a JSON-serializable representation of this object. + * + * @returns A JSON-serializable representation of this object. + */ + public toJSON(): object { + // JSON serialization + const json = { + smsRegionConfig: deepCopy(this.smsRegionConfig), + multiFactorConfig: deepCopy(this.multiFactorConfig), + recaptchaConfig: this.recaptchaConfig_?.toJSON(), + passwordPolicyConfig: deepCopy(this.passwordPolicyConfig), + emailPrivacyConfig: deepCopy(this.emailPrivacyConfig), + }; + if (typeof json.smsRegionConfig === 'undefined') { + delete json.smsRegionConfig; + } + if (typeof json.multiFactorConfig === 'undefined') { + delete json.multiFactorConfig; + } + if (typeof json.recaptchaConfig === 'undefined') { + delete json.recaptchaConfig; + } + if (typeof json.passwordPolicyConfig === 'undefined') { + delete json.passwordPolicyConfig; + } + if (typeof json.emailPrivacyConfig === 'undefined') { + delete json.emailPrivacyConfig; + } + return json; + } +} + diff --git a/src/auth/tenant-manager.ts b/src/auth/tenant-manager.ts new file mode 100644 index 0000000000..71a505ecc7 --- /dev/null +++ b/src/auth/tenant-manager.ts @@ -0,0 +1,275 @@ +/*! + * 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 * as validator from '../utils/validator'; +import { App } from '../app'; +import * as utils from '../utils/index'; +import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; + +import { BaseAuth, createFirebaseTokenGenerator, SessionCookieOptions } from './base-auth'; +import { Tenant, TenantServerResponse, CreateTenantRequest, UpdateTenantRequest } from './tenant'; +import { + AuthRequestHandler, TenantAwareAuthRequestHandler, +} from './auth-api-request'; +import { DecodedIdToken } from './token-verifier'; + +/** + * Interface representing the object returned from a + * {@link TenantManager.listTenants} + * operation. + * Contains the list of tenants for the current batch and the next page token if available. + */ +export interface ListTenantsResult { + + /** + * The list of {@link Tenant} objects for the downloaded batch. + */ + tenants: Tenant[]; + + /** + * The next page token if available. This is needed for the next batch download. + */ + pageToken?: string; +} + +/** + * Tenant-aware `Auth` interface used for managing users, configuring SAML/OIDC providers, + * generating email links for password reset, email verification, etc for specific tenants. + * + * Multi-tenancy support requires Google Cloud's Identity Platform + * (GCIP). To learn more about GCIP, including pricing and features, + * see the {@link https://cloud.google.com/identity-platform | GCIP documentation}. + * + * Each tenant contains its own identity providers, settings and sets of users. + * Using `TenantAwareAuth`, users for a specific tenant and corresponding OIDC/SAML + * configurations can also be managed, ID tokens for users signed in to a specific tenant + * can be verified, and email action links can also be generated for users belonging to the + * tenant. + * + * `TenantAwareAuth` instances for a specific `tenantId` can be instantiated by calling + * {@link TenantManager.authForTenant}. + */ +export class TenantAwareAuth extends BaseAuth { + + /** + * The tenant identifier corresponding to this `TenantAwareAuth` instance. + * All calls to the user management APIs, OIDC/SAML provider management APIs, email link + * generation APIs, etc will only be applied within the scope of this tenant. + */ + public readonly tenantId: string; + + /** + * The TenantAwareAuth class constructor. + * + * @param app - The app that created this tenant. + * @param tenantId - The corresponding tenant ID. + * @constructor + * @internal + */ + constructor(app: App, tenantId: string) { + super(app, new TenantAwareAuthRequestHandler( + app, tenantId), createFirebaseTokenGenerator(app, tenantId)); + utils.addReadonlyGetter(this, 'tenantId', tenantId); + } + + /** + * {@inheritdoc BaseAuth.verifyIdToken} + */ + public verifyIdToken(idToken: string, checkRevoked = false): Promise { + return super.verifyIdToken(idToken, checkRevoked) + .then((decodedClaims) => { + // Validate tenant ID. + if (decodedClaims.firebase.tenant !== this.tenantId) { + throw new FirebaseAuthError(AuthClientErrorCode.MISMATCHING_TENANT_ID); + } + return decodedClaims; + }); + } + + /** + * {@inheritdoc BaseAuth.createSessionCookie} + */ + public createSessionCookie( + idToken: string, sessionCookieOptions: SessionCookieOptions): Promise { + // Validate arguments before processing. + if (!validator.isNonEmptyString(idToken)) { + return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ID_TOKEN)); + } + if (!validator.isNonNullObject(sessionCookieOptions) || + !validator.isNumber(sessionCookieOptions.expiresIn)) { + return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_SESSION_COOKIE_DURATION)); + } + // This will verify the ID token and then match the tenant ID before creating the session cookie. + return this.verifyIdToken(idToken) + .then(() => { + return super.createSessionCookie(idToken, sessionCookieOptions); + }); + } + + /** + * {@inheritdoc BaseAuth.verifySessionCookie} + */ + public verifySessionCookie( + sessionCookie: string, checkRevoked = false): Promise { + return super.verifySessionCookie(sessionCookie, checkRevoked) + .then((decodedClaims) => { + if (decodedClaims.firebase.tenant !== this.tenantId) { + throw new FirebaseAuthError(AuthClientErrorCode.MISMATCHING_TENANT_ID); + } + return decodedClaims; + }); + } +} + +/** + * Defines the tenant manager used to help manage tenant related operations. + * This includes: + *
    + *
  • The ability to create, update, list, get and delete tenants for the underlying + * project.
  • + *
  • Getting a `TenantAwareAuth` instance for running Auth related operations + * (user management, provider configuration management, token verification, + * email link generation, etc) in the context of a specified tenant.
  • + *
+ */ +export class TenantManager { + private readonly authRequestHandler: AuthRequestHandler; + private readonly tenantsMap: {[key: string]: TenantAwareAuth}; + + /** + * Initializes a TenantManager instance for a specified FirebaseApp. + * + * @param app - The app for this TenantManager instance. + * + * @constructor + * @internal + */ + constructor(private readonly app: App) { + this.authRequestHandler = new AuthRequestHandler(app); + this.tenantsMap = {}; + } + + /** + * Returns a `TenantAwareAuth` instance bound to the given tenant ID. + * + * @param tenantId - The tenant ID whose `TenantAwareAuth` instance is to be returned. + * + * @returns The `TenantAwareAuth` instance corresponding to this tenant identifier. + */ + public authForTenant(tenantId: string): TenantAwareAuth { + if (!validator.isNonEmptyString(tenantId)) { + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_TENANT_ID); + } + if (typeof this.tenantsMap[tenantId] === 'undefined') { + this.tenantsMap[tenantId] = new TenantAwareAuth(this.app, tenantId); + } + return this.tenantsMap[tenantId]; + } + + /** + * Gets the tenant configuration for the tenant corresponding to a given `tenantId`. + * + * @param tenantId - The tenant identifier corresponding to the tenant whose data to fetch. + * + * @returns A promise fulfilled with the tenant configuration to the provided `tenantId`. + */ + public getTenant(tenantId: string): Promise { + return this.authRequestHandler.getTenant(tenantId) + .then((response: TenantServerResponse) => { + return new Tenant(response); + }); + } + + /** + * Retrieves a list of tenants (single batch only) with a size of `maxResults` + * starting from the offset as specified by `pageToken`. This is used to + * retrieve all the tenants of a specified project in batches. + * + * @param maxResults - The page size, 1000 if undefined. This is also + * the maximum allowed limit. + * @param pageToken - The next page token. If not specified, returns + * tenants starting without any offset. + * + * @returns A promise that resolves with + * a batch of downloaded tenants and the next page token. + */ + public listTenants( + maxResults?: number, + pageToken?: string): Promise { + return this.authRequestHandler.listTenants(maxResults, pageToken) + .then((response: {tenants: TenantServerResponse[]; nextPageToken?: string}) => { + // List of tenants to return. + const tenants: Tenant[] = []; + // Convert each user response to a Tenant. + response.tenants.forEach((tenantResponse: TenantServerResponse) => { + tenants.push(new Tenant(tenantResponse)); + }); + // Return list of tenants and the next page token if available. + const result = { + tenants, + pageToken: response.nextPageToken, + }; + // Delete result.pageToken if undefined. + if (typeof result.pageToken === 'undefined') { + delete result.pageToken; + } + return result; + }); + } + + /** + * Deletes an existing tenant. + * + * @param tenantId - The `tenantId` corresponding to the tenant to delete. + * + * @returns An empty promise fulfilled once the tenant has been deleted. + */ + public deleteTenant(tenantId: string): Promise { + return this.authRequestHandler.deleteTenant(tenantId); + } + + /** + * Creates a new tenant. + * When creating new tenants, tenants that use separate billing and quota will require their + * own project and must be defined as `full_service`. + * + * @param tenantOptions - The properties to set on the new tenant configuration to be created. + * + * @returns A promise fulfilled with the tenant configuration corresponding to the newly + * created tenant. + */ + public createTenant(tenantOptions: CreateTenantRequest): Promise { + return this.authRequestHandler.createTenant(tenantOptions) + .then((response: TenantServerResponse) => { + return new Tenant(response); + }); + } + + /** + * Updates an existing tenant configuration. + * + * @param tenantId - The `tenantId` corresponding to the tenant to delete. + * @param tenantOptions - The properties to update on the provided tenant. + * + * @returns A promise fulfilled with the update tenant data. + */ + 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 new file mode 100644 index 0000000000..76d97f3259 --- /dev/null +++ b/src/auth/tenant.ts @@ -0,0 +1,428 @@ +/*! + * 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 * as validator from '../utils/validator'; +import { deepCopy } from '../utils/deep-copy'; +import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; + +import { + EmailSignInConfig, EmailSignInConfigServerRequest, MultiFactorAuthServerConfig, + MultiFactorConfig, validateTestPhoneNumbers, EmailSignInProviderConfig, + MultiFactorAuthConfig, SmsRegionConfig, SmsRegionsAuthConfig, RecaptchaAuthConfig, RecaptchaConfig, + PasswordPolicyConfig, + PasswordPolicyAuthConfig, PasswordPolicyAuthServerConfig, EmailPrivacyConfig, EmailPrivacyAuthConfig, +} from './auth-config'; + +/** + * Interface representing the properties to update on the provided tenant. + */ +export interface UpdateTenantRequest { + + /** + * The tenant display name. + */ + displayName?: string; + + /** + * The email sign in configuration. + */ + emailSignInConfig?: EmailSignInProviderConfig; + + /** + * Whether the anonymous provider is enabled. + */ + anonymousSignInEnabled?: boolean; + + /** + * The multi-factor auth configuration to update on the tenant. + */ + 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; + + /** + * The SMS configuration to update on the project. + */ + smsRegionConfig?: SmsRegionConfig; + + /** + * The reCAPTCHA configuration to update on the tenant. + * By enabling reCAPTCHA Enterprise integration, you are + * agreeing to the reCAPTCHA Enterprise + * {@link https://cloud.google.com/terms/service-terms | Term of Service}. + */ + recaptchaConfig?: RecaptchaConfig; + /** + * The password policy configuration for the tenant + */ + passwordPolicyConfig?: PasswordPolicyConfig; + /** + * The email privacy configuration for the tenant + */ + emailPrivacyConfig?: EmailPrivacyConfig; +} + +/** + * Interface representing the properties to set on a new tenant. + */ +export type CreateTenantRequest = 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}; + smsRegionConfig?: SmsRegionConfig; + recaptchaConfig?: RecaptchaConfig; + passwordPolicyConfig?: PasswordPolicyAuthServerConfig; + emailPrivacyConfig?: EmailPrivacyConfig; +} + +/** The tenant server response interface. */ +export interface TenantServerResponse { + name: string; + displayName?: string; + allowPasswordSignup?: boolean; + enableEmailLinkSignin?: boolean; + enableAnonymousUser?: boolean; + mfaConfig?: MultiFactorAuthServerConfig; + testPhoneNumbers?: {[key: string]: string}; + smsRegionConfig?: SmsRegionConfig; + recaptchaConfig? : RecaptchaConfig; + passwordPolicyConfig?: PasswordPolicyAuthServerConfig; + emailPrivacyConfig?: EmailPrivacyConfig; +} + +/** + * Represents a tenant configuration. + * + * Multi-tenancy support requires Google Cloud's Identity Platform + * (GCIP). To learn more about GCIP, including pricing and features, + * see the {@link https://cloud.google.com/identity-platform | GCIP documentation}. + * + * Before multi-tenancy can be used on a Google Cloud Identity Platform project, + * tenants must be allowed on that project via the Cloud Console UI. + * + * A tenant configuration provides information such as the display name, tenant + * identifier and email authentication configuration. + * For OIDC/SAML provider configuration management, `TenantAwareAuth` instances should + * be used instead of a `Tenant` to retrieve the list of configured IdPs on a tenant. + * When configuring these providers, note that tenants will inherit + * whitelisted domains and authenticated redirect URIs of their parent project. + * + * All other settings of a tenant will also be inherited. These will need to be managed + * from the Cloud Console UI. + */ +export class Tenant { + + /** + * The tenant identifier. + */ + public readonly tenantId: string; + + /** + * The tenant display name. + */ + public readonly displayName?: string; + + public readonly anonymousSignInEnabled: boolean; + + /** + * The map containing the test phone number / code pairs for the tenant. + */ + public readonly testPhoneNumbers?: {[phoneNumber: string]: string}; + + private readonly emailSignInConfig_?: EmailSignInConfig; + private readonly multiFactorConfig_?: MultiFactorAuthConfig; + + /** + * The map conatining the reCAPTCHA config. + * By enabling reCAPTCHA Enterprise Integration you are + * agreeing to reCAPTCHA Enterprise + * {@link https://cloud.google.com/terms/service-terms | Term of Service}. + */ + private readonly recaptchaConfig_?: RecaptchaAuthConfig; + /** + * The SMS Regions Config to update a tenant. + * Configures the regions where users are allowed to send verification SMS. + * This is based on the calling code of the destination phone number. + */ + public readonly smsRegionConfig?: SmsRegionConfig; + /** + * The password policy configuration for the tenant + */ + public readonly passwordPolicyConfig?: PasswordPolicyConfig; + /** + * The email privacy configuration for the tenant + */ + public readonly emailPrivacyConfig?: EmailPrivacyConfig; + + /** + * Builds the corresponding server request for a TenantOptions object. + * + * @param tenantOptions - The properties to convert to a server request. + * @param createRequest - Whether this is a create request. + * @returns The equivalent server request. + * + * @internal + */ + public static buildServerRequest( + tenantOptions: UpdateTenantRequest, createRequest: boolean): TenantOptionsServerRequest { + Tenant.validate(tenantOptions, createRequest); + let request: TenantOptionsServerRequest = {}; + if (typeof tenantOptions.emailSignInConfig !== 'undefined') { + request = EmailSignInConfig.buildServerRequest(tenantOptions.emailSignInConfig); + } + 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); + } + if (typeof tenantOptions.testPhoneNumbers !== 'undefined') { + // null will clear existing test phone numbers. Translate to empty object. + request.testPhoneNumbers = tenantOptions.testPhoneNumbers ?? {}; + } + if (typeof tenantOptions.smsRegionConfig !== 'undefined') { + request.smsRegionConfig = tenantOptions.smsRegionConfig; + } + if (typeof tenantOptions.recaptchaConfig !== 'undefined') { + request.recaptchaConfig = tenantOptions.recaptchaConfig; + } + if (typeof tenantOptions.passwordPolicyConfig !== 'undefined') { + request.passwordPolicyConfig = PasswordPolicyAuthConfig.buildServerRequest(tenantOptions.passwordPolicyConfig); + } + if (typeof tenantOptions.emailPrivacyConfig !== 'undefined') { + request.emailPrivacyConfig = tenantOptions.emailPrivacyConfig; + } + return request; + } + + /** + * Returns the tenant ID corresponding to the resource name if available. + * + * @param resourceName - The server side resource name + * @returns The tenant ID corresponding to the resource, null otherwise. + * + * @internal + */ + public static getTenantIdFromResourceName(resourceName: string): string | null { + // name is of form projects/project1/tenants/tenant1 + const matchTenantRes = resourceName.match(/\/tenants\/(.*)$/); + if (!matchTenantRes || matchTenantRes.length < 2) { + return null; + } + return matchTenantRes[1]; + } + + /** + * Validates a tenant options object. Throws an error on failure. + * + * @param request - The tenant options object to validate. + * @param createRequest - Whether this is a create request. + */ + private static validate(request: any, createRequest: boolean): void { + const validKeys = { + displayName: true, + emailSignInConfig: true, + anonymousSignInEnabled: true, + multiFactorConfig: true, + testPhoneNumbers: true, + smsRegionConfig: true, + recaptchaConfig: true, + passwordPolicyConfig: true, + emailPrivacyConfig: true, + }; + const label = createRequest ? 'CreateTenantRequest' : 'UpdateTenantRequest'; + if (!validator.isNonNullObject(request)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + `"${label}" must be a valid non-null object.`, + ); + } + // Check for unsupported top level attributes. + for (const key in request) { + if (!(key in validKeys)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + `"${key}" is not a valid ${label} parameter.`, + ); + } + } + // Validate displayName type if provided. + if (typeof request.displayName !== 'undefined' && + !validator.isNonEmptyString(request.displayName)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + `"${label}.displayName" must be a valid non-empty string.`, + ); + } + // Validate emailSignInConfig type if provided. + if (typeof request.emailSignInConfig !== 'undefined') { + // 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); + } + // Validate SMS Regions Config if provided. + if (typeof request.smsRegionConfig !== 'undefined') { + SmsRegionsAuthConfig.validate(request.smsRegionConfig); + } + // Validate reCAPTCHAConfig type if provided. + if (typeof request.recaptchaConfig !== 'undefined') { + RecaptchaAuthConfig.validate(request.recaptchaConfig); + } + // Validate passwordPolicyConfig type if provided. + if (typeof request.passwordPolicyConfig !== 'undefined') { + // This will throw an error if invalid. + PasswordPolicyAuthConfig.buildServerRequest(request.passwordPolicyConfig); + } + // Validate Email Privacy Config if provided. + if (typeof request.emailPrivacyConfig !== 'undefined') { + EmailPrivacyAuthConfig.validate(request.emailPrivacyConfig); + } + } + + /** + * The Tenant object constructor. + * + * @param response - The server side response used to initialize the Tenant object. + * @constructor + * @internal + */ + constructor(response: TenantServerResponse) { + const tenantId = Tenant.getTenantIdFromResourceName(response.name); + if (!tenantId) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Invalid tenant response', + ); + } + this.tenantId = tenantId; + this.displayName = response.displayName; + try { + this.emailSignInConfig_ = new EmailSignInConfig(response); + } catch (e) { + // If allowPasswordSignup is undefined, it is disabled by default. + this.emailSignInConfig_ = new EmailSignInConfig({ + allowPasswordSignup: false, + }); + } + this.anonymousSignInEnabled = !!response.enableAnonymousUser; + if (typeof response.mfaConfig !== 'undefined') { + this.multiFactorConfig_ = new MultiFactorAuthConfig(response.mfaConfig); + } + if (typeof response.testPhoneNumbers !== 'undefined') { + this.testPhoneNumbers = deepCopy(response.testPhoneNumbers || {}); + } + if (typeof response.smsRegionConfig !== 'undefined') { + this.smsRegionConfig = deepCopy(response.smsRegionConfig); + } + if (typeof response.recaptchaConfig !== 'undefined') { + this.recaptchaConfig_ = new RecaptchaAuthConfig(response.recaptchaConfig); + } + if (typeof response.passwordPolicyConfig !== 'undefined') { + this.passwordPolicyConfig = new PasswordPolicyAuthConfig(response.passwordPolicyConfig); + } + if (typeof response.emailPrivacyConfig !== 'undefined') { + this.emailPrivacyConfig = deepCopy(response.emailPrivacyConfig); + } + } + + /** + * The email sign in provider configuration. + */ + get emailSignInConfig(): EmailSignInProviderConfig | undefined { + return this.emailSignInConfig_; + } + + /** + * The multi-factor auth configuration on the current tenant. + */ + get multiFactorConfig(): MultiFactorConfig | undefined { + return this.multiFactorConfig_; + } + + /** + * The recaptcha config auth configuration of the current tenant. + */ + get recaptchaConfig(): RecaptchaConfig | undefined { + return this.recaptchaConfig_; + } + + /** + * Returns a JSON-serializable representation of this object. + * + * @returns A JSON-serializable representation of this object. + */ + public toJSON(): object { + const json = { + tenantId: this.tenantId, + displayName: this.displayName, + emailSignInConfig: this.emailSignInConfig_?.toJSON(), + multiFactorConfig: this.multiFactorConfig_?.toJSON(), + anonymousSignInEnabled: this.anonymousSignInEnabled, + testPhoneNumbers: this.testPhoneNumbers, + smsRegionConfig: deepCopy(this.smsRegionConfig), + recaptchaConfig: this.recaptchaConfig_?.toJSON(), + passwordPolicyConfig: deepCopy(this.passwordPolicyConfig), + emailPrivacyConfig: deepCopy(this.emailPrivacyConfig), + }; + if (typeof json.multiFactorConfig === 'undefined') { + delete json.multiFactorConfig; + } + if (typeof json.testPhoneNumbers === 'undefined') { + delete json.testPhoneNumbers; + } + if (typeof json.smsRegionConfig === 'undefined') { + delete json.smsRegionConfig; + } + if (typeof json.recaptchaConfig === 'undefined') { + delete json.recaptchaConfig; + } + if (typeof json.passwordPolicyConfig === 'undefined') { + delete json.passwordPolicyConfig; + } + if (typeof json.emailPrivacyConfig === 'undefined') { + delete json.emailPrivacyConfig; + } + return json; + } +} + diff --git a/src/auth/token-generator.ts b/src/auth/token-generator.ts index c84a32c7f9..7d3bd8a7c6 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"); @@ -14,305 +15,229 @@ * limitations under the License. */ -import {Certificate} from './credential'; -import {AuthClientErrorCode, FirebaseAuthError} from '../utils/error'; +import { AuthClientErrorCode, ErrorInfo, FirebaseAuthError } from '../utils/error'; +import { HttpError } from '../utils/api-request'; +import { CryptoSigner, CryptoSignerError, CryptoSignerErrorCode } from '../utils/crypto-signer'; import * as validator from '../utils/validator'; +import { toWebSafeBase64 } from '../utils'; +import { Algorithm } from 'jsonwebtoken'; -import * as jwt from 'jsonwebtoken'; +const ALGORITHM_NONE: Algorithm = 'none' as const; -// Use untyped import syntax for Node built-ins -import https = require('https'); - - -const ALGORITHM = 'RS256'; const ONE_HOUR_IN_SECONDS = 60 * 60; // List of blacklisted claims which cannot be provided when creating a custom token -const BLACKLISTED_CLAIMS = [ +export const BLACKLISTED_CLAIMS = [ 'acr', 'amr', 'at_hash', 'aud', 'auth_time', 'azp', 'cnf', 'c_hash', 'exp', 'iat', 'iss', 'jti', 'nbf', 'nonce', ]; -// 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'; - // Audience to use for Firebase Auth Custom tokens const FIREBASE_AUDIENCE = 'https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit'; -interface JWTPayload { +/** + * Represents the header of a JWT. + */ +interface JWTHeader { + alg: string; + typ: string; +} + +/** + * Represents the body of a JWT. + */ +interface JWTBody { claims?: object; - uid?: string; + uid: string; + aud: string; + iat: number; + exp: number; + iss: string; + sub: string; + tenant_id?: string; } /** - * Class for generating and verifying different types of Firebase Auth tokens (JWTs). + * 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 + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public sign(buffer: Buffer): Promise { + return Promise.resolve(Buffer.from('')); + } + + /** + * @inheritDoc + */ + public getAccountId(): Promise { + return Promise.resolve('firebase-auth-emulator@example.com'); + } +} + +/** + * Class for generating different types of Firebase Auth tokens (JWTs). + * + * @internal */ export class FirebaseTokenGenerator { - private certificate_: Certificate; - private publicKeys_: object; - private publicKeysExpireAt_: number; - constructor(certificate: Certificate) { - if (!certificate) { + private readonly signer: CryptoSigner; + + /** + * @param tenantId - The tenant ID to use for the generated Firebase Auth + * Custom token. If absent, then no tenant ID claim will be set in the + * resulting JWT. + */ + constructor(signer: CryptoSigner, public readonly tenantId?: string) { + if (!validator.isNonNullObject(signer)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_CREDENTIAL, - 'INTERNAL ASSERT: Must provide a certificate to use FirebaseTokenGenerator.', + 'INTERNAL ASSERT: Must provide a CryptoSigner to use FirebaseTokenGenerator.', ); } - this.certificate_ = certificate; + if (typeof this.tenantId !== 'undefined' && !validator.isNonEmptyString(this.tenantId)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '`tenantId` argument must be a non-empty string.'); + } + this.signer = signer; } /** * Creates a new Firebase Auth Custom token. * - * @param {string} uid The user ID to use for the generated Firebase Auth Custom token. - * @param {object} [developerClaims] Optional developer claims to include in the generated Firebase - * Auth Custom token. - * @return {Promise} A Promise fulfilled with a Firebase Auth Custom token signed with a - * service account key and containing the provided payload. + * @param uid - The user ID to use for the generated Firebase Auth Custom token. + * @param developerClaims - Optional developer claims to include in the generated Firebase + * Auth Custom token. + * @returns A Promise fulfilled with a Firebase Auth Custom token signed with a + * service account key and containing the provided payload. */ - public createCustomToken(uid: string, developerClaims?: object): Promise { - let errorMessage: string; - if (typeof uid !== 'string' || uid === '') { - errorMessage = 'First argument to createCustomToken() must be a non-empty string uid.'; + public createCustomToken(uid: string, developerClaims?: {[key: string]: any}): Promise { + let errorMessage: string | undefined; + if (!validator.isNonEmptyString(uid)) { + errorMessage = '`uid` argument must be a non-empty string uid.'; } else if (uid.length > 128) { - errorMessage = 'First argument to createCustomToken() must a uid with less than or equal to 128 characters.'; + errorMessage = '`uid` argument must a uid with less than or equal to 128 characters.'; } else if (!this.isDeveloperClaimsValid_(developerClaims)) { - errorMessage = 'Second argument to createCustomToken() must be an object containing the developer claims.'; + errorMessage = '`developerClaims` argument must be a valid, non-null object containing the developer claims.'; } - if (typeof errorMessage !== 'undefined') { + if (errorMessage) { throw new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage); } - if (!validator.isNonEmptyString(this.certificate_.privateKey)) { - errorMessage = 'createCustomToken() requires a certificate with "private_key" set.'; - } else if (!validator.isNonEmptyString(this.certificate_.clientEmail)) { - errorMessage = 'createCustomToken() requires a certificate with "client_email" set.'; - } - - if (typeof errorMessage !== 'undefined') { - throw new FirebaseAuthError(AuthClientErrorCode.INVALID_CREDENTIAL, errorMessage); - } - - const jwtPayload: JWTPayload = {}; - + const claims: {[key: string]: any} = {}; if (typeof developerClaims !== 'undefined') { - const claims = {}; - for (const key in developerClaims) { /* istanbul ignore else */ - if (developerClaims.hasOwnProperty(key)) { + if (Object.prototype.hasOwnProperty.call(developerClaims, key)) { if (BLACKLISTED_CLAIMS.indexOf(key) !== -1) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, `Developer claim "${key}" is reserved and cannot be specified.`, ); } - claims[key] = developerClaims[key]; } } - jwtPayload.claims = claims; - } - jwtPayload.uid = uid; - - const customToken = jwt.sign(jwtPayload, this.certificate_.privateKey, { - audience: FIREBASE_AUDIENCE, - expiresIn: ONE_HOUR_IN_SECONDS, - issuer: this.certificate_.clientEmail, - subject: this.certificate_.clientEmail, - algorithm: ALGORITHM, - }); - - return Promise.resolve(customToken); - } - - /** - * Verifies the format and signature of a Firebase Auth ID token. - * - * @param {string} idToken The Firebase Auth ID token to verify. - * @return {Promise} A promise fulfilled with the decoded claims of the Firebase Auth ID - * token. - */ - public verifyIdToken(idToken: string): Promise { - if (typeof idToken !== 'string') { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, - 'First argument to verifyIdToken() must be a Firebase ID token string.', - ); - } - - if (!validator.isNonEmptyString(this.certificate_.projectId)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CREDENTIAL, - 'verifyIdToken() requires a certificate with "project_id" set.', - ); } - - const fullDecodedToken: any = jwt.decode(idToken, { - complete: true, - }); - - const header = fullDecodedToken && fullDecodedToken.header; - const payload = fullDecodedToken && fullDecodedToken.payload; - - const projectIdMatchMessage = ' Make sure the ID token comes from the same Firebase project as the ' + - 'service account used to authenticate this SDK.'; - const verifyIdTokenDocsMessage = ' See https://firebase.google.com/docs/auth/admin/verify-id-tokens ' + - 'for details on how to retrieve an ID token.'; - - let errorMessage: string; - if (!fullDecodedToken) { - errorMessage = 'Decoding Firebase ID token failed. Make sure you passed the entire string JWT ' + - 'which represents an ID token.' + verifyIdTokenDocsMessage; - } else if (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); - - if (isCustomToken) { - errorMessage = 'verifyIdToken() expects an ID token, but was given a custom token.'; - } else if (isLegacyCustomToken) { - errorMessage = 'verifyIdToken() expects an ID token, but was given a legacy custom token.'; - } else { - errorMessage = 'Firebase ID token has no "kid" claim.'; + return this.signer.getAccountId().then((account) => { + const header: JWTHeader = { + alg: this.signer.algorithm, + typ: 'JWT', + }; + const iat = Math.floor(Date.now() / 1000); + const body: JWTBody = { + aud: FIREBASE_AUDIENCE, + iat, + exp: iat + ONE_HOUR_IN_SECONDS, + iss: account, + sub: account, + uid, + }; + if (this.tenantId) { + body.tenant_id = this.tenantId; } - - errorMessage += verifyIdTokenDocsMessage; - } else if (header.alg !== ALGORITHM) { - errorMessage = 'Firebase ID token has incorrect algorithm. Expected "' + ALGORITHM + '" but got ' + - '"' + header.alg + '".' + verifyIdTokenDocsMessage; - } else if (payload.aud !== this.certificate_.projectId) { - errorMessage = 'Firebase ID token has incorrect "aud" (audience) claim. Expected "' + - this.certificate_.projectId + '" but got "' + payload.aud + '".' + projectIdMatchMessage + - verifyIdTokenDocsMessage; - } else if (payload.iss !== 'https://securetoken.google.com/' + this.certificate_.projectId) { - errorMessage = 'Firebase ID token has incorrect "iss" (issuer) claim. Expected ' + - '"https://securetoken.google.com/' + this.certificate_.projectId + '" but got "' + - payload.iss + '".' + projectIdMatchMessage + verifyIdTokenDocsMessage; - } else if (typeof payload.sub !== 'string') { - errorMessage = 'Firebase ID token has no "sub" (subject) claim.' + verifyIdTokenDocsMessage; - } else if (payload.sub === '') { - errorMessage = 'Firebase ID token has an empty string "sub" (subject) claim.' + verifyIdTokenDocsMessage; - } else if (payload.sub.length > 128) { - errorMessage = 'Firebase ID token has "sub" (subject) claim longer than 128 characters.' + - verifyIdTokenDocsMessage; - } - - if (typeof errorMessage !== 'undefined') { - return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage)); - } - - return this.fetchPublicKeys_().then((publicKeys) => { - if (!publicKeys.hasOwnProperty(header.kid)) { - return Promise.reject( - new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, - 'Firebase ID token has "kid" claim which does not correspond to a known public key. ' + - 'Most likely the ID token is expired, so get a fresh token from your client app and ' + - 'try again.' + verifyIdTokenDocsMessage, - ), - ); + if (Object.keys(claims).length > 0) { + body.claims = claims; } - - return new Promise((resolve, reject) => { - jwt.verify(idToken, publicKeys[header.kid], { - algorithms: [ALGORITHM], - }, (error, decodedToken: any) => { - if (error) { - if (error.name === 'TokenExpiredError') { - errorMessage = 'Firebase ID token has expired. Get a fresh token from your client app and try ' + - 'again (auth/id-token-expired).' + verifyIdTokenDocsMessage; - } else if (error.name === 'JsonWebTokenError') { - errorMessage = 'Firebase ID token has invalid signature.' + verifyIdTokenDocsMessage; - } - - return reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage)); - } else { - decodedToken.uid = decodedToken.sub; - resolve(decodedToken); - } - }); - }); + 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)}`; + }).catch((err) => { + throw handleCryptoSignerError(err); }); } + private encodeSegment(segment: object | Buffer): string { + const buffer: Buffer = (segment instanceof Buffer) ? segment : Buffer.from(JSON.stringify(segment)); + return toWebSafeBase64(buffer).replace(/=+$/, ''); + } /** * Returns whether or not the provided developer claims are valid. * - * @param {object} [developerClaims] Optional developer claims to validate. - * @return {boolean} True if the provided claims are valid; otherwise, false. + * @param developerClaims - Optional developer claims to validate. + * @returns True if the provided claims are valid; otherwise, false. */ + // eslint-disable-next-line @typescript-eslint/naming-convention private isDeveloperClaimsValid_(developerClaims?: object): boolean { if (typeof developerClaims === 'undefined') { return true; } - - if (typeof developerClaims === 'object' && developerClaims !== null && !(developerClaims instanceof Array)) { - return true; - } - - return false; + return validator.isNonNullObject(developerClaims); } +} - - /** - * Fetches the public keys for the Google certs. - * - * @return {Promise} A promise fulfilled with public keys for the Google certs. - */ - private fetchPublicKeys_(): Promise { - 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_); +/** + * Creates a new FirebaseAuthError by extracting the error code, message and other relevant + * details from a CryptoSignerError. + * + * @param err - The Error to convert into a FirebaseAuthError error + * @returns 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); +} - return new Promise((resolve, reject) => { - https.get(CLIENT_CERT_URL, (res) => { - const buffers: Buffer[] = []; - - res.on('data', (buffer) => buffers.push(buffer as Buffer)); - - res.on('end', () => { - try { - const response = JSON.parse(Buffer.concat(buffers).toString()); - - if (response.error) { - let errorMessage = 'Error fetching public keys for Google certs: ' + response.error; - /* istanbul ignore else */ - if (response.error_description) { - errorMessage += ' (' + response.error_description + ')'; - } - - reject(new FirebaseAuthError(AuthClientErrorCode.INTERNAL_ERROR, errorMessage)); - } else { - /* istanbul ignore else */ - if (res.headers.hasOwnProperty('cache-control')) { - const cacheControlHeader: string = res.headers['cache-control'] as string; - 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_ = response; - resolve(response); - } - } catch (e) { - /* istanbul ignore next */ - reject(e); - } - }); - }).on('error', reject); - }); +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/auth/token-verifier.ts b/src/auth/token-verifier.ts new file mode 100644 index 0000000000..73fc8f678c --- /dev/null +++ b/src/auth/token-verifier.ts @@ -0,0 +1,633 @@ +/*! + * Copyright 2018 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 { AuthClientErrorCode, FirebaseAuthError, ErrorInfo } from '../utils/error'; +import * as util from '../utils/index'; +import * as validator from '../utils/validator'; +import { + DecodedToken, decodeJwt, JwtError, JwtErrorCode, EmulatorSignatureVerifier, + PublicKeySignatureVerifier, ALGORITHM_RS256, SignatureVerifier, +} from '../utils/jwt'; +import { App } from '../app/index'; + +/** + * Interface representing a decoded Firebase ID token, returned from the + * {@link BaseAuth.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. + */ +export interface DecodedIdToken { + + /** + * The audience for which this token is intended. + * + * This value is a string equal to your Firebase project ID, the unique + * identifier for your Firebase project, which can be found in [your project's + * settings](https://console.firebase.google.com/project/_/settings/general/android:com.random.android). + */ + aud: string; + + /** + * Time, in seconds since the Unix epoch, when the end-user authentication + * occurred. + * + * This value is not set when this particular ID token was created, but when the + * user initially logged in to this session. In a single session, the Firebase + * SDKs will refresh a user's ID tokens every hour. Each ID token will have a + * different [`iat`](#iat) value, but the same `auth_time` value. + */ + auth_time: number; + + /** + * The email of the user to whom the ID token belongs, if available. + */ + email?: string; + + /** + * Whether or not the email of the user to whom the ID token belongs is + * verified, provided the user has an email. + */ + email_verified?: boolean; + + /** + * The ID token's expiration time, in seconds since the Unix epoch. That is, the + * time at which this ID token expires and should no longer be considered valid. + * + * The Firebase SDKs transparently refresh ID tokens every hour, issuing a new + * ID token with up to a one hour expiration. + */ + exp: number; + + /** + * Information about the sign in event, including which sign in provider was + * used and provider-specific identity details. + * + * This data is provided by the Firebase Authentication service and is a + * reserved claim in the ID token. + */ + firebase: { + + /** + * Provider-specific identity details corresponding + * to the provider used to sign in the user. + */ + identities: { + [key: string]: any; + }; + + /** + * The ID of the provider used to sign in the user. + * One of `"anonymous"`, `"password"`, `"facebook.com"`, `"github.com"`, + * `"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; + + /** + * The type identifier or `factorId` of the second factor, provided the + * ID token was obtained from a multi-factor authenticated user. + * For phone, this is `"phone"`. + */ + sign_in_second_factor?: string; + + /** + * The `uid` of the second factor used to sign in, provided the + * ID token was obtained from a multi-factor authenticated user. + */ + second_factor_identifier?: string; + + /** + * The ID of the tenant the user belongs to, if available. + */ + tenant?: string; + [key: string]: any; + }; + + /** + * The ID token's issued-at time, in seconds since the Unix epoch. That is, the + * time at which this ID token was issued and should start to be considered + * valid. + * + * The Firebase SDKs transparently refresh ID tokens every hour, issuing a new + * ID token with a new issued-at time. If you want to get the time at which the + * user session corresponding to the ID token initially occurred, see the + * [`auth_time`](#auth_time) property. + */ + iat: number; + + /** + * The issuer identifier for the issuer of the response. + * + * This value is a URL with the format + * `https://securetoken.google.com/`, where `` is the + * same project ID specified in the [`aud`](#aud) property. + */ + iss: string; + + /** + * The phone number of the user to whom the ID token belongs, if available. + */ + phone_number?: string; + + /** + * The photo URL for the user to whom the ID token belongs, if available. + */ + picture?: string; + + /** + * The `uid` corresponding to the user who the ID token belonged to. + * + * As a convenience, this value is copied over to the [`uid`](#uid) property. + */ + sub: string; + + /** + * The `uid` corresponding to the user who the ID token belonged to. + * + * This value is not actually in the JWT token claims itself. It is added as a + * convenience, and is set as the value of the [`sub`](#sub) property. + */ + uid: string; + + /** + * Other arbitrary claims included in the ID token. + */ + [key: string]: any; +} + +/** @alpha */ +export interface DecodedAuthBlockingSharedUserInfo { + uid: string; + display_name?: string; + email?: string; + photo_url?: string; + phone_number?: string; +} + +/** @alpha */ +export interface DecodedAuthBlockingMetadata { + creation_time?: number; + last_sign_in_time?: number; +} + +/** @alpha */ +export interface DecodedAuthBlockingUserInfo extends DecodedAuthBlockingSharedUserInfo { + provider_id: string; +} + +/** @alpha */ +export interface DecodedAuthBlockingMfaInfo { + uid: string; + display_name?: string; + phone_number?: string; + enrollment_time?: string; + factor_id?: string; +} + +/** @alpha */ +export interface DecodedAuthBlockingEnrolledFactors { + enrolled_factors?: DecodedAuthBlockingMfaInfo[]; +} + +/** @alpha */ +export interface DecodedAuthBlockingUserRecord extends DecodedAuthBlockingSharedUserInfo { + email_verified?: boolean; + disabled?: boolean; + metadata?: DecodedAuthBlockingMetadata; + password_hash?: string; + password_salt?: string; + provider_data?: DecodedAuthBlockingUserInfo[]; + multi_factor?: DecodedAuthBlockingEnrolledFactors; + custom_claims?: any; + tokens_valid_after_time?: number; + tenant_id?: string; + [key: string]: any; +} + +/** @alpha */ +export interface DecodedAuthBlockingToken { + aud: string; + exp: number; + iat: number; + iss: string; + sub: string; + event_id: string; + event_type: string; + ip_address: string; + user_agent?: string; + locale?: string; + sign_in_method?: string; + user_record?: DecodedAuthBlockingUserRecord; + tenant_id?: string; + raw_user_info?: string; + sign_in_attributes?: { + [key: string]: any; + }; + oauth_id_token?: string; + oauth_access_token?: string; + oauth_refresh_token?: string; + oauth_token_secret?: string; + oauth_expires_in?: number; + [key: string]: any; +} + +// Audience to use for Firebase Auth Custom tokens +const FIREBASE_AUDIENCE = 'https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit'; + +// 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'; + +// 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. + * + * @internal + */ +export const ID_TOKEN_INFO: FirebaseTokenInfo = { + url: 'https://firebase.google.com/docs/auth/admin/verify-id-tokens', + verifyApiName: 'verifyIdToken()', + jwtName: 'Firebase ID token', + shortName: 'ID token', + expiredErrorCode: AuthClientErrorCode.ID_TOKEN_EXPIRED, +}; + +/** + * User facing token information related to the Firebase Auth Blocking token. + * + * @internal + */ +export const AUTH_BLOCKING_TOKEN_INFO: FirebaseTokenInfo = { + url: 'https://cloud.google.com/identity-platform/docs/blocking-functions', + verifyApiName: '_verifyAuthBlockingToken()', + jwtName: 'Firebase Auth Blocking token', + shortName: 'Auth Blocking token', + expiredErrorCode: AuthClientErrorCode.AUTH_BLOCKING_TOKEN_EXPIRED, +}; + +/** + * User facing token information related to the Firebase session cookie. + * + * @internal + */ +export const SESSION_COOKIE_INFO: FirebaseTokenInfo = { + url: 'https://firebase.google.com/docs/auth/admin/manage-cookies', + verifyApiName: 'verifySessionCookie()', + jwtName: 'Firebase session cookie', + shortName: 'session cookie', + expiredErrorCode: AuthClientErrorCode.SESSION_COOKIE_EXPIRED, +}; + +/** + * Interface that defines token related user facing information. + * + * @internal + */ +export interface FirebaseTokenInfo { + /** Documentation URL. */ + url: string; + /** verify API name. */ + verifyApiName: string; + /** The JWT full name. */ + jwtName: string; + /** The JWT short name. */ + shortName: string; + /** JWT Expiration error code. */ + expiredErrorCode: ErrorInfo; +} + +/** + * Class for verifying general purpose Firebase JWTs. This verifies ID tokens and session cookies. + * + * @internal + */ +export class FirebaseTokenVerifier { + + private readonly shortNameArticle: string; + private readonly signatureVerifier: SignatureVerifier; + + constructor(clientCertUrl: string, private issuer: string, private tokenInfo: FirebaseTokenInfo, + private readonly app: App) { + + if (!validator.isURL(clientCertUrl)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + 'The provided public client certificate URL is an invalid URL.', + ); + } else if (!validator.isURL(issuer)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '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.', + ); + } else if (!validator.isURL(tokenInfo.url)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '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.', + ); + } else if (!validator.isNonEmptyString(tokenInfo.jwtName)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '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.', + ); + } 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.', + ); + } + 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 jwtToken - The Firebase Auth JWT token to verify. + * @param isEmulator - Whether to accept Auth Emulator tokens. + * @returns A promise fulfilled with the decoded claims of the Firebase Auth ID token. + */ + public verifyJWT(jwtToken: string, isEmulator = false): Promise { + if (!validator.isString(jwtToken)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + `First argument to ${this.tokenInfo.verifyApiName} must be a ${this.tokenInfo.jwtName} string.`, + ); + } + + return this.ensureProjectId() + .then((projectId) => { + return this.decodeAndVerify(jwtToken, projectId, isEmulator); + }) + .then((decoded) => { + const decodedIdToken = decoded.payload as DecodedIdToken; + decodedIdToken.uid = decodedIdToken.sub; + return decodedIdToken; + }); + } + + /** @alpha */ + // eslint-disable-next-line @typescript-eslint/naming-convention + public _verifyAuthBlockingToken( + jwtToken: string, + isEmulator: boolean, + audience: string | undefined): Promise { + if (!validator.isString(jwtToken)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + `First argument to ${this.tokenInfo.verifyApiName} must be a ${this.tokenInfo.jwtName} string.`, + ); + } + + return this.ensureProjectId() + .then((projectId) => { + if (typeof audience === 'undefined') { + audience = `${projectId}.cloudfunctions.net/`; + } + return this.decodeAndVerify(jwtToken, projectId, isEmulator, audience); + }) + .then((decoded) => { + const decodedAuthBlockingToken = decoded.payload as DecodedAuthBlockingToken; + decodedAuthBlockingToken.uid = decodedAuthBlockingToken.sub; + return decodedAuthBlockingToken; + }); + } + + 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); + }) + } + + private decodeAndVerify( + token: string, + projectId: string, + isEmulator: boolean, + audience?: string): Promise { + return this.safeDecode(token) + .then((decodedToken) => { + this.verifyContent(decodedToken, projectId, isEmulator, audience); + 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, + audience: string | undefined): void { + const header = fullDecodedToken && fullDecodedToken.header; + 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.'; + const verifyJwtTokenDocsMessage = ` See ${this.tokenInfo.url} ` + + `for details on how to retrieve ${this.shortNameArticle} ${this.tokenInfo.shortName}.`; + + let errorMessage: string | 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); + + if (isCustomToken) { + errorMessage = `${this.tokenInfo.verifyApiName} expects ${this.shortNameArticle} ` + + `${this.tokenInfo.shortName}, but was given a custom token.`; + } else if (isLegacyCustomToken) { + errorMessage = `${this.tokenInfo.verifyApiName} expects ${this.shortNameArticle} ` + + `${this.tokenInfo.shortName}, but was given a legacy custom token.`; + } else { + errorMessage = `${this.tokenInfo.jwtName} has no "kid" claim.`; + } + + errorMessage += verifyJwtTokenDocsMessage; + } else if (!isEmulator && header.alg !== ALGORITHM_RS256) { + errorMessage = `${this.tokenInfo.jwtName} has incorrect algorithm. Expected "` + ALGORITHM_RS256 + '" but got ' + + '"' + header.alg + '".' + verifyJwtTokenDocsMessage; + } else if (typeof audience !== 'undefined' && !(payload.aud as string).includes(audience)) { + errorMessage = `${this.tokenInfo.jwtName} has incorrect "aud" (audience) claim. Expected "` + + audience + '" but got "' + payload.aud + '".' + verifyJwtTokenDocsMessage; + } else if (typeof audience === 'undefined' && payload.aud !== projectId) { + errorMessage = `${this.tokenInfo.jwtName} has incorrect "aud" (audience) claim. Expected "` + + 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; + } else if (!(payload.event_type !== undefined && + (payload.event_type === 'beforeSendSms' || payload.event_type === 'beforeSendEmail'))) { + // excluding `beforeSendSms` and `beforeSendEmail` from processing `sub` as there is no user record available. + // `sub` is the same as `uid` which is part of the user record. + if (typeof payload.sub !== 'string') { + errorMessage = `${this.tokenInfo.jwtName} has no "sub" (subject) claim.` + verifyJwtTokenDocsMessage; + } else if (payload.sub === '') { + errorMessage = `${this.tokenInfo.jwtName} has an empty "sub" (subject) claim.` + + verifyJwtTokenDocsMessage; + } else if (payload.sub.length > 128) { + errorMessage = `${this.tokenInfo.jwtName} has a "sub" (subject) claim longer than 128 characters.` + + verifyJwtTokenDocsMessage; + } + } + if (errorMessage) { + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage); + } + } + + private verifySignature(jwtToken: string, isEmulator: boolean): + Promise { + const verifier = isEmulator ? EMULATOR_VERIFIER : this.signatureVerifier; + return verifier.verify(jwtToken) + .catch((error) => { + throw this.mapJwtErrorToAuthError(error); + }); + } + + /** + * Maps JwtError to FirebaseAuthError + * + * @param error - JwtError to be mapped. + * @returns FirebaseAuthError or Error instance. + */ + private mapJwtErrorToAuthError(error: JwtError): Error { + const verifyJwtTokenDocsMessage = ` See ${this.tokenInfo.url} ` + + `for details on how to retrieve ${this.shortNameArticle} ${this.tokenInfo.shortName}.`; + 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); + } + return new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, error.message); + } +} + +/** + * Creates a new FirebaseTokenVerifier to verify Firebase ID tokens. + * + * @internal + * @param app - Firebase app instance. + * @returns FirebaseTokenVerifier + */ +export function createIdTokenVerifier(app: App): FirebaseTokenVerifier { + return new FirebaseTokenVerifier( + CLIENT_CERT_URL, + 'https://securetoken.google.com/', + ID_TOKEN_INFO, + app + ); +} + +/** + * Creates a new FirebaseTokenVerifier to verify Firebase Auth Blocking tokens. + * + * @internal + * @param app - Firebase app instance. + * @returns FirebaseTokenVerifier + */ +export function createAuthBlockingTokenVerifier(app: App): FirebaseTokenVerifier { + return new FirebaseTokenVerifier( + CLIENT_CERT_URL, + 'https://securetoken.google.com/', + AUTH_BLOCKING_TOKEN_INFO, + app + ); +} + +/** + * Creates a new FirebaseTokenVerifier to verify Firebase session cookies. + * + * @internal + * @param app - Firebase app instance. + * @returns FirebaseTokenVerifier + */ +export function createSessionCookieVerifier(app: App): FirebaseTokenVerifier { + return new FirebaseTokenVerifier( + SESSION_COOKIE_CERT_URL, + 'https://session.firebase.google.com/', + SESSION_COOKIE_INFO, + app + ); +} diff --git a/src/auth/user-import-builder.ts b/src/auth/user-import-builder.ts new file mode 100644 index 0000000000..23e4e5aba3 --- /dev/null +++ b/src/auth/user-import-builder.ts @@ -0,0 +1,761 @@ +/*! + * Copyright 2018 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 } from '../app/index'; +import { deepCopy, deepExtend } from '../utils/deep-copy'; +import * as utils from '../utils'; +import * as validator from '../utils/validator'; +import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; +import { + UpdateMultiFactorInfoRequest, UpdatePhoneMultiFactorInfoRequest, MultiFactorUpdateSettings +} from './auth-config'; + +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 BaseAuth.importUsers} method. This is used to + * provide the password hashing algorithm information. + */ +export interface UserImportOptions { + + /** + * The password hashing information. + */ + hash: { + + /** + * The password hashing algorithm identifier. The following algorithm + * identifiers are supported: + * `SCRYPT`, `STANDARD_SCRYPT`, `HMAC_SHA512`, `HMAC_SHA256`, `HMAC_SHA1`, + * `HMAC_MD5`, `MD5`, `PBKDF_SHA1`, `BCRYPT`, `PBKDF2_SHA256`, `SHA512`, + * `SHA256` and `SHA1`. + */ + algorithm: HashAlgorithmType; + + /** + * The signing key used in the hash algorithm in buffer bytes. + * Required by hashing algorithms `SCRYPT`, `HMAC_SHA512`, `HMAC_SHA256`, + * `HAMC_SHA1` and `HMAC_MD5`. + */ + key?: Buffer; + + /** + * The salt separator in buffer bytes which is appended to salt when + * verifying a password. This is only used by the `SCRYPT` algorithm. + */ + saltSeparator?: Buffer; + + /** + * The number of rounds for hashing calculation. + * Required for `SCRYPT`, `MD5`, `SHA512`, `SHA256`, `SHA1`, `PBKDF_SHA1` and + * `PBKDF2_SHA256`. + */ + rounds?: number; + + /** + * The memory cost required for `SCRYPT` algorithm, or the CPU/memory cost. + * Required for `STANDARD_SCRYPT` algorithm. + */ + memoryCost?: number; + + /** + * The parallelization of the hashing algorithm. Required for the + * `STANDARD_SCRYPT` algorithm. + */ + parallelization?: number; + + /** + * The block size (normally 8) of the hashing algorithm. Required for the + * `STANDARD_SCRYPT` algorithm. + */ + blockSize?: number; + /** + * The derived key length of the hashing algorithm. Required for the + * `STANDARD_SCRYPT` algorithm. + */ + derivedKeyLength?: number; + }; +} + +/** + * Interface representing a user to import to Firebase Auth via the + * {@link BaseAuth.importUsers} method. + */ +export interface UserImportRecord { + + /** + * The user's `uid`. + */ + uid: string; + + /** + * The user's primary email, if set. + */ + email?: string; + + /** + * Whether or not the user's primary email is verified. + */ + emailVerified?: boolean; + + /** + * The user's display name. + */ + displayName?: string; + + /** + * The user's primary phone number, if set. + */ + phoneNumber?: string; + + /** + * The user's photo URL. + */ + photoURL?: string; + + /** + * Whether or not the user is disabled: `true` for disabled; `false` for + * enabled. + */ + disabled?: boolean; + + /** + * Additional metadata about the user. + */ + metadata?: UserMetadataRequest; + + /** + * An array of providers (for example, Google, Facebook) linked to the user. + */ + 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 }; + + /** + * The buffer of bytes representing the user's hashed password. + * When a user is to be imported with a password hash, + * {@link UserImportOptions} are required to be + * specified to identify the hashing algorithm used to generate this hash. + */ + passwordHash?: Buffer; + + /** + * The buffer of bytes representing the user's password salt. + */ + passwordSalt?: Buffer; + + /** + * The identifier of the tenant where user is to be imported to. + * When not provided in an `admin.auth.Auth` context, the user is uploaded to + * the default parent project. + * 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; + + /** + * The user's multi-factor related properties. + */ + multiFactor?: MultiFactorUpdateSettings; +} + +/** + * User metadata to include when importing a user. + */ +export interface UserMetadataRequest { + + /** + * The date the user last signed in, formatted as a UTC string. + */ + lastSignInTime?: string; + + /** + * The date the user was created, formatted as a UTC string. + */ + creationTime?: string; +} + +/** + * User provider data to include when importing a user. + */ +export interface UserProviderRequest { + + /** + * 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 the response from the + * {@link BaseAuth.importUsers} method for batch + * importing users to Firebase Auth. + */ +export interface UserImportResult { + + /** + * The number of user records that failed to import to Firebase Auth. + */ + failureCount: number; + + /** + * The number of user records that successfully imported to Firebase Auth. + */ + successCount: number; + + /** + * An array of errors corresponding to the provided users to import. The + * length of this array is equal to [`failureCount`](#failureCount). + */ + errors: FirebaseArrayIndexError[]; +} + +/** Interface representing an Auth second factor in Auth server format. */ +export interface AuthFactorInfo { + // Not required for signupNewUser endpoint. + mfaEnrollmentId?: string; + displayName?: string; + phoneInfo?: string; + enrolledAt?: string; + [key: string]: any; +} + + +/** UploadAccount endpoint request user interface. */ +interface UploadAccountUser { + localId: string; + email?: string; + emailVerified?: boolean; + displayName?: string; + disabled?: boolean; + photoUrl?: string; + phoneNumber?: string; + providerUserInfo?: Array<{ + rawId: string; + providerId: string; + email?: string; + displayName?: string; + photoUrl?: string; + }>; + mfaInfo?: AuthFactorInfo[]; + passwordHash?: string; + salt?: string; + lastLoginAt?: number; + createdAt?: number; + customAttributes?: string; + tenantId?: string; +} + + +/** UploadAccount endpoint request hash options. */ +export interface UploadAccountOptions { + hashAlgorithm?: string; + signerKey?: string; + rounds?: number; + memoryCost?: number; + saltSeparator?: string; + cpuMemCost?: number; + parallelization?: number; + blockSize?: number; + dkLen?: number; +} + + +/** UploadAccount endpoint complete request interface. */ +export interface UploadAccountRequest extends UploadAccountOptions { + users?: UploadAccountUser[]; +} + + +/** Callback function to validate an UploadAccountUser object. */ +export type ValidatorFunction = (data: UploadAccountUser) => void; + + +/** + * Converts a client format second factor object to server format. + * @param multiFactorInfo - The client format second factor. + * @returns The corresponding AuthFactorInfo server request format. + */ +export function convertMultiFactorInfoToServerFormat(multiFactorInfo: UpdateMultiFactorInfoRequest): AuthFactorInfo { + let enrolledAt; + if (typeof multiFactorInfo.enrollmentTime !== 'undefined') { + if (validator.isUTCDateString(multiFactorInfo.enrollmentTime)) { + // Convert from UTC date string (client side format) to ISO date string (server side format). + enrolledAt = new Date(multiFactorInfo.enrollmentTime).toISOString(); + } else { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ENROLLMENT_TIME, + `The second factor "enrollmentTime" for "${multiFactorInfo.uid}" must be a valid ` + + 'UTC date string.'); + } + } + // Currently only phone second factors are supported. + if (isPhoneFactor(multiFactorInfo)) { + // If any required field is missing or invalid, validation will still fail later. + const authFactorInfo: AuthFactorInfo = { + mfaEnrollmentId: multiFactorInfo.uid, + displayName: multiFactorInfo.displayName, + // Required for all phone second factors. + phoneInfo: multiFactorInfo.phoneNumber, + enrolledAt, + }; + for (const objKey in authFactorInfo) { + if (typeof authFactorInfo[objKey] === 'undefined') { + delete authFactorInfo[objKey]; + } + } + return authFactorInfo; + } else { + // Unsupported second factor. + throw new FirebaseAuthError( + AuthClientErrorCode.UNSUPPORTED_SECOND_FACTOR, + `Unsupported second factor "${JSON.stringify(multiFactorInfo)}" provided.`); + } +} + +function isPhoneFactor(multiFactorInfo: UpdateMultiFactorInfoRequest): + multiFactorInfo is UpdatePhoneMultiFactorInfoRequest { + return multiFactorInfo.factorId === 'phone'; +} + +/** + * @param {any} obj The object to check for number field within. + * @param {string} key The entry key. + * @returns {number} The corresponding number if available. Otherwise, NaN. + */ +function getNumberField(obj: any, key: string): number { + if (typeof obj[key] !== 'undefined' && obj[key] !== null) { + return parseInt(obj[key].toString(), 10); + } + return NaN; +} + + +/** + * Converts a UserImportRecord to a UploadAccountUser object. Throws an error when invalid + * fields are provided. + * @param {UserImportRecord} user The UserImportRecord to conver to UploadAccountUser. + * @param {ValidatorFunction=} userValidator The user validator function. + * @returns {UploadAccountUser} The corresponding UploadAccountUser to return. + */ +function populateUploadAccountUser( + user: UserImportRecord, userValidator?: ValidatorFunction): UploadAccountUser { + const result: UploadAccountUser = { + localId: user.uid, + email: user.email, + emailVerified: user.emailVerified, + displayName: user.displayName, + disabled: user.disabled, + photoUrl: user.photoURL, + phoneNumber: user.phoneNumber, + providerUserInfo: [], + mfaInfo: [], + tenantId: user.tenantId, + customAttributes: user.customClaims && JSON.stringify(user.customClaims), + }; + if (typeof user.passwordHash !== 'undefined') { + if (!validator.isBuffer(user.passwordHash)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_PASSWORD_HASH, + ); + } + result.passwordHash = utils.toWebSafeBase64(user.passwordHash); + } + if (typeof user.passwordSalt !== 'undefined') { + if (!validator.isBuffer(user.passwordSalt)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_PASSWORD_SALT, + ); + } + result.salt = utils.toWebSafeBase64(user.passwordSalt); + } + if (validator.isNonNullObject(user.metadata)) { + if (validator.isNonEmptyString(user.metadata.creationTime)) { + result.createdAt = new Date(user.metadata.creationTime).getTime(); + } + if (validator.isNonEmptyString(user.metadata.lastSignInTime)) { + result.lastLoginAt = new Date(user.metadata.lastSignInTime).getTime(); + } + } + if (validator.isArray(user.providerData)) { + user.providerData.forEach((providerData) => { + result.providerUserInfo!.push({ + providerId: providerData.providerId, + rawId: providerData.uid, + email: providerData.email, + displayName: providerData.displayName, + photoUrl: providerData.photoURL, + }); + }); + } + + // Convert user.multiFactor.enrolledFactors to server format. + if (validator.isNonNullObject(user.multiFactor) && + validator.isNonEmptyArray(user.multiFactor.enrolledFactors)) { + user.multiFactor.enrolledFactors.forEach((multiFactorInfo) => { + result.mfaInfo!.push(convertMultiFactorInfoToServerFormat(multiFactorInfo)); + }); + } + + // Remove blank fields. + let key: keyof UploadAccountUser; + for (key in result) { + if (typeof result[key] === 'undefined') { + delete result[key]; + } + } + if (result.providerUserInfo!.length === 0) { + delete result.providerUserInfo; + } + if (result.mfaInfo!.length === 0) { + delete result.mfaInfo; + } + // Validate the constructured user individual request. This will throw if an error + // is detected. + if (typeof userValidator === 'function') { + userValidator(result); + } + return result; +} + + +/** + * Class that provides a helper for building/validating uploadAccount requests and + * UserImportResult responses. + */ +export class UserImportBuilder { + private requiresHashOptions: boolean; + private validatedUsers: UploadAccountUser[]; + private validatedOptions: UploadAccountOptions; + private indexMap: {[key: number]: number}; + private userImportResultErrors: FirebaseArrayIndexError[]; + + /** + * @param {UserImportRecord[]} users The list of user records to import. + * @param {UserImportOptions=} options The import options which includes hashing + * algorithm details. + * @param {ValidatorFunction=} userRequestValidator The user request validator function. + * @constructor + */ + constructor( + users: UserImportRecord[], + options?: UserImportOptions, + userRequestValidator?: ValidatorFunction) { + this.requiresHashOptions = false; + this.validatedUsers = []; + this.userImportResultErrors = []; + this.indexMap = {}; + + this.validatedUsers = this.populateUsers(users, userRequestValidator); + this.validatedOptions = this.populateOptions(options, this.requiresHashOptions); + } + + /** + * Returns the corresponding constructed uploadAccount request. + * @returns {UploadAccountRequest} The constructed uploadAccount request. + */ + public buildRequest(): UploadAccountRequest { + const users = this.validatedUsers.map((user) => { + return deepCopy(user); + }); + return deepExtend({ users }, deepCopy(this.validatedOptions)) as UploadAccountRequest; + } + + /** + * Populates the UserImportResult using the client side detected errors and the server + * side returned errors. + * @returns {UserImportResult} The user import result based on the returned failed + * uploadAccount response. + */ + public buildResponse( + failedUploads: Array<{index: number; message: string}>): UserImportResult { + // Initialize user import result. + const importResult: UserImportResult = { + successCount: this.validatedUsers.length, + failureCount: this.userImportResultErrors.length, + errors: deepCopy(this.userImportResultErrors), + }; + importResult.failureCount += failedUploads.length; + importResult.successCount -= failedUploads.length; + failedUploads.forEach((failedUpload) => { + importResult.errors.push({ + // Map backend request index to original developer provided array index. + index: this.indexMap[failedUpload.index], + error: new FirebaseAuthError( + AuthClientErrorCode.INVALID_USER_IMPORT, + failedUpload.message, + ), + }); + }); + // Sort errors by index. + importResult.errors.sort((a, b) => { + return a.index - b.index; + }); + // Return sorted result. + return importResult; + } + + /** + * Validates and returns the hashing options of the uploadAccount request. + * Throws an error whenever an invalid or missing options is detected. + * @param {UserImportOptions} options The UserImportOptions. + * @param {boolean} requiresHashOptions Whether to require hash options. + * @returns {UploadAccountOptions} The populated UploadAccount options. + */ + private populateOptions( + options: UserImportOptions | undefined, requiresHashOptions: boolean): UploadAccountOptions { + let populatedOptions: UploadAccountOptions; + if (!requiresHashOptions) { + return {}; + } + if (!validator.isNonNullObject(options)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"UserImportOptions" are required when importing users with passwords.', + ); + } + if (!validator.isNonNullObject(options.hash)) { + throw new FirebaseAuthError( + AuthClientErrorCode.MISSING_HASH_ALGORITHM, + '"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.', + ); + } + + let rounds: number; + switch (options.hash.algorithm) { + case 'HMAC_SHA512': + case 'HMAC_SHA256': + case 'HMAC_SHA1': + case 'HMAC_MD5': + if (!validator.isBuffer(options.hash.key)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_HASH_KEY, + 'A non-empty "hash.key" byte buffer must be provided for ' + + `hash algorithm ${options.hash.algorithm}.`, + ); + } + populatedOptions = { + hashAlgorithm: options.hash.algorithm, + signerKey: utils.toWebSafeBase64(options.hash.key), + }; + break; + + case 'MD5': + case 'SHA1': + case 'SHA256': + case 'SHA512': { + // MD5 is [0,8192] but SHA1, SHA256, and SHA512 are [1,8192] + rounds = getNumberField(options.hash, 'rounds'); + const minRounds = options.hash.algorithm === 'MD5' ? 0 : 1; + if (isNaN(rounds) || rounds < minRounds || rounds > 8192) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_HASH_ROUNDS, + `A valid "hash.rounds" number between ${minRounds} and 8192 must be provided for ` + + `hash algorithm ${options.hash.algorithm}.`, + ); + } + populatedOptions = { + hashAlgorithm: options.hash.algorithm, + rounds, + }; + break; + } + case 'PBKDF_SHA1': + case 'PBKDF2_SHA256': + rounds = getNumberField(options.hash, 'rounds'); + 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 ' + + `hash algorithm ${options.hash.algorithm}.`, + ); + } + populatedOptions = { + hashAlgorithm: options.hash.algorithm, + rounds, + }; + break; + + case 'SCRYPT': { + if (!validator.isBuffer(options.hash.key)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_HASH_KEY, + 'A "hash.key" byte buffer must be provided for ' + + `hash algorithm ${options.hash.algorithm}.`, + ); + } + rounds = getNumberField(options.hash, 'rounds'); + 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 ' + + `hash algorithm ${options.hash.algorithm}.`, + ); + } + const memoryCost = getNumberField(options.hash, 'memoryCost'); + 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 ' + + `hash algorithm ${options.hash.algorithm}.`, + ); + } + if (typeof options.hash.saltSeparator !== 'undefined' && + !validator.isBuffer(options.hash.saltSeparator)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_HASH_SALT_SEPARATOR, + '"hash.saltSeparator" must be a byte buffer.', + ); + } + populatedOptions = { + hashAlgorithm: options.hash.algorithm, + signerKey: utils.toWebSafeBase64(options.hash.key), + rounds, + memoryCost, + saltSeparator: utils.toWebSafeBase64(options.hash.saltSeparator || Buffer.from('')), + }; + break; + } + case 'BCRYPT': + populatedOptions = { + hashAlgorithm: options.hash.algorithm, + }; + break; + + case 'STANDARD_SCRYPT': { + const cpuMemCost = getNumberField(options.hash, 'memoryCost'); + if (isNaN(cpuMemCost)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_HASH_MEMORY_COST, + 'A valid "hash.memoryCost" number must be provided for ' + + `hash algorithm ${options.hash.algorithm}.`, + ); + } + const parallelization = getNumberField(options.hash, 'parallelization'); + if (isNaN(parallelization)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_HASH_PARALLELIZATION, + 'A valid "hash.parallelization" number must be provided for ' + + `hash algorithm ${options.hash.algorithm}.`, + ); + } + const blockSize = getNumberField(options.hash, 'blockSize'); + if (isNaN(blockSize)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_HASH_BLOCK_SIZE, + 'A valid "hash.blockSize" number must be provided for ' + + `hash algorithm ${options.hash.algorithm}.`, + ); + } + const dkLen = getNumberField(options.hash, 'derivedKeyLength'); + if (isNaN(dkLen)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_HASH_DERIVED_KEY_LENGTH, + 'A valid "hash.derivedKeyLength" number must be provided for ' + + `hash algorithm ${options.hash.algorithm}.`, + ); + } + populatedOptions = { + hashAlgorithm: options.hash.algorithm, + cpuMemCost, + parallelization, + blockSize, + dkLen, + }; + break; + } + default: + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_HASH_ALGORITHM, + `Unsupported hash algorithm provider "${options.hash.algorithm}".`, + ); + } + return populatedOptions; + } + + /** + * Validates and returns the users list of the uploadAccount request. + * Whenever a user with an error is detected, the error is cached and will later be + * merged into the user import result. This allows the processing of valid users without + * failing early on the first error detected. + * @param {UserImportRecord[]} users The UserImportRecords to convert to UnploadAccountUser + * objects. + * @param {ValidatorFunction=} userValidator The user validator function. + * @returns {UploadAccountUser[]} The populated uploadAccount users. + */ + private populateUsers( + users: UserImportRecord[], userValidator?: ValidatorFunction): UploadAccountUser[] { + const populatedUsers: UploadAccountUser[] = []; + users.forEach((user, index) => { + try { + const result = populateUploadAccountUser(user, userValidator); + if (typeof result.passwordHash !== 'undefined') { + this.requiresHashOptions = true; + } + // Only users that pass client screening will be passed to backend for processing. + populatedUsers.push(result); + // Map user's index (the one to be sent to backend) to original developer provided array. + this.indexMap[populatedUsers.length - 1] = index; + } catch (error) { + // Save the client side error with respect to the developer provided array. + this.userImportResultErrors.push({ + index, + error, + }); + } + }); + return populatedUsers; + } +} diff --git a/src/auth/user-record.ts b/src/auth/user-record.ts index 303cc99610..5b00151401 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"); @@ -14,17 +15,23 @@ * limitations under the License. */ -import {deepCopy} from '../utils/deep-copy'; +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. + */ +const B64_REDACTED = Buffer.from('REDACTED').toString('base64'); /** * Parses a time stamp string or number and returns the corresponding date if valid. * - * @param {any} time The unix timestamp string or number in milliseconds. - * @return {string} The corresponding date as a UTC string, if valid. + * @param time - The unix timestamp string or number in milliseconds. + * @returns The corresponding date as a UTC string, if valid. Otherwise, null. */ -function parseDate(time: any): string { +function parseDate(time: any): string | null { try { const date = new Date(parseInt(time, 10)); if (!isNaN(date.getTime())) { @@ -36,69 +43,417 @@ function parseDate(time: any): string { return null; } -/** Parameters for update user operation */ -export interface UpdateRequest { +export interface MultiFactorInfoResponse { + mfaEnrollmentId: string; + displayName?: string; + phoneInfo?: string; + totpInfo?: TotpInfoResponse; + enrolledAt?: string; + [key: string]: unknown; +} + +export interface TotpInfoResponse { + [key: string]: unknown; +} + +export interface ProviderUserInfoResponse { + rawId: string; displayName?: string; email?: string; + photoUrl?: string; + phoneNumber?: string; + providerId: string; + federatedId?: string; +} + +export interface GetAccountInfoUserResponse { + localId: string; + email?: string; emailVerified?: boolean; phoneNumber?: string; - photoURL?: string; + displayName?: string; + photoUrl?: string; disabled?: boolean; - password?: string; + passwordHash?: string; + salt?: string; + customAttributes?: string; + validSince?: string; + tenantId?: string; + providerUserInfo?: ProviderUserInfoResponse[]; + mfaInfo?: MultiFactorInfoResponse[]; + createdAt?: string; + lastLoginAt?: string; + lastRefreshAt?: string; + [key: string]: any; } -/** Parameters for create user operation */ -export interface CreateRequest extends UpdateRequest { - uid?: string; +enum MultiFactorId { + Phone = 'phone', + Totp = 'totp', } /** - * User metadata class that provides metadata information like user account creation - * and last sign in time. - * - * @param {object} response The server side response returned from the getAccountInfo - * endpoint. - * @constructor + * Interface representing the common properties of a user-enrolled second factor. + */ +export abstract class MultiFactorInfo { + + /** + * The ID of the enrolled second factor. This ID is unique to the user. + */ + public readonly uid: string; + + /** + * The optional display name of the enrolled second factor. + */ + public readonly displayName?: string; + + /** + * The type identifier of the second factor. + * For SMS second factors, this is `phone`. + * For TOTP second factors, this is `totp`. + */ + public readonly factorId: string; + + /** + * The optional date the second factor was enrolled, formatted as a UTC string. + */ + public readonly enrollmentTime?: string; + + /** + * Initializes the MultiFactorInfo associated subclass using the server side. + * If no MultiFactorInfo is associated with the response, null is returned. + * + * @param response - The server side response. + * @internal + */ + public static initMultiFactorInfo(response: MultiFactorInfoResponse): MultiFactorInfo | null { + let multiFactorInfo: MultiFactorInfo | null = null; + // PhoneMultiFactorInfo, TotpMultiFactorInfo currently available. + try { + if (response.phoneInfo !== undefined) { + multiFactorInfo = new PhoneMultiFactorInfo(response); + } else if (response.totpInfo !== undefined) { + multiFactorInfo = new TotpMultiFactorInfo(response); + } else { + // Ignore the other SDK unsupported MFA factors to prevent blocking developers using the current SDK. + } + } catch (e) { + // Ignore error. + } + return multiFactorInfo; + } + + /** + * Initializes the MultiFactorInfo object using the server side response. + * + * @param response - The server side response. + * @constructor + * @internal + */ + constructor(response: MultiFactorInfoResponse) { + this.initFromServerResponse(response); + } + + /** + * Returns a JSON-serializable representation of this object. + * + * @returns A JSON-serializable representation of this object. + */ + public toJSON(): object { + return { + uid: this.uid, + displayName: this.displayName, + factorId: this.factorId, + enrollmentTime: this.enrollmentTime, + }; + } + + /** + * Returns the factor ID based on the response provided. + * + * @param response - The server side response. + * @returns The multi-factor ID associated with the provided response. If the response is + * not associated with any known multi-factor ID, null is returned. + * + * @internal + */ + protected abstract getFactorId(response: MultiFactorInfoResponse): string | null; + + /** + * Initializes the MultiFactorInfo object using the provided server response. + * + * @param response - The server side response. + */ + private initFromServerResponse(response: MultiFactorInfoResponse): void { + const factorId = response && this.getFactorId(response); + if (!factorId || !response || !response.mfaEnrollmentId) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Invalid multi-factor info response'); + } + utils.addReadonlyGetter(this, 'uid', response.mfaEnrollmentId); + utils.addReadonlyGetter(this, 'factorId', factorId); + 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. + // This can be computed using Data.prototype.toISOString. + if (response.enrolledAt) { + utils.addReadonlyGetter( + this, 'enrollmentTime', new Date(response.enrolledAt).toUTCString()); + } else { + utils.addReadonlyGetter(this, 'enrollmentTime', null); + } + } +} + +/** + * Interface representing a phone specific user-enrolled second factor. + */ +export class PhoneMultiFactorInfo extends MultiFactorInfo { + + /** + * The phone number associated with a phone second factor. + */ + public readonly phoneNumber: string; + + /** + * Initializes the PhoneMultiFactorInfo object using the server side response. + * + * @param response - The server side response. + * @constructor + * @internal + */ + constructor(response: MultiFactorInfoResponse) { + super(response); + utils.addReadonlyGetter(this, 'phoneNumber', response.phoneInfo); + } + + /** + * {@inheritdoc MultiFactorInfo.toJSON} + */ + public toJSON(): object { + return Object.assign( + super.toJSON(), + { + phoneNumber: this.phoneNumber, + }); + } + + /** + * Returns the factor ID based on the response provided. + * + * @param response - The server side response. + * @returns The multi-factor ID associated with the provided response. If the response is + * not associated with any known multi-factor ID, null is returned. + * + * @internal + */ + protected getFactorId(response: MultiFactorInfoResponse): string | null { + return (response && response.phoneInfo) ? MultiFactorId.Phone : null; + } +} + +/** + * `TotpInfo` struct associated with a second factor + */ +export class TotpInfo { + +} + +/** + * Interface representing a TOTP specific user-enrolled second factor. + */ +export class TotpMultiFactorInfo extends MultiFactorInfo { + + /** + * `TotpInfo` struct associated with a second factor + */ + public readonly totpInfo: TotpInfo; + + /** + * Initializes the `TotpMultiFactorInfo` object using the server side response. + * + * @param response - The server side response. + * @constructor + * @internal + */ + constructor(response: MultiFactorInfoResponse) { + super(response); + utils.addReadonlyGetter(this, 'totpInfo', response.totpInfo); + } + + /** + * {@inheritdoc MultiFactorInfo.toJSON} + */ + public toJSON(): object { + return Object.assign( + super.toJSON(), + { + totpInfo: this.totpInfo, + }); + } + + /** + * Returns the factor ID based on the response provided. + * + * @param response - The server side response. + * @returns The multi-factor ID associated with the provided response. If the response is + * not associated with any known multi-factor ID, `null` is returned. + * + * @internal + */ + protected getFactorId(response: MultiFactorInfoResponse): string | null { + return (response && response.totpInfo) ? MultiFactorId.Totp : null; + } +} + +/** + * The multi-factor related user settings. + */ +export class MultiFactorSettings { + + /** + * List of second factors enrolled with the current user. + * Currently only phone and TOTP second factors are supported. + */ + public enrolledFactors: MultiFactorInfo[]; + + /** + * Initializes the `MultiFactor` object using the server side or JWT format response. + * + * @param response - The server side response. + * @constructor + * @internal + */ + constructor(response: GetAccountInfoUserResponse) { + const parsedEnrolledFactors: MultiFactorInfo[] = []; + if (!isNonNullObject(response)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Invalid multi-factor response'); + } else if (response.mfaInfo) { + response.mfaInfo.forEach((factorResponse) => { + const multiFactorInfo = MultiFactorInfo.initMultiFactorInfo(factorResponse); + if (multiFactorInfo) { + parsedEnrolledFactors.push(multiFactorInfo); + } + }); + } + // Make enrolled factors immutable. + utils.addReadonlyGetter( + this, 'enrolledFactors', Object.freeze(parsedEnrolledFactors)); + } + + /** + * Returns a JSON-serializable representation of this multi-factor object. + * + * @returns A JSON-serializable representation of this multi-factor object. + */ + public toJSON(): object { + return { + enrolledFactors: this.enrolledFactors.map((info) => info.toJSON()), + }; + } +} + +/** + * Represents a user's metadata. */ export class UserMetadata { + + /** + * The date the user was created, formatted as a UTC string. + */ public readonly creationTime: string; + + /** + * The date the user last signed in, formatted as a UTC string. + */ public readonly lastSignInTime: string; - constructor(response: any) { + /** + * The time at which the user was last active (ID token refreshed), + * formatted as a UTC Date string (eg 'Sat, 03 Feb 2001 04:05:06 GMT'). + * Returns null if the user was never active. + */ + public readonly lastRefreshTime?: string | null; + + /** + * @param response - The server side response returned from the `getAccountInfo` + * endpoint. + * @constructor + * @internal + */ + constructor(response: GetAccountInfoUserResponse) { // Creation date should always be available but due to some backend bugs there // were cases in the past where users did not have creation date properly set. // This included legacy Firebase migrating project users and some anonymous users. // These bugs have already been addressed since then. utils.addReadonlyGetter(this, 'creationTime', parseDate(response.createdAt)); utils.addReadonlyGetter(this, 'lastSignInTime', parseDate(response.lastLoginAt)); + const lastRefreshAt = response.lastRefreshAt ? new Date(response.lastRefreshAt).toUTCString() : null; + utils.addReadonlyGetter(this, 'lastRefreshTime', lastRefreshAt); } - /** @return {object} The plain object representation of the user's metadata. */ + /** + * Returns a JSON-serializable representation of this object. + * + * @returns A JSON-serializable representation of this object. + */ public toJSON(): object { return { lastSignInTime: this.lastSignInTime, creationTime: this.creationTime, + lastRefreshTime: this.lastRefreshTime, }; } } /** - * User info class that provides provider user information for different - * Firebase providers like google.com, facebook.com, password, etc. - * - * @param {object} response The server side response returned from the getAccountInfo - * endpoint. - * @constructor + * Represents a user's info from a third-party identity provider + * such as Google or Facebook. */ export class UserInfo { + + /** + * The user identifier for the linked provider. + */ public readonly uid: string; + + /** + * The display name for the linked provider. + */ public readonly displayName: string; + + /** + * The email for the linked provider. + */ public readonly email: string; + + /** + * The photo URL for the linked provider. + */ public readonly photoURL: string; + + /** + * The linked provider ID (for example, "google.com" for the Google provider). + */ public readonly providerId: string; + + /** + * The phone number for the linked provider. + */ public readonly phoneNumber: string; - constructor(response: any) { + + /** + * @param response - The server side response returned from the `getAccountInfo` + * endpoint. + * @constructor + * @internal + */ + constructor(response: ProviderUserInfoResponse) { // Provider user id and provider id are required. if (!response.rawId || !response.providerId) { throw new FirebaseAuthError( @@ -114,7 +469,11 @@ export class UserInfo { utils.addReadonlyGetter(this, 'phoneNumber', response.phoneNumber); } - /** @return {object} The plain object representation of the current provider data. */ + /** + * Returns a JSON-serializable representation of this object. + * + * @returns A JSON-serializable representation of this object. + */ public toJSON(): object { return { uid: this.uid, @@ -128,29 +487,108 @@ export class UserInfo { } /** - * User record class that defines the Firebase user object populated from - * the Firebase Auth getAccountInfo response. - * - * @param {any} response The server side response returned from the getAccountInfo - * endpoint. - * @constructor + * Represents a user. */ export class UserRecord { + + /** + * The user's `uid`. + */ public readonly uid: string; - public readonly email: string; + + /** + * The user's primary email, if set. + */ + public readonly email?: string; + + /** + * Whether or not the user's primary email is verified. + */ public readonly emailVerified: boolean; - public readonly displayName: string; - public readonly photoURL: string; - public readonly phoneNumber: string; + + /** + * The user's display name. + */ + public readonly displayName?: string; + + /** + * The user's photo URL. + */ + public readonly photoURL?: string; + + /** + * The user's primary phone number, if set. + */ + public readonly phoneNumber?: string; + + /** + * Whether or not the user is disabled: `true` for disabled; `false` for + * enabled. + */ public readonly disabled: boolean; + + /** + * Additional metadata about the user. + */ public readonly metadata: UserMetadata; + + /** + * An array of providers (for example, Google, Facebook) linked to the user. + */ public readonly providerData: UserInfo[]; + + /** + * The user's hashed password (base64-encoded), only if Firebase Auth hashing + * algorithm (SCRYPT) is used. If a different hashing algorithm had been used + * 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 BaseAuth.listUsers}. + */ public readonly passwordHash?: string; + + /** + * The user's password salt (base64-encoded), only if Firebase Auth hashing + * algorithm (SCRYPT) is used. If a different hashing algorithm had been used to + * 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 BaseAuth.listUsers}. + */ public readonly passwordSalt?: string; - public readonly customClaims: object; + + /** + * 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 BaseAuth.setCustomUserClaims} + */ + public readonly customClaims?: {[key: string]: any}; + + /** + * The ID of the tenant the user belongs to, if available. + */ + public readonly tenantId?: string | null; + + /** + * 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 BaseAuth.revokeRefreshTokens} + * API or from the Firebase Auth backend on big account changes (password + * resets, password or email updates, etc). + */ public readonly tokensValidAfterTime?: string; - constructor(response: any) { + /** + * The multi-factor related properties for the current user, if available. + */ + public readonly multiFactor?: MultiFactorSettings; + + /** + * @param response - The server side response returned from the getAccountInfo + * endpoint. + * @constructor + * @internal + */ + constructor(response: GetAccountInfoUserResponse) { // The Firebase user id is required. if (!response.localId) { throw new FirebaseAuthError( @@ -172,24 +610,40 @@ export class UserRecord { providerData.push(new UserInfo(entry)); } utils.addReadonlyGetter(this, 'providerData', providerData); - utils.addReadonlyGetter(this, 'passwordHash', response.passwordHash); + + // If the password hash is redacted (probably due to missing permissions) + // then clear it out, similar to how the salt is returned. (Otherwise, it + // *looks* like a b64-encoded hash is present, which is confusing.) + if (response.passwordHash === B64_REDACTED) { + utils.addReadonlyGetter(this, 'passwordHash', undefined); + } else { + utils.addReadonlyGetter(this, 'passwordHash', response.passwordHash); + } + utils.addReadonlyGetter(this, 'passwordSalt', response.salt); - try { + if (response.customAttributes) { utils.addReadonlyGetter( - this, 'customClaims', JSON.parse(response.customAttributes)); - } catch (e) { - // Ignore error. - utils.addReadonlyGetter(this, 'customClaims', undefined); + this, 'customClaims', JSON.parse(response.customAttributes)); } - let validAfterTime: string = null; + + let validAfterTime: string | null = null; // Convert validSince first to UTC milliseconds and then to UTC date string. if (typeof response.validSince !== 'undefined') { - validAfterTime = parseDate(response.validSince * 1000); + validAfterTime = parseDate(parseInt(response.validSince, 10) * 1000); + } + utils.addReadonlyGetter(this, 'tokensValidAfterTime', validAfterTime || undefined); + utils.addReadonlyGetter(this, 'tenantId', response.tenantId); + const multiFactor = new MultiFactorSettings(response); + if (multiFactor.enrolledFactors.length > 0) { + utils.addReadonlyGetter(this, 'multiFactor', multiFactor); } - utils.addReadonlyGetter(this, 'tokensValidAfterTime', validAfterTime); } - /** @return {object} The plain object representation of the user record. */ + /** + * Returns a JSON-serializable representation of this object. + * + * @returns A JSON-serializable representation of this object. + */ public toJSON(): object { const json: any = { uid: this.uid, @@ -205,11 +659,15 @@ export class UserRecord { passwordSalt: this.passwordSalt, customClaims: deepCopy(this.customClaims), tokensValidAfterTime: this.tokensValidAfterTime, + tenantId: this.tenantId, }; + if (this.multiFactor) { + json.multiFactor = this.multiFactor.toJSON(); + } json.providerData = []; for (const entry of this.providerData) { - // Convert each provider data to json. - json.providerData.push(entry.toJSON()); + // Convert each provider data to json. + json.providerData.push(entry.toJSON()); } return json; } diff --git a/src/credential/index.ts b/src/credential/index.ts new file mode 100644 index 0000000000..f83adcc148 --- /dev/null +++ b/src/credential/index.ts @@ -0,0 +1,138 @@ +/*! + * 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 { + Credential as TCredential, + applicationDefault as applicationDefaultFn, + cert as certFn, + refreshToken as refreshTokenFn, +} from '../app/index'; + +export { ServiceAccount, GoogleOAuthAccessToken } from '../app/index'; + +/* eslint-disable @typescript-eslint/no-namespace */ +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 the `admin.credential` namespace. + */ + export type Credential = TCredential; + + /** + * 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 firebase-admin.app#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 httpAgent - Optional {@link https://nodejs.org/api/http.html#http_class_http_agent | HTTP Agent} + * to be used when retrieving access tokens from Google token servers. + * + * @returns A credential authenticated via Google + * Application Default Credentials that can be used to initialize an app. + */ + export const applicationDefault = applicationDefaultFn; + + /** + * 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 firebase-admin.app#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 {@link https://nodejs.org/api/http.html#http_class_http_agent | HTTP Agent} + * to be used when retrieving access tokens from Google token servers. + * + * @returns A credential authenticated via the + * provided service account that can be used to initialize an app. + */ + export const cert = certFn; + + /** + * 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 firebase-admin.app#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 {@link https://nodejs.org/api/http.html#http_class_http_agent | HTTP Agent} + * to be used when retrieving access tokens from Google token servers. + * + * @returns A credential authenticated via the + * provided service account that can be used to initialize an app. + */ + export const refreshToken = refreshTokenFn; +} diff --git a/src/database/database-namespace.ts b/src/database/database-namespace.ts new file mode 100644 index 0000000000..0bb190bc27 --- /dev/null +++ b/src/database/database-namespace.ts @@ -0,0 +1,107 @@ +/*! + * 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 rtdb from '@firebase/database-types'; +import { App } from '../app'; +import { Database as TDatabase } from './database'; + +/** + * Gets the {@link firebase-admin.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 `Database` service or as `admin.database(app)` to access the + * `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. + * + * @returns The default `Database` service if no app + * is provided or the `Database` service associated with the provided app. + */ +export declare function database(app?: App): database.Database; + +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace database { + /** + * Type alias to {@link firebase-admin.database#Database}. + */ + export type Database = TDatabase; + + /** + * Type alias to {@link https://firebase.google.com/docs/reference/js/v8/firebase.database.DataSnapshot | DataSnapshot} + * type from the `@firebase/database-compat` package. + */ + export type DataSnapshot = rtdb.DataSnapshot; + + /** + * Type alias to the {@link https://firebase.google.com/docs/reference/js/v8/firebase.database#eventtype | EventType} + * type from the `@firebase/database-compat` package. + */ + export type EventType = rtdb.EventType; + + /** + * Type alias to {@link https://firebase.google.com/docs/reference/js/v8/firebase.database.OnDisconnect | OnDisconnect} + * type from the `@firebase/database-compat` package. + */ + export type OnDisconnect = rtdb.OnDisconnect; + + /** + * Type alias to {@link https://firebase.google.com/docs/reference/js/v8/firebase.database.Query | Query} + * type from the `@firebase/database-compat` package. + */ + export type Query = rtdb.Query; + + /** + * Type alias to {@link https://firebase.google.com/docs/reference/js/v8/firebase.database.Reference | Reference} + * type from the `@firebase/database-compat` package. + */ + export type Reference = rtdb.Reference; + + /** + * Type alias to {@link https://firebase.google.com/docs/reference/js/v8/firebase.database.ThenableReference | + * ThenableReference} type from the `@firebase/database-compat` package. + */ + export type ThenableReference = rtdb.ThenableReference; + + /** + * {@link https://firebase.google.com/docs/reference/js/v8/firebase.database#enablelogging | enableLogging} + * function from the `@firebase/database-compat` package. + */ + export declare const enableLogging: typeof rtdb.enableLogging; + + /** + * {@link https://firebase.google.com/docs/reference/js/v8/firebase.database.ServerValue | ServerValue} + * constant from the `@firebase/database-compat` package. + */ + // eslint-disable-next-line @typescript-eslint/naming-convention + export declare const ServerValue: rtdb.ServerValue; +} diff --git a/src/database/database.ts b/src/database/database.ts index 8ca2741f10..555072f2df 100644 --- a/src/database/database.ts +++ b/src/database/database.ts @@ -1,41 +1,77 @@ -import {FirebaseApp} from '../firebase-app'; -import {FirebaseDatabaseError} from '../utils/error'; -import {FirebaseServiceInterface, FirebaseServiceInternalsInterface} from '../firebase-service'; -import {Database} from '@firebase/database'; +/*! + * 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 validator from '../utils/validator'; +import { URL } from 'url'; +import * as path from 'path'; + +import { FirebaseDatabase } from '@firebase/database-types'; +import { FirebaseDatabaseError, AppErrorCodes, FirebaseAppError } from '../utils/error'; +import { Database as DatabaseImpl } from '@firebase/database-compat/standalone'; +import { App } from '../app'; +import { FirebaseApp } from '../app/firebase-app'; +import * as validator from '../utils/validator'; +import { AuthorizedHttpClient, HttpRequestConfig, HttpError } from '../utils/api-request'; +import { getSdkVersion } from '../utils/index'; /** - * Internals of a Database instance. + * The Firebase Database service interface. Extends the + * {@link https://firebase.google.com/docs/reference/js/v8/firebase.database.Database | Database} + * interface provided by the `@firebase/database-compat` package. */ -class DatabaseInternals implements FirebaseServiceInternalsInterface { +export interface Database extends FirebaseDatabase { + /** + * Gets the currently applied security rules as a string. The return value consists of + * the rules source including comments. + * + * @returns A promise fulfilled with the rules as a raw string. + */ + getRules(): Promise; - public databases: { - [dbUrl: string]: Database, - } = {}; + /** + * Gets the currently applied security rules as a parsed JSON object. Any comments in + * the original source are stripped away. + * + * @returns A promise fulfilled with the parsed rules object. + */ + getRulesJSON(): Promise; /** - * Deletes the service and its associated resources. + * 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. * - * @return {Promise<()>} An empty Promise that will be fulfilled when the service is deleted. + * @param source - Source of the rules to apply. Must not be `null` or empty. + * @returns Resolves when the rules are set on the Realtime Database. */ - public delete(): Promise { - for (const dbUrl of Object.keys(this.databases)) { - const db: Database = this.databases[dbUrl]; - db.INTERNAL.delete(); - } - return Promise.resolve(undefined); - } + setRules(source: string | Buffer | object): Promise; } -export class DatabaseService implements FirebaseServiceInterface { +const TOKEN_REFRESH_THRESHOLD_MILLIS = 5 * 60 * 1000; + +export class DatabaseService { - public INTERNAL: DatabaseInternals = new DatabaseInternals(); + private readonly appInternal: App; + private tokenListener: (token: string) => void; + private tokenRefreshTimeout: NodeJS.Timeout; - private appInternal: FirebaseApp; + private databases: { + [dbUrl: string]: Database; + } = {}; - constructor(app: FirebaseApp) { + constructor(app: App) { if (!validator.isNonNullObject(app) || !('options' in app)) { throw new FirebaseDatabaseError({ code: 'invalid-argument', @@ -45,12 +81,35 @@ export class DatabaseService implements FirebaseServiceInterface { this.appInternal = app; } + private get firebaseApp(): FirebaseApp { + return this.app as FirebaseApp; + } + + /** + * @internal + */ + public delete(): Promise { + if (this.tokenListener) { + this.firebaseApp.INTERNAL.removeAuthTokenListener(this.tokenListener); + clearTimeout(this.tokenRefreshTimeout); + } + + 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. * - * @return {FirebaseApp} The app associated with this DatabaseService instance. + * @returns The app associated with this DatabaseService instance. */ - get app(): FirebaseApp { + get app(): App { return this.appInternal; } @@ -63,16 +122,59 @@ 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'); - const { version } = require('../../package.json'); - db = rtdb.initStandalone(this.appInternal, dbUrl, version).instance; - this.INTERNAL.databases[dbUrl] = db; + // eslint-disable-next-line @typescript-eslint/no-var-requires + const rtdb = require('@firebase/database-compat/standalone'); + 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.databases[dbUrl] = db; } + + if (!this.tokenListener) { + this.tokenListener = this.onTokenChange.bind(this); + this.firebaseApp.INTERNAL.addAuthTokenListener(this.tokenListener); + } + return db; } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + private onTokenChange(_: string): void { + const token = this.firebaseApp.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 { + clearTimeout(this.tokenRefreshTimeout); + this.tokenRefreshTimeout = setTimeout(() => { + this.firebaseApp.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; @@ -85,3 +187,142 @@ export class DatabaseService implements FirebaseServiceInterface { }); } } + +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: App, dbUrl: string) { + 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 as FirebaseApp); + } + + /** + * Gets the currently applied security rules as a string. The return value consists of + * the rules source including comments. + * + * @returns 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. + * + * @returns {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. + * @returns {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}`; + } +} + +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/src/database/index.ts b/src/database/index.ts new file mode 100644 index 0000000000..7a17deb751 --- /dev/null +++ b/src/database/index.ts @@ -0,0 +1,129 @@ +/*! + * 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. + */ + +/** + * Firebase Realtime Database. + * + * @packageDocumentation + */ + +import * as rtdb from '@firebase/database-types'; +import { + enableLogging as enableLoggingFunc, + ServerValue as serverValueConst, +} from '@firebase/database-compat/standalone'; + +import { App, getApp } from '../app'; +import { FirebaseApp } from '../app/firebase-app'; +import { Database, DatabaseService } from './database'; + +export { Database }; +export { + DataSnapshot, + EventType, + OnDisconnect, + Query, + Reference, + ThenableReference, +} from '@firebase/database-types'; + +// TODO: Remove the following any-cast once the typins in @firebase/database-types are fixed. + +/** + * {@link https://firebase.google.com/docs/reference/js/v8/firebase.database#enablelogging | enableLogging} + * function from the `@firebase/database-compat` package. + */ +export const enableLogging: typeof rtdb.enableLogging = enableLoggingFunc as any; + +/** + * {@link https://firebase.google.com/docs/reference/js/v8/firebase.database.ServerValue | ServerValue} + * constant from the `@firebase/database-compat` package. + */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export const ServerValue: rtdb.ServerValue = serverValueConst; + +/** + * Gets the {@link Database} service for the default + * app or a given app. + * + * `getDatabase()` can be called with no arguments to access the default + * app's `Database` service or as `getDatabase(app)` to access the + * `Database` service associated with a specific app. + * + * @example + * ```javascript + * // Get the Database service for the default app + * const defaultDatabase = getDatabase(); + * ``` + * + * @example + * ```javascript + * // Get the Database service for a specific app + * const otherDatabase = getDatabase(app); + * ``` + * + * @param App - whose `Database` service to + * return. If not provided, the default `Database` service will be returned. + * + * @returns The default `Database` service if no app + * is provided or the `Database` service associated with the provided app. + */ +export function getDatabase(app?: App): Database { + return getDatabaseInstance({ app }); +} + +/** + * Gets the {@link Database} service for the default + * app or a given app. + * + * `getDatabaseWithUrl()` can be called with no arguments to access the default + * app's {@link Database} service or as `getDatabaseWithUrl(app)` to access the + * {@link Database} service associated with a specific app. + * + * @example + * ```javascript + * // Get the Database service for the default app + * const defaultDatabase = getDatabaseWithUrl('https://example.firebaseio.com'); + * ``` + * + * @example + * ```javascript + * // Get the Database service for a specific app + * const otherDatabase = getDatabaseWithUrl('https://example.firebaseio.com', app); + * ``` + * + * @param App - whose `Database` service to + * return. If not provided, the default `Database` service will be returned. + * + * @returns The default `Database` service if no app + * is provided or the `Database` service associated with the provided app. + */ +export function getDatabaseWithUrl(url: string, app?: App): Database { + return getDatabaseInstance({ url, app }); +} + +function getDatabaseInstance(options: { url?: string; app?: App }): Database { + let { app } = options; + if (typeof app === 'undefined') { + app = getApp(); + } + + const firebaseApp: FirebaseApp = app as FirebaseApp; + const dbService = firebaseApp.getOrInitService('database', (app) => new DatabaseService(app)); + return dbService.getDatabase(options.url); +} + +export { FirebaseDatabaseError } from '../utils/error'; diff --git a/src/default-namespace.d.ts b/src/default-namespace.d.ts new file mode 100644 index 0000000000..5405cc67c6 --- /dev/null +++ b/src/default-namespace.d.ts @@ -0,0 +1,23 @@ +/*! + * 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. + */ + +/** + * Firebase namespaced API (legacy). + * + * @packageDocumentation + */ + +export * from './firebase-namespace-api'; diff --git a/src/default-namespace.ts b/src/default-namespace.ts index e82e6b6b7a..12e855e2d8 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"); @@ -14,9 +15,7 @@ * limitations under the License. */ -import {FirebaseNamespace} from './firebase-namespace'; - -const firebaseAdmin = new FirebaseNamespace(); +import { defaultNamespace as firebaseAdmin } from './app/firebase-namespace'; // Inject a circular default export to allow users to use both: // diff --git a/src/eventarc/cloudevent.ts b/src/eventarc/cloudevent.ts new file mode 100644 index 0000000000..9fa0749f2e --- /dev/null +++ b/src/eventarc/cloudevent.ts @@ -0,0 +1,95 @@ + +/*! + * @license + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * A CloudEvent version. + */ +export type CloudEventVersion = '1.0'; + +/** + * A CloudEvent describes event data. + * + * @see https://github.com/cloudevents/spec/blob/v1.0/spec.md + */ +export interface CloudEvent { + + /** + * Identifier for the event. If not provided, it is auto-populated with a UUID. + * + * @see https://github.com/cloudevents/spec/blob/v1.0/spec.md#id + */ + id?: string; + + /** + * Identifies the context in which an event happened. If not provided, the value of `EVENTARC_CLOUD_EVENT_SOURCE` + * environment variable is used and if that is not set, a validation error is thrown. + * + * @see https://github.com/cloudevents/spec/blob/v1.0/spec.md#source-1 + */ + source?: string; + + /** + * The version of the CloudEvents specification which the event uses. If not provided, is set to `1.0` -- + * the only supported value. + * + * @see https://github.com/cloudevents/spec/blob/v1.0/spec.md#specversion + */ + specversion?: CloudEventVersion; + + /** + * Type of the event. Should be prefixed with a reverse-DNS name (`com.my-org.v1.something.happended`). + * + * @see https://github.com/cloudevents/spec/blob/v1.0/spec.md#type + */ + type: string; + + /** + * Subject (context) of the event in the context of the event producer. + * + * @see https://github.com/cloudevents/spec/blob/v1.0/spec.md#subject + */ + subject?: string; + + /** + * MIME type of the data being sent with the event in the `data` field. Only `application/json` and `text/plain` + * are currently supported. If not specified, it is automatically inferred from the type of provided data. + * + * @see https://github.com/cloudevents/spec/blob/v1.0/spec.md#datacontenttype + */ + datacontenttype?: string; + + /** + * Timestamp of the event. Must be in ISO time format. If not specified, current time (at the moment of publishing) + * is used. + * + * @see https://github.com/cloudevents/spec/blob/v1.0/spec.md#time + */ + time?: string; + + /** + * Data payload of the event. Objects are stringified with JSON and strings are be passed along as-is. + */ + data?: object | string; + + /** + * Custom attributes/extensions. Must be strings. Added to the event as is. + * + * @see https://github.com/cloudevents/spec/blob/v1.0/spec.md#extension-context-attributes + */ + [key: string]: any; + } diff --git a/src/eventarc/eventarc-client-internal.ts b/src/eventarc/eventarc-client-internal.ts new file mode 100644 index 0000000000..ce02967c93 --- /dev/null +++ b/src/eventarc/eventarc-client-internal.ts @@ -0,0 +1,160 @@ +/*! + * @license + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as validator from '../utils/validator'; +import { FirebaseEventarcError, toCloudEventProtoFormat } from './eventarc-utils'; +import { App } from '../app'; +import { Channel } from './eventarc'; +import { + HttpRequestConfig, HttpClient, HttpError, AuthorizedHttpClient +} from '../utils/api-request'; +import { FirebaseApp } from '../app/firebase-app'; +import * as utils from '../utils'; +import { PrefixedFirebaseError } from '../utils/error'; +import { CloudEvent } from './cloudevent'; + +const EVENTARC_API = 'https://eventarcpublishing.googleapis.com/v1'; +const FIREBASE_VERSION_HEADER = { + 'X-Firebase-Client': `fire-admin-node/${utils.getSdkVersion()}`, +}; +const CHANNEL_NAME_REGEX = /^(projects\/([^/]+)\/)?locations\/([^/]+)\/channels\/([^/]+)$/; +const DEFAULT_CHANNEL_REGION = 'us-central1'; + +/** + * Class that facilitates sending requests to the Eventarc backend API. + * + * @internal + */ +export class EventarcApiClient { + private readonly httpClient: HttpClient; + private projectId?: string; + private readonly resolvedChannelName: Promise; + + constructor(private readonly app: App, private readonly channel: Channel) { + if (!validator.isNonNullObject(app) || !('options' in app)) { + throw new FirebaseEventarcError( + 'invalid-argument', + 'First argument passed to Channel() must be a valid Eventarc service instance.'); + } + this.httpClient = new AuthorizedHttpClient(app as FirebaseApp); + this.resolvedChannelName = this.resolveChannelName(channel.name); + } + + private getProjectId(): Promise { + if (this.projectId) { + return Promise.resolve(this.projectId); + } + return utils.findProjectId(this.app) + .then((projectId) => { + if (!validator.isNonEmptyString(projectId)) { + throw new FirebaseEventarcError( + 'unknown-error', + 'Failed to determine project ID. Initialize the ' + + 'SDK with service account credentials or set project ID as an app option. ' + + 'Alternatively, set the GOOGLE_CLOUD_PROJECT environment variable.'); + } + this.projectId = projectId; + return projectId; + }); + } + + /** + * Publishes provided events to this channel. If channel was created with `allowedEventsTypes` and event type + * is not on that list, the event is ignored. + * + * The following CloudEvent fields are auto-populated if not set: + * * specversion - `1.0` + * * id - uuidv4() + * * source - populated with `process.env.EVENTARC_CLOUD_EVENT_SOURCE` and + * if not set an error is thrown. + * + * @param events - CloudEvent to publish to the channel. + */ + public async publish(events: CloudEvent | CloudEvent[]): Promise { + if (!Array.isArray(events)) { + events = [events as CloudEvent]; + } + return this.publishToEventarcApi( + await this.resolvedChannelName, + events + .filter(e => typeof this.channel.allowedEventTypes === 'undefined' || + this.channel.allowedEventTypes.includes(e.type)) + .map(toCloudEventProtoFormat)); + } + + private async publishToEventarcApi(channel:string, events: CloudEvent[]): Promise { + if (events.length === 0) { + return; + } + const request: HttpRequestConfig = { + method: 'POST', + url: `${this.getEventarcHost()}/${channel}:publishEvents`, + data: JSON.stringify({ events }), + }; + return this.sendRequest(request); + } + + private sendRequest(request: HttpRequestConfig): Promise { + request.headers = FIREBASE_VERSION_HEADER; + return this.httpClient.send(request) + .then(() => undefined) + .catch((err) => { + throw this.toFirebaseError(err); + }); + } + + private toFirebaseError(err: HttpError): PrefixedFirebaseError { + if (err instanceof PrefixedFirebaseError) { + return err; + } + + const response = err.response; + return new FirebaseEventarcError( + 'unknown-error', + `Unexpected response with status: ${response.status} and body: ${response.text}`); + } + + private resolveChannelName(name: string): Promise { + if (!name.includes('/')) { + const location = DEFAULT_CHANNEL_REGION; + const channelId = name; + return this.resolveChannelNameProjectId(location, channelId); + } else { + const match = CHANNEL_NAME_REGEX.exec(name); + if (match === null || match.length < 4) { + throw new FirebaseEventarcError('invalid-argument', 'Invalid channel name format.'); + } + const projectId = match[2]; + const location = match[3]; + const channelId = match[4]; + if (validator.isNonEmptyString(projectId)) { + return Promise.resolve(`projects/${projectId}/locations/${location}/channels/${channelId}`); + } else { + return this.resolveChannelNameProjectId(location, channelId); + } + } + } + + private async resolveChannelNameProjectId(location: string, channelId: string): Promise { + const projectId = await this.getProjectId(); + return `projects/${projectId}/locations/${location}/channels/${channelId}`; + } + + private getEventarcHost(): string { + return process.env.CLOUD_EVENTARC_EMULATOR_HOST ?? EVENTARC_API; + } +} diff --git a/src/eventarc/eventarc-utils.ts b/src/eventarc/eventarc-utils.ts new file mode 100644 index 0000000000..6bf6531285 --- /dev/null +++ b/src/eventarc/eventarc-utils.ts @@ -0,0 +1,138 @@ +/*! + * @license + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { PrefixedFirebaseError } from '../utils/error'; +import { CloudEvent } from './cloudevent'; +import { v4 as uuid } from 'uuid'; +import * as validator from '../utils/validator'; + +// List of CloudEvent properties that are handled "by hand" and should be skipped by +// automatic attribute copy. +const TOP_LEVEL_CE_ATTRS: string[] = + ['id', 'type', 'specversion', 'source', 'data', 'time', 'datacontenttype', 'subject']; + +export type EventarcErrorCode = 'unknown-error' | 'invalid-argument' + +/** + * Firebase Eventarc error code structure. This extends PrefixedFirebaseError. + * + * @param code - The error code. + * @param message - The error message. + * @constructor + */ +export class FirebaseEventarcError extends PrefixedFirebaseError { + constructor(code: EventarcErrorCode, message: string) { + super('eventarc', code, message); + } +} + +export function toCloudEventProtoFormat(ce: CloudEvent): any { + const source = ce.source ?? process.env.EVENTARC_CLOUD_EVENT_SOURCE; + if (typeof source === 'undefined' || !validator.isNonEmptyString(source)) { + throw new FirebaseEventarcError('invalid-argument', "CloudEvent 'source' is required."); + } + if (!validator.isNonEmptyString(ce.type)) { + throw new FirebaseEventarcError('invalid-argument', "CloudEvent 'type' is required."); + } + const out: Record = { + '@type': 'type.googleapis.com/io.cloudevents.v1.CloudEvent', + 'id': ce.id ?? uuid(), + 'type': ce.type, + 'specVersion': ce.specversion ?? '1.0', + 'source': source + } + + if (typeof ce.time !== 'undefined') { + if (!validator.isISODateString(ce.time)) { + throw new FirebaseEventarcError( + 'invalid-argument', "CloudEvent 'tyme' must be in ISO date format."); + } + setAttribute(out, 'time', { + 'ceTimestamp': ce.time + }); + } else { + setAttribute(out, 'time', { + 'ceTimestamp': new Date().toISOString() + }); + } + if (typeof ce.datacontenttype !== 'undefined') { + if (!validator.isNonEmptyString(ce.datacontenttype)) { + throw new FirebaseEventarcError( + 'invalid-argument', + "CloudEvent 'datacontenttype' if specified must be non-empty string."); + } + setAttribute(out, 'datacontenttype', { + 'ceString': ce.datacontenttype + }); + } + if (ce.subject) { + if (!validator.isNonEmptyString(ce.subject)) { + throw new FirebaseEventarcError( + 'invalid-argument', + "CloudEvent 'subject' if specified must be non-empty string."); + } + setAttribute(out, 'subject', { + 'ceString': ce.subject + }); + } + + if (typeof ce.data === 'undefined') { + throw new FirebaseEventarcError('invalid-argument', "CloudEvent 'data' is required."); + } + if (validator.isObject(ce.data)) { + out['textData'] = JSON.stringify(ce.data); + if (!ce.datacontenttype) { + setAttribute(out, 'datacontenttype', { + 'ceString': 'application/json' + }); + } + } else if (validator.isNonEmptyString(ce.data)) { + out['textData'] = ce.data; + if (!ce.datacontenttype) { + setAttribute(out, 'datacontenttype', { + 'ceString': 'text/plain' + }); + } + } else { + throw new FirebaseEventarcError( + 'invalid-argument', + `CloudEvent 'data' must be string or an object (which are converted to JSON), got '${typeof ce.data}'.`); + } + + for (const attr in ce) { + if (TOP_LEVEL_CE_ATTRS.includes(attr)) { + continue; + } + if (!validator.isNonEmptyString(ce[attr])) { + throw new FirebaseEventarcError( + 'invalid-argument', + `CloudEvent extension attributes ('${attr}') must be string.`); + } + setAttribute(out, attr, { + 'ceString': ce[attr] + }); + } + + return out; +} + +function setAttribute(event: any, attr: string, value: any): void { + if (!Object.prototype.hasOwnProperty.call(event, 'attributes')) { + event.attributes = {}; + } + event['attributes'][attr] = value; +} diff --git a/src/eventarc/eventarc.ts b/src/eventarc/eventarc.ts new file mode 100644 index 0000000000..a1edc4a011 --- /dev/null +++ b/src/eventarc/eventarc.ts @@ -0,0 +1,194 @@ +/*! + * @license + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { App } from '../app'; +import * as validator from '../utils/validator'; +import { FirebaseEventarcError } from './eventarc-utils'; +import { CloudEvent } from './cloudevent'; +import { EventarcApiClient } from './eventarc-client-internal'; + +/** + * Channel options interface. + */ +export interface ChannelOptions { + /** + * An array of allowed event types. If specified, publishing events of + * unknown types is a no op. When not provided, no event filtering is + * performed. + */ + allowedEventTypes?: string[] | string | undefined +} + +/** + * Eventarc service bound to the provided app. + */ +export class Eventarc { + + private readonly appInternal: App; + + /** + * @internal + */ + constructor(app: App) { + if (!validator.isNonNullObject(app) || !('options' in app)) { + throw new FirebaseEventarcError( + 'invalid-argument', + 'First argument passed to Eventarc() must be a valid Firebase app instance.', + ); + } + + this.appInternal = app; + } + + /** + * The {@link firebase-admin.app#App} associated with the current Eventarc service + * instance. + * + * @example + * ```javascript + * var app = eventarc.app; + * ``` + */ + get app(): App { + return this.appInternal; + } + + /** + * Creates a reference to the Eventarc channel using the provided channel resource name. + * The channel resource name can be either: + * + * - A fully qualified channel resource name: + * `projects/{project}/locations/{location}/channels/{channel-id}` + * + * - A partial resource name with location and channel ID, in which case + * the runtime project ID of the function is used: + * `locations/{location}/channels/{channel-id}` + * + * - A partial channel ID, in which case the runtime project ID of the + * function and `us-central1` as location is used: + * `{channel-id}` + * + * @param name - Channel resource name. + * @param options - (optional) additional channel options + * @returns An Eventarc channel reference for publishing events. + */ + public channel(name: string, options?: ChannelOptions): Channel; + + /** + * Create a reference to the default Firebase channel: + * `locations/us-central1/channels/firebase` + * + * @param options - (optional) additional channel options + * @returns Eventarc channel reference for publishing events. + */ + public channel(options?: ChannelOptions): Channel; + + public channel(nameOrOptions?: string | ChannelOptions, options?: ChannelOptions): Channel { + let channel: string; + let opts: ChannelOptions; + if (validator.isNonEmptyString(nameOrOptions)) { + channel = nameOrOptions; + } else { + channel = 'locations/us-central1/channels/firebase'; + } + + if (validator.isNonNullObject(nameOrOptions)) { + opts = nameOrOptions as ChannelOptions; + } else { + opts = options as ChannelOptions; + } + let allowedEventTypes : string[] | undefined = undefined; + if (typeof opts?.allowedEventTypes === 'string') { + allowedEventTypes = opts.allowedEventTypes.split(','); + } else if (validator.isArray(opts?.allowedEventTypes)) { + allowedEventTypes = opts?.allowedEventTypes as string[]; + } else if (typeof opts?.allowedEventTypes !== 'undefined') { + throw new FirebaseEventarcError( + 'invalid-argument', + 'AllowedEventTypes must be either an array of strings or a comma separated string.', + ); + } + return new Channel(this, channel, allowedEventTypes); + } +} + +/** + * Eventarc Channel. + */ +export class Channel { + private readonly eventarcInternal: Eventarc; + private nameInternal: string; + + /** + * List of event types allowed by this channel for publishing. Other event types are ignored. + */ + public readonly allowedEventTypes?: string[] + + private readonly client: EventarcApiClient; + + /** + * @internal + */ + constructor(eventarc: Eventarc, name: string, allowedEventTypes?: string[]) { + if (!validator.isNonNullObject(eventarc)) { + throw new FirebaseEventarcError( + 'invalid-argument', + 'First argument passed to Channel() must be a valid Eventarc service instance.', + ); + } + if (!validator.isNonEmptyString(name)) { + throw new FirebaseEventarcError( + 'invalid-argument', 'name is required.', + ); + } + + this.nameInternal = name; + this.eventarcInternal = eventarc; + this.allowedEventTypes = allowedEventTypes; + this.client = new EventarcApiClient(eventarc.app, this); + } + + /** + * The {@link firebase-admin.eventarc#Eventarc} service instance associated with the current `Channel`. + * + * @example + * ```javascript + * var app = channel.eventarc; + * ``` + */ + get eventarc(): Eventarc { + return this.eventarcInternal; + } + + /** + * The channel name as provided during channel creation. If it was not specifed, the default channel name is returned + * ('locations/us-central1/channels/firebase'). + */ + get name(): string { + return this.nameInternal; + } + + /** + * Publishes provided events to this channel. If channel was created with `allowedEventTypes` and event type is not + * on that list, the event is ignored. + * + * @param events - CloudEvent to publish to the channel. + */ + public publish(events: CloudEvent | CloudEvent[]): Promise { + return this.client.publish(events); + } +} diff --git a/src/eventarc/index.ts b/src/eventarc/index.ts new file mode 100644 index 0000000000..d1e6fc79bd --- /dev/null +++ b/src/eventarc/index.ts @@ -0,0 +1,65 @@ +/*! + * @license + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Firebase Eventarc. + * + * @packageDocumentation + */ + +import { App, getApp } from '../app'; +import { FirebaseApp } from '../app/firebase-app'; + +import { Eventarc } from './eventarc'; + +export { CloudEvent, CloudEventVersion } from './cloudevent'; +export { Eventarc, Channel, ChannelOptions } from './eventarc'; + +/** + * Gets the {@link Eventarc} service for the default app or a given app. + * + * `getEventarc()` can be called with no arguments to access the default + * app's `Eventarc` service or as `getEventarc(app)` to access the + * `Eventarc` service associated with specific app. + * + * @example + * ```javascript + * // Get the Eventarc service for the default app + * const defaultEventarc = getEventarc(); + * ``` + * + * @example + * ```javascript + * // Get the Eventarc service for a given app + * const otherEventarc = getEventarc(otherApp); + * ``` + * + * @param app - Optional app whose `Eventarc` service will be returned. + * If not provided, the default `Eventarc` service will be returned. + * + * @returns The default `Eventarc` service if no + * app is provided or the `Eventarc` service associated with the provided + * app. + */ +export function getEventarc(app?: App): Eventarc { + if (typeof app === 'undefined') { + app = getApp(); + } + + const firebaseApp: FirebaseApp = app as FirebaseApp; + return firebaseApp.getOrInitService('eventarc', (app) => new Eventarc(app)); +} diff --git a/src/extensions/extensions-api-client-internal.ts b/src/extensions/extensions-api-client-internal.ts new file mode 100644 index 0000000000..407dd5a82e --- /dev/null +++ b/src/extensions/extensions-api-client-internal.ts @@ -0,0 +1,150 @@ +/*! + * @license + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { App } from '../app'; +import { FirebaseApp } from '../app/firebase-app'; +import { AuthorizedHttpClient, HttpClient, HttpError, HttpRequestConfig } from '../utils/api-request'; +import { FirebaseAppError, PrefixedFirebaseError } from '../utils/error'; +import * as validator from '../utils/validator'; +import * as utils from '../utils'; + +const FIREBASE_FUNCTIONS_CONFIG_HEADERS = { + 'X-Firebase-Client': `fire-admin-node/${utils.getSdkVersion()}` +}; +const EXTENSIONS_API_VERSION = 'v1beta'; +// Note - use getExtensionsApiUri() instead so that changing environments is consistent. +const EXTENSIONS_URL = 'https://firebaseextensions.googleapis.com'; + +/** + * Class that facilitates sending requests to the Firebase Extensions backend API. + * + * @internal + */ +export class ExtensionsApiClient { + private readonly httpClient: HttpClient; + + constructor(private readonly app: App) { + if (!validator.isNonNullObject(app) || !('options' in app)) { + throw new FirebaseAppError( + 'invalid-argument', + 'First argument passed to getExtensions() must be a valid Firebase app instance.'); + } + this.httpClient = new AuthorizedHttpClient(this.app as FirebaseApp); + } + + async updateRuntimeData( + projectId: string, + instanceId: string, + runtimeData: RuntimeData + ): Promise { + const url = this.getRuntimeDataUri(projectId, instanceId); + const request: HttpRequestConfig = { + method: 'PATCH', + url, + headers: FIREBASE_FUNCTIONS_CONFIG_HEADERS, + data: runtimeData, + }; + try { + const res = await this.httpClient.send(request); + return res.data + } catch (err: any) { + throw this.toFirebaseError(err); + } + } + + private getExtensionsApiUri(): string { + return process.env['FIREBASE_EXT_URL'] ?? EXTENSIONS_URL; + } + + private getRuntimeDataUri(projectId: string, instanceId: string): string { + return `${ + this.getExtensionsApiUri() + }/${EXTENSIONS_API_VERSION}/projects/${projectId}/instances/${instanceId}/runtimeData`; + } + + private toFirebaseError(err: HttpError): PrefixedFirebaseError { + if (err instanceof PrefixedFirebaseError) { + return err; + } + + const response = err.response; + if (!response?.isJson()) { + return new FirebaseExtensionsError( + 'unknown-error', + `Unexpected response with status: ${response.status} and body: ${response.text}`); + } + const error = response.data?.error; + const message = error?.message || `Unknown server error: ${response.text}`; + switch (error.code) { + case 403: + return new FirebaseExtensionsError('forbidden', message); + case 404: + return new FirebaseExtensionsError('not-found', message); + case 500: + return new FirebaseExtensionsError('internal-error', message); + } + return new FirebaseExtensionsError('unknown-error', message); + } +} + +interface RuntimeData { + + //oneof + processingState?: ProcessingState; + fatalError?: FatalError; +} + +interface RuntimeDataResponse extends RuntimeData{ + name: string, + updateTime: string, +} + +interface FatalError { + errorMessage: string; +} + +interface ProcessingState { + detailMessage: string; + state: State; +} + +type State = 'STATE_UNSPECIFIED' | + 'NONE' | + 'PROCESSING' | + 'PROCESSING_COMPLETE' | + 'PROCESSING_WARNING' | + 'PROCESSING_FAILED'; + +type ExtensionsErrorCode = 'invalid-argument' | 'not-found' | 'forbidden' | 'internal-error' | 'unknown-error'; +/** + * Firebase Extensions error code structure. This extends PrefixedFirebaseError. + * + * @param code - The error code. + * @param message - The error message. + * @constructor + */ +export class FirebaseExtensionsError extends PrefixedFirebaseError { + constructor(code: ExtensionsErrorCode, message: string) { + super('Extensions', 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__ = FirebaseExtensionsError.prototype; + } +} diff --git a/src/extensions/extensions-api.ts b/src/extensions/extensions-api.ts new file mode 100644 index 0000000000..ce0c18f89d --- /dev/null +++ b/src/extensions/extensions-api.ts @@ -0,0 +1,44 @@ +/*! + * @license + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * `SettableProcessingState` represents all the processing states that can be set + * on an Extension instance's runtime data. + * + * @remarks + * You can set the following states: + * + * - `NONE`: No relevant lifecycle event work has been done. + * Set this to clear out old statuses. + * + * - `PROCESSING_COMPLETE`: Lifecycle event work completed with no errors. + * + * - `PROCESSING_WARNING`: Lifecycle event work succeeded partially, or + * something happened that the user should be warned about. + * + * - `PROCESSING_FAILED`: Lifecycle event work failed completely, but the + * instance will still work correctly going forward. + * + * If the extension instance is in a broken state due to errors, instead call + * {@link Runtime.setFatalError}. + * + * The "processing" state gets set automatically when a lifecycle event handler + * starts; you can't set it explicitly. + * To report the ongoing status of an extension's function, use `console.log` + * or the Cloud Functions logger SDK. + */ +export type SettableProcessingState = 'NONE' | 'PROCESSING_COMPLETE' | 'PROCESSING_WARNING' | 'PROCESSING_FAILED'; diff --git a/src/extensions/extensions.ts b/src/extensions/extensions.ts new file mode 100644 index 0000000000..e16f893aa0 --- /dev/null +++ b/src/extensions/extensions.ts @@ -0,0 +1,147 @@ +/*! + * @license + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { App } from '../app'; +import { SettableProcessingState } from './extensions-api'; +import { ExtensionsApiClient, FirebaseExtensionsError } from './extensions-api-client-internal'; +import * as validator from '../utils/validator'; + +/** + * The Firebase `Extensions` service interface. + */ +export class Extensions { + private readonly client: ExtensionsApiClient; + /** + * @param app - The app for this `Extensions` service. + * @constructor + * @internal + */ + constructor(readonly app: App) { + this.client = new ExtensionsApiClient(app); + } + + /** + * The runtime() method returns a new Runtime, which provides methods to modify an extension instance's runtime data. + * + * @remarks + * This method will throw an error if called outside an Extensions environment. + * + * @returns A new {@link Runtime} object. + */ + public runtime(): Runtime { + return new Runtime(this.client); + } +} + +/** + * Runtime provides methods to modify an extension instance's runtime data. + */ +export class Runtime { + private projectId: string; + private extensionInstanceId: string; + private readonly client: ExtensionsApiClient; + /** + * @param client - The API client for this `Runtime` service. + * @constructor + * @internal + */ + constructor(client: ExtensionsApiClient) { + this.projectId = this.getProjectId(); + if (!validator.isNonEmptyString(process.env['EXT_INSTANCE_ID'])) { + throw new FirebaseExtensionsError( + 'invalid-argument', + 'Runtime is only available from within a running Extension instance.' + ); + } + this.extensionInstanceId = process.env['EXT_INSTANCE_ID']; + if (!validator.isNonNullObject(client) || !('updateRuntimeData' in client)) { + throw new FirebaseExtensionsError( + 'invalid-argument', + 'Must provide a valid ExtensionsApiClient instance to create a new Runtime.'); + } + this.client = client; + } + + /** + * Sets the processing state of an extension instance. + * + * @remarks + * Use this method to report the results of a lifecycle event handler. + * + * If the lifecycle event failed & the extension instance will no longer work + * correctly, use {@link Runtime.setFatalError} instead. + * + * To report the status of function calls other than lifecycle event handlers, + * use `console.log` or the Cloud Functions logger SDK. + * + * @param state - The state to set the instance to. + * @param detailMessage - A message explaining the results of the lifecycle function. + */ + public async setProcessingState(state: SettableProcessingState, detailMessage: string): Promise { + await this.client.updateRuntimeData( + this.projectId, + this.extensionInstanceId, + { + processingState: { + state, + detailMessage, + }, + }, + ); + } + + /** + * Reports a fatal error while running a lifecycle event handler. + * + * @remarks + * Call this method when a lifecycle event handler fails in a way that makes + * the Instance inoperable. + * If the lifecycle event failed but the instance will still work as expected, + * call `setProcessingState` with the "PROCESSING_WARNING" or + * "PROCESSING_FAILED" state instead. + * + * @param errorMessage - A message explaining what went wrong and how to fix it. + */ + public async setFatalError(errorMessage: string): Promise { + if (!validator.isNonEmptyString(errorMessage)) { + throw new FirebaseExtensionsError( + 'invalid-argument', + 'errorMessage must not be empty' + ); + } + await this.client.updateRuntimeData( + this.projectId, + this.extensionInstanceId, + { + fatalError: { + errorMessage, + }, + }, + ); + } + + private getProjectId(): string { + const projectId = process.env['PROJECT_ID']; + if (!validator.isNonEmptyString(projectId)) { + throw new FirebaseExtensionsError( + 'invalid-argument', + 'PROJECT_ID must not be undefined in Extensions runtime environment' + ); + } + return projectId; + } +} diff --git a/src/extensions/index.ts b/src/extensions/index.ts new file mode 100644 index 0000000000..486ad645d0 --- /dev/null +++ b/src/extensions/index.ts @@ -0,0 +1,64 @@ +/*! + * @license + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Firebase Extensions service. + * + * @packageDocumentation + */ + +import { App, getApp } from '../app'; +import { FirebaseApp } from '../app/firebase-app'; +import { Extensions } from './extensions'; + +export { Extensions, Runtime } from './extensions'; +export { SettableProcessingState } from './extensions-api'; + +/** + * Gets the {@link Extensions} service for the default app + * or a given app. + * + * `getExtensions()` can be called with no arguments to access the default + * app's `Extensions` service or as `getExtensions(app)` to access the + * `Extensions` service associated with a specific app. + * + * @example + * ```javascript + * // Get the `Extensions` service for the default app + * const defaultExtensions = getExtensions(); + * ``` + * + * @example + * ```javascript + * // Get the `Extensions` service for a given app + * const otherExtensions = getExtensions(otherApp); + * ``` + * + * @param app - Optional app for which to return the `Extensions` service. + * If not provided, the default `Extensions` service is returned. + * + * @returns The default `Extensions` service if no app is provided, or the `Extensions` + * service associated with the provided app. + */ +export function getExtensions(app?: App): Extensions { + if (typeof app === 'undefined') { + app = getApp(); + } + + const firebaseApp: FirebaseApp = app as FirebaseApp; + return firebaseApp.getOrInitService('extensions', (app) => new Extensions(app)); +} diff --git a/src/firebase-app.ts b/src/firebase-app.ts deleted file mode 100644 index 75c083c9a6..0000000000 --- a/src/firebase-app.ts +++ /dev/null @@ -1,435 +0,0 @@ -/*! - * 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 {ApplicationDefaultCredential, Credential, GoogleOAuthAccessToken} 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 {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'; - -/** - * 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; - databaseURL?: string; - storageBucket?: string; - projectId?: string; -} - -/** - * Type representing a Firebase OAuth access token (derived from a Google OAuth2 access token) which - * can be used to authenticate to Firebase services such as the Realtime Database and Auth. - */ -export interface FirebaseAccessToken { - accessToken: string; - expirationTime: number; -} - -/** - * Internals of a FirebaseApp instance. - */ -export class FirebaseAppInternals { - private isDeleted_ = false; - private cachedToken_: FirebaseAccessToken; - private cachedTokenPromise_: Promise; - 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_; - } - } - - /** - * 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. - */ - public addAuthTokenListener(listener: (token: string) => void) { - this.tokenListeners_.push(listener); - if (this.cachedToken_) { - listener(this.cachedToken_.accessToken); - } - } - - /** - * Removes a token listener. - * - * @param {function(string)} listener The listener to remove. - */ - public removeAuthTokenListener(listener: (token: string) => 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((error) => { - // 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); - } -} - - - -/** - * Global context object for a collection of services using a shared authentication state. - */ -export class FirebaseApp { - public INTERNAL: FirebaseAppInternals; - - private name_: string; - private options_: FirebaseAppOptions; - private services_: {[name: string]: FirebaseServiceInterface} = {}; - private isDeleted_ = false; - - constructor(options: FirebaseAppOptions, name: string, private firebaseInternals_: FirebaseNamespaceInternals) { - this.name_ = name; - this.options_ = deepCopy(options) as FirebaseAppOptions; - - 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 ` + - `app named "${this.name_}". Options must be a non-null object.`, - ); - } - - const hasCredential = ('credential' in this.options_); - if (!hasCredential) { - this.options_.credential = new ApplicationDefaultCredential(); - } - - const credential = this.options_.credential; - 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 ` + - `app named "${this.name_}". The "credential" property must be an object which implements ` + - `the Credential interface.`, - ); - } - - Object.keys(firebaseInternals_.serviceFactories).forEach((serviceName) => { - // Defer calling createService() until the service is accessed - this[serviceName] = this.getService_.bind(this, serviceName); - }); - - this.INTERNAL = new FirebaseAppInternals(this.options_.credential); - } - - /** - * Returns the Auth service instance associated with this app. - * - * @return {Auth} The Auth service instance of this app. - */ - public auth(): Auth { - return this.ensureService_('auth', () => { - return new Auth(this); - }); - } - - /** - * Returns the Database service for the specified URL, and the current app. - * - * @return {Database} The Database service instance of this app. - */ - public database(url?: string): Database { - const service: DatabaseService = this.ensureService_('database', () => { - return new DatabaseService(this); - }); - return service.getDatabase(url); - } - - /** - * Returns the Messaging service instance associated with this app. - * - * @return {Messaging} The Messaging service instance of this app. - */ - public messaging(): Messaging { - return this.ensureService_('messaging', () => { - return new Messaging(this); - }); - } - - /** - * Returns the Storage service instance associated with this app. - * - * @return {Storage} The Storage service instance of this app. - */ - public storage(): Storage { - return this.ensureService_('storage', () => { - return new Storage(this); - }); - } - - public firestore(): Firestore { - const service: FirestoreService = this.ensureService_('firestore', () => { - return new FirestoreService(this); - }); - return service.client; - } - - /** - * Returns the InstanceId service instance associated with this app. - * - * @return {InstanceId} The InstanceId service instance of this app. - */ - public instanceId(): InstanceId { - return this.ensureService_('iid', () => { - return new InstanceId(this); - }); - } - - /** - * Returns the name of the FirebaseApp instance. - * - * @returns {string} The name of the FirebaseApp instance. - */ - get name(): string { - this.checkDestroyed_(); - return this.name_; - } - - /** - * Returns the options for the FirebaseApp instance. - * - * @returns {FirebaseAppOptions} The options for the FirebaseApp instance. - */ - get options(): FirebaseAppOptions { - this.checkDestroyed_(); - return deepCopy(this.options_) as FirebaseAppOptions; - } - - /** - * Deletes the FirebaseApp instance. - * - * @returns {Promise} An empty Promise fulfilled once the FirebaseApp instance is deleted. - */ - public delete(): Promise { - this.checkDestroyed_(); - this.firebaseInternals_.removeApp(this.name_); - - this.INTERNAL.delete(); - - return Promise.all(Object.keys(this.services_).map((serviceName) => { - return this.services_[serviceName].INTERNAL.delete(); - })).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 {string} serviceName The name of the service instance to return. - * @return {FirebaseServiceInterface} The service instance with the provided name. - */ - private getService_(serviceName: string): FirebaseServiceInterface { - this.checkDestroyed_(); - - if (!(serviceName in this.services_)) { - this.services_[serviceName] = this.firebaseInternals_.serviceFactories[serviceName]( - this, - this.extendApp_.bind(this), - ); - } - - 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); - } - - /** - * Throws an Error if the FirebaseApp instance has already been deleted. - */ - private checkDestroyed_(): void { - if (this.isDeleted_) { - throw new FirebaseAppError( - AppErrorCodes.APP_DELETED, - `Firebase app named "${this.name_}" has already been deleted.`, - ); - } - } -} diff --git a/src/firebase-namespace-api.ts b/src/firebase-namespace-api.ts new file mode 100644 index 0000000000..5358e5bd2c --- /dev/null +++ b/src/firebase-namespace-api.ts @@ -0,0 +1,104 @@ +/*! + * 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 './app-check/app-check-namespace'; +import { auth } from './auth/auth-namespace'; +import { database } from './database/database-namespace'; +import { firestore } from './firestore/firestore-namespace'; +import { instanceId } from './instance-id/instance-id-namespace'; +import { installations } from './installations/installations-namespace'; +import { machineLearning } from './machine-learning/machine-learning-namespace'; +import { messaging } from './messaging/messaging-namespace'; +import { projectManagement } from './project-management/project-management-namespace'; +import { remoteConfig } from './remote-config/remote-config-namespace'; +import { securityRules } from './security-rules/security-rules-namespace'; +import { storage } from './storage/storage-namespace'; + +import { App as AppCore, AppOptions } from './app/index'; + +export { AppOptions, FirebaseError, FirebaseArrayIndexError } from './app/index'; + +// 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 firebase-admin.app#initializeApp} to create an app. + */ + export interface App extends AppCore { + appCheck(): appCheck.AppCheck; + auth(): auth.Auth; + database(url?: string): database.Database; + firestore(): firestore.Firestore; + installations(): installations.Installations; + /** + * @deprecated Use {@link firebase-admin.installations#Installations} instead. + */ + 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; + } +} + +export * from './credential/index'; +export { appCheck } from './app-check/app-check-namespace'; +export { auth } from './auth/auth-namespace'; +export { database } from './database/database-namespace'; +export { firestore } from './firestore/firestore-namespace'; +export { instanceId } from './instance-id/instance-id-namespace'; +export { installations } from './installations/installations-namespace'; +export { machineLearning } from './machine-learning/machine-learning-namespace'; +export { messaging } from './messaging/messaging-namespace'; +export { projectManagement } from './project-management/project-management-namespace'; +export { remoteConfig } from './remote-config/remote-config-namespace'; +export { securityRules } from './security-rules/security-rules-namespace'; +export { storage } from './storage/storage-namespace'; + +// 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.ts b/src/firebase-namespace.ts deleted file mode 100644 index 7330a64a66..0000000000 --- a/src/firebase-namespace.ts +++ /dev/null @@ -1,427 +0,0 @@ -/*! - * 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 fs = require('fs'); -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, - CertCredential, - RefreshTokenCredential, - ApplicationDefaultCredential, -} from './auth/credential'; - -import {Auth} from './auth/auth'; -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 * as validator from './utils/validator'; - -const DEFAULT_APP_NAME = '[DEFAULT]'; - -/** - * Constant holding the environment variable name with the default config. - * If the environmet variable contains a string that starts with '{' it will be parsed as JSON, - * otherwise it will be assumed to be pointing to a file. - */ -export const FIREBASE_CONFIG_VAR: string = 'FIREBASE_CONFIG'; - - -let globalAppDefaultCred: ApplicationDefaultCredential; -const globalCertCreds: { [key: string]: CertCredential } = {}; -const globalRefreshTokenCreds: { [key: string]: RefreshTokenCredential } = {}; - - -export interface FirebaseServiceNamespace { - (app?: FirebaseApp): T; - [key: string]: any; -} - - -/** - * Internals of a FirebaseNamespace instance. - */ -export class FirebaseNamespaceInternals { - public serviceFactories: {[serviceName: string]: FirebaseServiceFactory} = {}; - - private apps_: {[appName: string]: FirebaseApp} = {}; - private appHooks_: {[service: string]: AppHook} = {}; - - constructor(public firebase_) {} - - /** - * Initializes the FirebaseApp 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 environmet 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. - * - * @return {FirebaseApp} A new FirebaseApp instance. - */ - public initializeApp(options?: FirebaseAppOptions, appName = DEFAULT_APP_NAME): FirebaseApp { - if (typeof options === 'undefined') { - options = this.loadOptionsFromEnvVar(); - options.credential = new ApplicationDefaultCredential(); - } - if (typeof appName !== 'string' || appName === '') { - throw new FirebaseAppError( - AppErrorCodes.INVALID_APP_NAME, - `Invalid Firebase app name "${appName}" provided. App name must be a non-empty string.`, - ); - } else if (appName in this.apps_) { - if (appName === DEFAULT_APP_NAME) { - throw new FirebaseAppError( - AppErrorCodes.DUPLICATE_APP, - 'The default Firebase app already exists. This means you called initializeApp() ' + - 'more than once without providing an app name as the second argument. In most cases ' + - 'you only need to call initializeApp() once. But if you do want to initialize ' + - 'multiple apps, pass a second argument to initializeApp() to give each app a unique ' + - 'name.', - ); - } else { - throw new FirebaseAppError( - AppErrorCodes.DUPLICATE_APP, - `Firebase app named "${appName}" already exists. This means you called initializeApp() ` + - 'more than once with the same app name as the second argument. Make sure you provide a ' + - 'unique name every time you call initializeApp().', - ); - } - } - - const app = new FirebaseApp(options, appName, this); - - this.apps_[appName] = app; - - this.callAppHooks_(app, 'create'); - - return app; - } - - /** - * Returns the FirebaseApp instance with the provided name (or the default FirebaseApp 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. - */ - public app(appName = DEFAULT_APP_NAME): FirebaseApp { - if (typeof appName !== 'string' || appName === '') { - throw new FirebaseAppError( - AppErrorCodes.INVALID_APP_NAME, - `Invalid Firebase app name "${appName}" provided. App name must be a non-empty string.`, - ); - } else if (!(appName in this.apps_)) { - let errorMessage: string = (appName === DEFAULT_APP_NAME) - ? 'The default Firebase app does not exist. ' : `Firebase app named "${appName}" does not exist. `; - errorMessage += 'Make sure you call initializeApp() before using any of the Firebase services.'; - - throw new FirebaseAppError(AppErrorCodes.NO_APP, errorMessage); - } - - return this.apps_[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[] { - // 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. - */ - 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.`, - ); - } - - const appToRemove = this.app(appName); - this.callAppHooks_(appToRemove, 'delete'); - 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. - */ - 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; - } - - let serviceNamespace: FirebaseServiceNamespace; - - // The service namespace is an accessor function which takes a FirebaseApp instance - // or uses the default app if no FirebaseApp instance is provided - serviceNamespace = (appArg?: FirebaseApp) => { - 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 FirebaseApp instance. - * - * @param {FirebaseApp} app The FirebaseApp instance whose app hooks to call. - * @param {string} eventName The event name representing which app hooks to call. - */ - private callAppHooks_(app: FirebaseApp, eventName: string) { - Object.keys(this.serviceFactories).forEach((serviceName) => { - if (this.appHooks_[serviceName]) { - this.appHooks_[serviceName](eventName, app); - } - }); - } - - /** - * Parse the file pointed to by the FIREBASE_CONFIG_VAR, if it exists. - * Or if the FIREBASE_CONFIG_ENV contains a valid JSON object, parse it directly. - * If the environmet 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 { - 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; - } catch (error) { - // Throw a nicely formed error message if the file contents cannot be parsed - throw new FirebaseAppError( - AppErrorCodes.INVALID_APP_OPTIONS, - 'Failed to parse app options file: ' + error, - ); - } - } -} - - -const firebaseCredential = { - cert: (serviceAccountPathOrObject: string | object): Credential => { - const stringifiedServiceAccount = JSON.stringify(serviceAccountPathOrObject); - if (!(stringifiedServiceAccount in globalCertCreds)) { - globalCertCreds[stringifiedServiceAccount] = new CertCredential(serviceAccountPathOrObject); - } - return globalCertCreds[stringifiedServiceAccount]; - }, - - refreshToken: (refreshTokenPathOrObject: string | object): Credential => { - const stringifiedRefreshToken = JSON.stringify(refreshTokenPathOrObject); - if (!(stringifiedRefreshToken in globalRefreshTokenCreds)) { - globalRefreshTokenCreds[stringifiedRefreshToken] = new RefreshTokenCredential(refreshTokenPathOrObject); - } - return globalRefreshTokenCreds[stringifiedRefreshToken]; - }, - - applicationDefault: (): Credential => { - if (typeof globalAppDefaultCred === 'undefined') { - globalAppDefaultCred = new ApplicationDefaultCredential(); - } - return globalAppDefaultCred; - }, -}; - - -/** - * Global Firebase context object. - */ -export class FirebaseNamespace { - // Hack to prevent Babel from modifying the object returned as the default admin namespace. - /* tslint:disable:variable-name */ - public __esModule = true; - /* tslint:enable:variable-name */ - - public credential = firebaseCredential; - public SDK_VERSION = ''; - public INTERNAL: FirebaseNamespaceInternals; - - /* tslint:disable */ - // TODO(jwenger): Database is the only consumer of firebase.Promise. We should update it to use - // use the native Promise and then remove this. - public Promise: any = Promise; - /* tslint:enable */ - - constructor() { - this.INTERNAL = new FirebaseNamespaceInternals(this); - } - - /** - * Gets the `Auth` service namespace. The returned namespace can be used to get the - * `Auth` service for the default app or an explicitly specified app. - */ - get auth(): FirebaseServiceNamespace { - const fn: FirebaseServiceNamespace = (app?: FirebaseApp) => { - return this.ensureApp(app).auth(); - }; - return Object.assign(fn, {Auth}); - } - - /** - * Gets the `Database` service namespace. The returned namespace can be used to get the - * `Database` service for the default app or an explicitly specified app. - */ - get database(): FirebaseServiceNamespace { - const fn: FirebaseServiceNamespace = (app?: FirebaseApp) => { - return this.ensureApp(app).database(); - }; - return Object.assign(fn, require('@firebase/database')); - } - - /** - * Gets the `Messaging` service namespace. The returned namespace can be used to get the - * `Messaging` service for the default app or an explicitly specified app. - */ - get messaging(): FirebaseServiceNamespace { - const fn: FirebaseServiceNamespace = (app?: FirebaseApp) => { - return this.ensureApp(app).messaging(); - }; - return Object.assign(fn, {Messaging}); - } - - /** - * Gets the `Storage` service namespace. The returned namespace can be used to get the - * `Storage` service for the default app or an explicitly specified app. - */ - get storage(): FirebaseServiceNamespace { - const fn: FirebaseServiceNamespace = (app?: FirebaseApp) => { - return this.ensureApp(app).storage(); - }; - return Object.assign(fn, {Storage}); - } - - /** - * Gets the `Firestore` service namespace. The returned namespace can be used to get the - * `Firestore` service for the default app or an explicitly specified app. - */ - get firestore(): FirebaseServiceNamespace { - const fn: FirebaseServiceNamespace = (app?: FirebaseApp) => { - return this.ensureApp(app).firestore(); - }; - return Object.assign(fn, require('@google-cloud/firestore')); - } - - /** - * 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. - */ - get instanceId(): FirebaseServiceNamespace { - const fn: FirebaseServiceNamespace = (app?: FirebaseApp) => { - return this.ensureApp(app).instanceId(); - }; - return Object.assign(fn, {InstanceId}); - } - - /** - * Initializes the FirebaseApp 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 environmet 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. - * - * @return {FirebaseApp} A new FirebaseApp instance. - */ - public initializeApp(options?: FirebaseAppOptions, appName?: string): FirebaseApp { - return this.INTERNAL.initializeApp(options, appName); - } - - /** - * 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. - */ - public app(appName?: string): FirebaseApp { - 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[] { - return this.INTERNAL.apps; - } - - private ensureApp(app?: FirebaseApp): FirebaseApp { - if (typeof app === 'undefined') { - app = this.app(); - } - return app; - } -} diff --git a/src/firebase-service.ts b/src/firebase-service.ts deleted file mode 100644 index 8d8fd64387..0000000000 --- a/src/firebase-service.ts +++ /dev/null @@ -1,40 +0,0 @@ -/*! - * 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 {FirebaseApp} from './firebase-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: FirebaseApp; - 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 new file mode 100644 index 0000000000..148d61e9f2 --- /dev/null +++ b/src/firestore/firestore-internal.ts @@ -0,0 +1,164 @@ +/*! + * @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 { FirebaseFirestoreError } from '../utils/error'; +import { ServiceAccountCredential, isApplicationDefault } from '../app/credential-internal'; +import { Firestore, Settings } from '@google-cloud/firestore'; + +import * as validator from '../utils/validator'; +import * as utils from '../utils/index'; +import { App } from '../app'; + +/** + * Settings to pass to the Firestore constructor. + * + * @public + */ +export interface FirestoreSettings { + /** + * Use HTTP/1.1 REST transport where possible. + * + * `preferRest` will force the use of HTTP/1.1 REST transport until a method + * that requires gRPC is called. When a method requires gRPC, this Firestore + * client will load dependent gRPC libraries and then use gRPC transport for + * all communication from that point forward. Currently the only operation + * that requires gRPC is creating a snapshot listener using `onSnapshot()`. + * + * @defaultValue `undefined` + */ + preferRest?: boolean; +} + +export class FirestoreService { + + private readonly appInternal: App; + private readonly databases: Map = new Map(); + private readonly firestoreSettings: Map = new Map(); + + constructor(app: App) { + this.appInternal = app; + } + + initializeDatabase(databaseId: string, settings: FirestoreSettings): Firestore { + const existingInstance = this.databases.get(databaseId); + if (existingInstance) { + const initialSettings = this.firestoreSettings.get(databaseId) ?? {}; + if (this.checkIfSameSettings(settings, initialSettings)) { + return existingInstance; + } + throw new FirebaseFirestoreError({ + code: 'failed-precondition', + message: 'initializeFirestore() has already been called with ' + + 'different options. To avoid this error, call initializeFirestore() with the ' + + 'same options as when it was originally called, or call getFirestore() to return the' + + ' already initialized instance.' + }); + } + const newInstance = initFirestore(this.app, databaseId, settings); + this.databases.set(databaseId, newInstance); + this.firestoreSettings.set(databaseId, settings); + return newInstance; + } + + getDatabase(databaseId: string): Firestore { + let database = this.databases.get(databaseId); + if (database === undefined) { + database = initFirestore(this.app, databaseId, {}); + this.databases.set(databaseId, database); + this.firestoreSettings.set(databaseId, {}); + } + return database; + } + + private checkIfSameSettings(settingsA: FirestoreSettings, settingsB: FirestoreSettings): boolean { + const a = settingsA ?? {}; + const b = settingsB ?? {}; + // If we start passing more settings to Firestore constructor, + // replace this with deep equality check. + return (a.preferRest === b.preferRest); + } + + /** + * Returns the app associated with this Storage instance. + * + * @returns The app associated with this Storage instance. + */ + get app(): App { + return this.appInternal; + } +} + +export function getFirestoreOptions(app: App, firestoreSettings?: FirestoreSettings): Settings { + if (!validator.isNonNullObject(app) || !('options' in app)) { + throw new FirebaseFirestoreError({ + code: 'invalid-argument', + message: 'First argument passed to admin.firestore() must be a valid Firebase app instance.', + }); + } + + const projectId: string | null = utils.getExplicitProjectId(app); + const credential = app.options.credential; + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { version: firebaseVersion } = require('../../package.json'); + const preferRest = firestoreSettings?.preferRest; + if (credential instanceof ServiceAccountCredential) { + return { + credentials: { + private_key: credential.privateKey, + client_email: credential.clientEmail, + }, + // When the SDK is initialized with ServiceAccountCredentials an explicit projectId is + // guaranteed to be available. + projectId: projectId!, + firebaseVersion, + preferRest, + }; + } else if (isApplicationDefault(app.options.credential)) { + // 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, preferRest } + : { firebaseVersion, preferRest }; + } + + throw new FirebaseFirestoreError({ + code: 'invalid-credential', + message: 'Failed to initialize Google Cloud Firestore client with the available credentials. ' + + 'Must initialize the SDK with a certificate credential or application default credentials ' + + 'to use Cloud Firestore API.', + }); +} + +function initFirestore(app: App, databaseId: string, firestoreSettings?: FirestoreSettings): Firestore { + const options = getFirestoreOptions(app, firestoreSettings); + options.databaseId = databaseId; + let firestoreDatabase: typeof Firestore; + try { + // Lazy-load the Firestore implementation here, which in turns loads gRPC. + firestoreDatabase = require('@google-cloud/firestore').Firestore; + } catch (err) { + throw new FirebaseFirestoreError({ + code: 'missing-dependencies', + message: 'Failed to import the Cloud Firestore client library for Node.js. ' + + 'Make sure to install the "@google-cloud/firestore" npm package. ' + + `Original error: ${err}`, + }); + } + + return new firestoreDatabase(options); +} diff --git a/src/firestore/firestore-namespace.ts b/src/firestore/firestore-namespace.ts new file mode 100644 index 0000000000..67733fb8f6 --- /dev/null +++ b/src/firestore/firestore-namespace.ts @@ -0,0 +1,78 @@ +/*! + * 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 _firestore from '@google-cloud/firestore'; +import { App } from '../app'; + +export declare function firestore(app?: App): _firestore.Firestore; + +/* eslint-disable @typescript-eslint/no-namespace */ +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; + export import v1 = _firestore.v1; + + export import AggregateField = _firestore.AggregateField; + export import AggregateFieldType = _firestore.AggregateFieldType; + export import AggregateQuery = _firestore.AggregateQuery; + export import AggregateQuerySnapshot = _firestore.AggregateQuerySnapshot; + export import AggregateSpecData = _firestore.AggregateSpecData; + export import AggregateSpec = _firestore.AggregateSpec; + export import AggregateType = _firestore.AggregateType; + export import BulkWriter = _firestore.BulkWriter; + export import BulkWriterOptions = _firestore.BulkWriterOptions; + export import BundleBuilder = _firestore.BundleBuilder; + export import CollectionGroup = _firestore.CollectionGroup; + export import CollectionReference = _firestore.CollectionReference; + export import DocumentChange = _firestore.DocumentChange; + 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 Filter = _firestore.Filter; + export import Firestore = _firestore.Firestore; + export import FirestoreDataConverter = _firestore.FirestoreDataConverter; + export import GeoPoint = _firestore.GeoPoint; + export import GrpcStatus = _firestore.GrpcStatus; + export import OrderByDirection = _firestore.OrderByDirection; + 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 SetOptions = _firestore.SetOptions; + export import Timestamp = _firestore.Timestamp; + export import Transaction = _firestore.Transaction; + export import UpdateData = _firestore.UpdateData; + export import WhereFilterOp = _firestore.WhereFilterOp; + export import WriteBatch = _firestore.WriteBatch; + export import WriteResult = _firestore.WriteResult; + export import PartialWithFieldValue = _firestore.PartialWithFieldValue; + export import WithFieldValue = _firestore.WithFieldValue; + export import Primitive = _firestore.Primitive; + export import NestedUpdateFields = _firestore.NestedUpdateFields; + export import ChildUpdateFields = _firestore.ChildUpdateFields; + export import AddPrefixToKeys = _firestore.AddPrefixToKeys; + export import UnionToIntersection = _firestore.UnionToIntersection; + export import ReadOnlyTransactionOptions = _firestore.ReadOnlyTransactionOptions; + + export import setLogFunction = _firestore.setLogFunction; +} diff --git a/src/firestore/firestore.ts b/src/firestore/firestore.ts deleted file mode 100644 index f63692897b..0000000000 --- a/src/firestore/firestore.ts +++ /dev/null @@ -1,114 +0,0 @@ -/*! - * 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 {FirebaseApp} from '../firebase-app'; -import {FirebaseFirestoreError} from '../utils/error'; -import {FirebaseServiceInterface, FirebaseServiceInternalsInterface} from '../firebase-service'; -import {ApplicationDefaultCredential, Certificate} from '../auth/credential'; -import {Firestore} 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(); - - private appInternal: FirebaseApp; - private firestoreClient: Firestore; - - constructor(app: FirebaseApp) { - this.firestoreClient = initFirestore(app); - this.appInternal = app; - } - - /** - * Returns the app associated with this Storage instance. - * - * @return {FirebaseApp} The app associated with this Storage instance. - */ - get app(): FirebaseApp { - return this.appInternal; - } - - get client(): Firestore { - return this.firestoreClient; - } -} - -function initFirestore(app: FirebaseApp): Firestore { - if (!validator.isNonNullObject(app) || !('options' in app)) { - throw new FirebaseFirestoreError({ - code: 'invalid-argument', - message: 'First argument passed to admin.firestore() must be a valid Firebase app instance.', - }); - } - - const projectId: string = utils.getProjectId(app); - const cert: Certificate = app.options.credential.getCertificate(); - let options: any; - if (cert != null) { - // cert is available when the SDK has been initialized with a service account JSON file, - // or by setting the GOOGLE_APPLICATION_CREDENTIALS envrionment variable. - - if (!validator.isNonEmptyString(projectId)) { - // Assert for an explicit projct ID (either via AppOptions or the cert itself). - throw new FirebaseFirestoreError({ - code: 'no-project-id', - message: 'Failed to determine project ID for Firestore. Initialize the ' - + 'SDK with service account credentials or set project ID as an app option. ' - + 'Alternatively set the GCLOUD_PROJECT environment variable.', - }); - } - options = { - credentials: { - private_key: cert.privateKey, - client_email: cert.clientEmail, - }, - projectId, - }; - } else if (app.options.credential instanceof ApplicationDefaultCredential) { - // 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 GCLOUD_PROJECT in GCP runtimes. - options = validator.isNonEmptyString(projectId) ? {projectId} : {}; - } else { - throw new FirebaseFirestoreError({ - code: 'invalid-credential', - message: 'Failed to initialize Google Cloud Firestore client with the available credentials. ' + - 'Must initialize the SDK with a certificate credential or application default credentials ' + - 'to use Cloud Firestore API.', - }); - } - - // Lazy-load the Firestore implementation here, which in turns loads gRPC. - const firestoreDatabase = require('@google-cloud/firestore'); - return new firestoreDatabase(options); -} diff --git a/src/firestore/index.ts b/src/firestore/index.ts new file mode 100644 index 0000000000..f906f382e2 --- /dev/null +++ b/src/firestore/index.ts @@ -0,0 +1,224 @@ +/*! + * 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. + */ + +/** + * Cloud Firestore. + * + * @packageDocumentation + */ + +import { Firestore } from '@google-cloud/firestore'; +import { App, getApp } from '../app'; +import { FirebaseApp } from '../app/firebase-app'; +import { FirestoreService, FirestoreSettings } from './firestore-internal'; +import { DEFAULT_DATABASE_ID } from '@google-cloud/firestore/build/src/path'; + +export { + AddPrefixToKeys, + AggregateField, + AggregateFieldType, + AggregateQuery, + AggregateQuerySnapshot, + AggregateSpecData, + AggregateSpec, + AggregateType, + BulkWriter, + BulkWriterOptions, + BundleBuilder, + ChildUpdateFields, + CollectionGroup, + CollectionReference, + DocumentChange, + DocumentChangeType, + DocumentData, + DocumentReference, + DocumentSnapshot, + FieldPath, + FieldValue, + Filter, + Firestore, + FirestoreDataConverter, + GeoPoint, + GrpcStatus, + NestedUpdateFields, + OrderByDirection, + PartialWithFieldValue, + Precondition, + Primitive, + Query, + QueryDocumentSnapshot, + QueryPartition, + QuerySnapshot, + ReadOptions, + ReadOnlyTransactionOptions, + ReadWriteTransactionOptions, + Settings, + SetOptions, + Timestamp, + Transaction, + UpdateData, + UnionToIntersection, + WhereFilterOp, + WithFieldValue, + WriteBatch, + WriteResult, + v1, + setLogFunction, +} from '@google-cloud/firestore'; + +export { FirestoreSettings }; + +/** + * Gets the default {@link https://googleapis.dev/nodejs/firestore/latest/Firestore.html | Firestore} + * service for the default app. + * + * @example + * ```javascript + * // Get the default Firestore service for the default app + * const defaultFirestore = getFirestore(); + * ``` + + * @returns The default {@link https://googleapis.dev/nodejs/firestore/latest/Firestore.html | Firestore} + * service for the default app. + */ +export function getFirestore(): Firestore; + +/** + * Gets the default {@link https://googleapis.dev/nodejs/firestore/latest/Firestore.html | Firestore} + * service for the given app. + * + * @example + * ```javascript + * // Get the default Firestore service for a specific app + * const otherFirestore = getFirestore(app); + * ``` + * + * @param app - which `Firestore` service to return. + * + * @returns The default {@link https://googleapis.dev/nodejs/firestore/latest/Firestore.html | Firestore} + * service associated with the provided app. + */ +export function getFirestore(app: App): Firestore; + +/** + * Gets the named {@link https://googleapis.dev/nodejs/firestore/latest/Firestore.html | Firestore} + * service for the default app. + * + * @example + * ```javascript + * // Get the Firestore service for a named database and default app + * const otherFirestore = getFirestore('otherDb'); + * ``` + * + * @param databaseId - name of database to return. + * + * @returns The named {@link https://googleapis.dev/nodejs/firestore/latest/Firestore.html | Firestore} + * service for the default app. + * @beta + */ +export function getFirestore(databaseId: string): Firestore; + +/** + * Gets the named {@link https://googleapis.dev/nodejs/firestore/latest/Firestore.html | Firestore} + * service for the given app. + * + * @example + * ```javascript + * // Get the Firestore service for a named database and specific app. + * const otherFirestore = getFirestore('otherDb'); + * ``` + * + * @param app - which `Firestore` service to return. + * + * @param databaseId - name of database to return. + * + * @returns The named {@link https://googleapis.dev/nodejs/firestore/latest/Firestore.html | Firestore} + * service associated with the provided app. + * @beta + */ +export function getFirestore(app: App, databaseId: string): Firestore; + +export function getFirestore( + appOrDatabaseId?: App | string, + optionalDatabaseId?: string +): Firestore { + const app: App = typeof appOrDatabaseId === 'object' ? appOrDatabaseId : getApp(); + const databaseId = + (typeof appOrDatabaseId === 'string' ? appOrDatabaseId : optionalDatabaseId) || DEFAULT_DATABASE_ID; + const firebaseApp: FirebaseApp = app as FirebaseApp; + const firestoreService = firebaseApp.getOrInitService( + 'firestore', (app) => new FirestoreService(app)); + return firestoreService.getDatabase(databaseId); +} + +/** + * Gets the default {@link https://googleapis.dev/nodejs/firestore/latest/Firestore.html | Firestore} + * service for the given app, passing extra parameters to its constructor. + * + * @example + * ```javascript + * // Get the Firestore service for a specific app, require HTTP/1.1 REST transport + * const otherFirestore = initializeFirestore(app, {preferRest: true}); + * ``` + * + * @param app - which `Firestore` service to return. + * + * @param settings - Settings object to be passed to the constructor. + * + * @returns The default `Firestore` service associated with the provided app and settings. + */ +export function initializeFirestore(app: App, settings?: FirestoreSettings): Firestore; + +/** + * Gets the named {@link https://googleapis.dev/nodejs/firestore/latest/Firestore.html | Firestore} + * service for the given app, passing extra parameters to its constructor. + * + * @example + * ```javascript + * // Get the Firestore service for a specific app, require HTTP/1.1 REST transport + * const otherFirestore = initializeFirestore(app, {preferRest: true}, 'otherDb'); + * ``` + * + * @param app - which `Firestore` service to return. + * + * @param settings - Settings object to be passed to the constructor. + * + * @param databaseId - name of database to return. + * + * @returns The named `Firestore` service associated with the provided app and settings. + * @beta + */ +export function initializeFirestore( + app: App, + settings: FirestoreSettings, + databaseId: string +): Firestore; + +export function initializeFirestore( + app: App, + settings?: FirestoreSettings, + databaseId?: string +): Firestore { + settings ??= {}; + databaseId ??= DEFAULT_DATABASE_ID; + const firebaseApp: FirebaseApp = app as FirebaseApp; + const firestoreService = firebaseApp.getOrInitService( + 'firestore', (app) => new FirestoreService(app)); + + return firestoreService.initializeDatabase(databaseId, settings); +} + +export { FirebaseFirestoreError } from '../utils/error'; diff --git a/src/functions/functions-api-client-internal.ts b/src/functions/functions-api-client-internal.ts new file mode 100644 index 0000000000..85a447cf33 --- /dev/null +++ b/src/functions/functions-api-client-internal.ts @@ -0,0 +1,419 @@ +/*! + * @license + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { App } from '../app'; +import { FirebaseApp } from '../app/firebase-app'; +import { + HttpRequestConfig, HttpClient, HttpError, AuthorizedHttpClient +} from '../utils/api-request'; +import { PrefixedFirebaseError } from '../utils/error'; +import * as utils from '../utils/index'; +import * as validator from '../utils/validator'; +import { TaskOptions } from './functions-api'; +import { ComputeEngineCredential } from '../app/credential-internal'; + +const CLOUD_TASKS_API_RESOURCE_PATH = 'projects/{projectId}/locations/{locationId}/queues/{resourceId}/tasks'; +const CLOUD_TASKS_API_URL_FORMAT = 'https://cloudtasks.googleapis.com/v2/' + CLOUD_TASKS_API_RESOURCE_PATH; +const FIREBASE_FUNCTION_URL_FORMAT = 'https://{locationId}-{projectId}.cloudfunctions.net/{resourceId}'; + +const FIREBASE_FUNCTIONS_CONFIG_HEADERS = { + 'X-Firebase-Client': `fire-admin-node/${utils.getSdkVersion()}` +}; + +// Default canonical location ID of the task queue. +const DEFAULT_LOCATION = 'us-central1'; + +/** + * Class that facilitates sending requests to the Firebase Functions backend API. + * + * @internal + */ +export class FunctionsApiClient { + private readonly httpClient: HttpClient; + private projectId?: string; + private accountId?: string; + + constructor(private readonly app: App) { + if (!validator.isNonNullObject(app) || !('options' in app)) { + throw new FirebaseFunctionsError( + 'invalid-argument', + 'First argument passed to getFunctions() must be a valid Firebase app instance.'); + } + this.httpClient = new AuthorizedHttpClient(app as FirebaseApp); + } + /** + * Deletes a task from a queue. + * + * @param id - The ID of the task to delete. + * @param functionName - The function name of the queue. + * @param extensionId - Optional canonical ID of the extension. + */ + public async delete(id: string, functionName: string, extensionId?: string): Promise { + if (!validator.isNonEmptyString(functionName)) { + throw new FirebaseFunctionsError( + 'invalid-argument', 'Function name must be a non empty string'); + } + if (!validator.isTaskId(id)) { + throw new FirebaseFunctionsError( + 'invalid-argument', 'id can contain only letters ([A-Za-z]), numbers ([0-9]), ' + + 'hyphens (-), or underscores (_). The maximum length is 500 characters.'); + } + + let resources: utils.ParsedResource; + try { + resources = utils.parseResourceName(functionName, 'functions'); + } catch (err) { + throw new FirebaseFunctionsError( + 'invalid-argument', 'Function name must be a single string or a qualified resource name'); + } + resources.projectId = resources.projectId || await this.getProjectId(); + resources.locationId = resources.locationId || DEFAULT_LOCATION; + if (!validator.isNonEmptyString(resources.resourceId)) { + throw new FirebaseFunctionsError( + 'invalid-argument', 'No valid function name specified to enqueue tasks for.'); + } + if (typeof extensionId !== 'undefined' && validator.isNonEmptyString(extensionId)) { + resources.resourceId = `ext-${extensionId}-${resources.resourceId}`; + } + + try { + const serviceUrl = await this.getUrl(resources, CLOUD_TASKS_API_URL_FORMAT.concat('/', id)); + const request: HttpRequestConfig = { + method: 'DELETE', + url: serviceUrl, + headers: FIREBASE_FUNCTIONS_CONFIG_HEADERS, + }; + await this.httpClient.send(request); + } catch (err: unknown) { + if (err instanceof HttpError) { + if (err.response.status === 404) { + // if no task with the provided ID exists, then ignore the delete. + return; + } + throw this.toFirebaseError(err); + } else { + throw err; + } + } + } + + /** + * Creates a task and adds it to a queue. + * + * @param data - The data payload of the task. + * @param functionName - The functionName of the queue. + * @param extensionId - Optional canonical ID of the extension. + * @param opts - Optional options when enqueuing a new task. + */ + public async enqueue(data: any, functionName: string, extensionId?: string, opts?: TaskOptions): Promise { + if (!validator.isNonEmptyString(functionName)) { + throw new FirebaseFunctionsError( + 'invalid-argument', 'Function name must be a non empty string'); + } + + let resources: utils.ParsedResource; + try { + resources = utils.parseResourceName(functionName, 'functions'); + } catch (err) { + throw new FirebaseFunctionsError( + 'invalid-argument', 'Function name must be a single string or a qualified resource name'); + } + resources.projectId = resources.projectId || await this.getProjectId(); + resources.locationId = resources.locationId || DEFAULT_LOCATION; + if (!validator.isNonEmptyString(resources.resourceId)) { + throw new FirebaseFunctionsError( + 'invalid-argument', 'No valid function name specified to enqueue tasks for.'); + } + if (typeof extensionId !== 'undefined' && validator.isNonEmptyString(extensionId)) { + resources.resourceId = `ext-${extensionId}-${resources.resourceId}`; + } + + const task = this.validateTaskOptions(data, resources, opts); + try { + const serviceUrl = await this.getUrl(resources, CLOUD_TASKS_API_URL_FORMAT); + const taskPayload = await this.updateTaskPayload(task, resources, extensionId); + const request: HttpRequestConfig = { + method: 'POST', + url: serviceUrl, + headers: FIREBASE_FUNCTIONS_CONFIG_HEADERS, + data: { + task: taskPayload, + } + }; + await this.httpClient.send(request); + } catch (err: unknown) { + if (err instanceof HttpError) { + if (err.response.status === 409) { + throw new FirebaseFunctionsError('task-already-exists', `A task with ID ${opts?.id} already exists`); + } else { + throw this.toFirebaseError(err); + } + } else { + throw err; + } + } + } + + private getUrl(resourceName: utils.ParsedResource, urlFormat: string): Promise { + let { locationId } = resourceName; + const { projectId, resourceId } = resourceName; + if (typeof locationId === 'undefined' || !validator.isNonEmptyString(locationId)) { + locationId = DEFAULT_LOCATION; + } + return Promise.resolve() + .then(() => { + if (typeof projectId !== 'undefined' && validator.isNonEmptyString(projectId)) { + return projectId; + } + return this.getProjectId(); + }) + .then((projectId) => { + const urlParams = { + projectId, + locationId, + resourceId, + }; + // Formats a string of form 'project/{projectId}/{api}' and replaces + // with corresponding arguments {projectId: '1234', api: 'resource'} + // and returns output: 'project/1234/resource'. + return utils.formatString(urlFormat, urlParams); + }); + } + + private getProjectId(): Promise { + if (this.projectId) { + return Promise.resolve(this.projectId); + } + return utils.findProjectId(this.app) + .then((projectId) => { + if (!validator.isNonEmptyString(projectId)) { + throw new FirebaseFunctionsError( + 'unknown-error', + 'Failed to determine project ID. Initialize the ' + + 'SDK with service account credentials or set project ID as an app option. ' + + 'Alternatively, set the GOOGLE_CLOUD_PROJECT environment variable.'); + } + this.projectId = projectId; + return projectId; + }); + } + + private getServiceAccount(): Promise { + if (this.accountId) { + return Promise.resolve(this.accountId); + } + return utils.findServiceAccountEmail(this.app) + .then((accountId) => { + if (!validator.isNonEmptyString(accountId)) { + throw new FirebaseFunctionsError( + 'unknown-error', + 'Failed to determine service account. Initialize the ' + + 'SDK with service account credentials or set service account ID as an app option.'); + } + this.accountId = accountId; + return accountId; + }); + } + + private validateTaskOptions(data: any, resources: utils.ParsedResource, opts?: TaskOptions): Task { + const task: Task = { + httpRequest: { + url: '', + oidcToken: { + serviceAccountEmail: '', + }, + body: Buffer.from(JSON.stringify({ data })).toString('base64'), + headers: { + 'Content-Type': 'application/json', + ...opts?.headers, + } + } + } + + if (typeof opts !== 'undefined') { + if (!validator.isNonNullObject(opts)) { + throw new FirebaseFunctionsError( + 'invalid-argument', 'TaskOptions must be a non-null object'); + } + if ('scheduleTime' in opts && 'scheduleDelaySeconds' in opts) { + throw new FirebaseFunctionsError( + 'invalid-argument', 'Both scheduleTime and scheduleDelaySeconds are provided. ' + + 'Only one value should be set.'); + } + if ('scheduleTime' in opts && typeof opts.scheduleTime !== 'undefined') { + if (!(opts.scheduleTime instanceof Date)) { + throw new FirebaseFunctionsError( + 'invalid-argument', 'scheduleTime must be a valid Date object.'); + } + task.scheduleTime = opts.scheduleTime.toISOString(); + } + if ('scheduleDelaySeconds' in opts && typeof opts.scheduleDelaySeconds !== 'undefined') { + if (!validator.isNumber(opts.scheduleDelaySeconds) || opts.scheduleDelaySeconds < 0) { + throw new FirebaseFunctionsError( + 'invalid-argument', 'scheduleDelaySeconds must be a non-negative duration in seconds.'); + } + const date = new Date(); + date.setSeconds(date.getSeconds() + opts.scheduleDelaySeconds); + task.scheduleTime = date.toISOString(); + } + if (typeof opts.dispatchDeadlineSeconds !== 'undefined') { + if (!validator.isNumber(opts.dispatchDeadlineSeconds) || opts.dispatchDeadlineSeconds < 15 + || opts.dispatchDeadlineSeconds > 1800) { + throw new FirebaseFunctionsError( + 'invalid-argument', 'dispatchDeadlineSeconds must be a non-negative duration in seconds ' + + 'and must be in the range of 15s to 30 mins.'); + } + task.dispatchDeadline = `${opts.dispatchDeadlineSeconds}s`; + } + if ('id' in opts && typeof opts.id !== 'undefined') { + if (!validator.isTaskId(opts.id)) { + throw new FirebaseFunctionsError( + 'invalid-argument', 'id can contain only letters ([A-Za-z]), numbers ([0-9]), ' + + 'hyphens (-), or underscores (_). The maximum length is 500 characters.'); + } + const resourcePath = utils.formatString(CLOUD_TASKS_API_RESOURCE_PATH, { + projectId: resources.projectId, + locationId: resources.locationId, + resourceId: resources.resourceId, + }); + task.name = resourcePath.concat('/', opts.id); + } + if (typeof opts.uri !== 'undefined') { + if (!validator.isURL(opts.uri)) { + throw new FirebaseFunctionsError( + 'invalid-argument', 'uri must be a valid URL string.'); + } + task.httpRequest.url = opts.uri; + } + } + return task; + } + + private async updateTaskPayload(task: Task, resources: utils.ParsedResource, extensionId?: string): Promise { + const functionUrl = validator.isNonEmptyString(task.httpRequest.url) + ? task.httpRequest.url + : await this.getUrl(resources, FIREBASE_FUNCTION_URL_FORMAT); + task.httpRequest.url = functionUrl; + // When run from a deployed extension, we should be using ComputeEngineCredentials + if (validator.isNonEmptyString(extensionId) && this.app.options.credential instanceof ComputeEngineCredential) { + const idToken = await this.app.options.credential.getIDToken(functionUrl); + task.httpRequest.headers = { ...task.httpRequest.headers, 'Authorization': `Bearer ${idToken}` }; + // Don't send httpRequest.oidcToken if we set Authorization header, or Cloud Tasks will overwrite it. + delete task.httpRequest.oidcToken; + } else { + const account = await this.getServiceAccount(); + task.httpRequest.oidcToken = { serviceAccountEmail: account }; + } + return task; + } + + private toFirebaseError(err: HttpError): PrefixedFirebaseError { + if (err instanceof PrefixedFirebaseError) { + return err; + } + + const response = err.response; + if (!response.isJson()) { + return new FirebaseFunctionsError( + 'unknown-error', + `Unexpected response with status: ${response.status} and body: ${response.text}`); + } + + const error: Error = (response.data as ErrorResponse).error || {}; + let code: FunctionsErrorCode = 'unknown-error'; + if (error.status && error.status in FUNCTIONS_ERROR_CODE_MAPPING) { + code = FUNCTIONS_ERROR_CODE_MAPPING[error.status]; + } + const message = error.message || `Unknown server error: ${response.text}`; + return new FirebaseFunctionsError(code, message); + } +} + +interface ErrorResponse { + error?: Error; +} + +interface Error { + code?: number; + message?: string; + status?: string; +} + +/** + * Task is a limited subset of https://cloud.google.com/tasks/docs/reference/rest/v2/projects.locations.queues.tasks#resource:-task + * containing the relevant fields for enqueueing tasks that tirgger Cloud Functions. + */ +export interface Task { + name?: string; + // A timestamp in RFC3339 UTC "Zulu" format, with nanosecond resolution and up to nine fractional + // digits. Examples: "2014-10-02T15:01:23Z" and "2014-10-02T15:01:23.045123456Z". + scheduleTime?: string; + // A duration in seconds with up to nine fractional digits, terminated by 's'. Example: "3.5s". + dispatchDeadline?: string; + httpRequest: { + url: string; + oidcToken?: { + serviceAccountEmail: string; + }; + // A base64-encoded string. + body: string; + headers: { [key: string]: string }; + }; +} + +export const FUNCTIONS_ERROR_CODE_MAPPING: { [key: string]: FunctionsErrorCode } = { + ABORTED: 'aborted', + INVALID_ARGUMENT: 'invalid-argument', + INVALID_CREDENTIAL: 'invalid-credential', + INTERNAL: 'internal-error', + FAILED_PRECONDITION: 'failed-precondition', + PERMISSION_DENIED: 'permission-denied', + UNAUTHENTICATED: 'unauthenticated', + NOT_FOUND: 'not-found', + UNKNOWN: 'unknown-error', +}; + +export type FunctionsErrorCode = + 'aborted' + | 'invalid-argument' + | 'invalid-credential' + | 'internal-error' + | 'failed-precondition' + | 'permission-denied' + | 'unauthenticated' + | 'not-found' + | 'unknown-error' + | 'task-already-exists'; + +/** + * Firebase Functions error code structure. This extends PrefixedFirebaseError. + * + * @param code - The error code. + * @param message - The error message. + * @constructor + */ +export class FirebaseFunctionsError extends PrefixedFirebaseError { + constructor(code: FunctionsErrorCode, message: string) { + super('functions', code, message); + + /* tslint:disable:max-line-length */ + // Set the prototype explicitly. See the following link for more details: + // https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work + /* tslint:enable:max-line-length */ + (this as any).__proto__ = FirebaseFunctionsError.prototype; + } +} diff --git a/src/functions/functions-api.ts b/src/functions/functions-api.ts new file mode 100644 index 0000000000..a0473baee2 --- /dev/null +++ b/src/functions/functions-api.ts @@ -0,0 +1,106 @@ +/*! + * @license + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Interface representing task options with delayed delivery. + */ +export interface DelayDelivery { + /** + * The duration of delay of the time when the task is scheduled to be attempted or retried. + * This delay is added to the current time. + */ + scheduleDelaySeconds?: number; + /** @alpha */ + scheduleTime?: never; +} + +/** + * Interface representing task options with absolute delivery. + */ +export interface AbsoluteDelivery { + /** + * The time when the task is scheduled to be attempted or retried. + */ + scheduleTime?: Date; + /** @alpha */ + scheduleDelaySeconds?: never; +} + +/** + * Type representing delivery schedule options. + * `DeliverySchedule` is a union type of {@link DelayDelivery} and {@link AbsoluteDelivery} types. + */ +export type DeliverySchedule = DelayDelivery | AbsoluteDelivery + +/** + * Type representing task options. + */ +export type TaskOptions = DeliverySchedule & TaskOptionsExperimental & { + + /** + * The deadline for requests sent to the worker. If the worker does not respond by this deadline + * then the request is cancelled and the attempt is marked as a DEADLINE_EXCEEDED failure. + * Cloud Tasks will retry the task according to the `RetryConfig`. + * The default is 10 minutes. The deadline must be in the range of 15 seconds and 30 minutes. + */ + dispatchDeadlineSeconds?: number; + + /** + * The ID to use for the enqueued event. + * If not provided, one will be automatically generated. + * If provided, an explicitly specified task ID enables task de-duplication. If a task's ID is + * identical to that of an existing task or a task that was deleted or executed recently then + * the call will throw an error with code "functions/task-already-exists". Another task with + * the same ID can't be created for ~1hour after the original task was deleted or executed. + * + * Because there is an extra lookup cost to identify duplicate task IDs, setting ID + * significantly increases latency. Using hashed strings for the task ID or for the prefix of + * the task ID is recommended. Choosing task IDs that are sequential or have sequential + * prefixes, for example using a timestamp, causes an increase in latency and error rates in + * all task commands. The infrastructure relies on an approximately uniform distribution of + * task IDs to store and serve tasks efficiently. + * + * "Push IDs" from the Firebase Realtime Database make poor IDs because they are based on + * timestamps and will cause contention (slowdowns) in your task queue. Reversed push IDs + * however form a perfect distribution and are an ideal key. To reverse a string in + * javascript use `someString.split("").reverse().join("")` + */ + id?: string; + + /** + * HTTP request headers to include in the request to the task queue function. + * These headers represent a subset of the headers that will accompany the task's HTTP + * request. Some HTTP request headers will be ignored or replaced, e.g. Authorization, Host, Content-Length, + * User-Agent etc. cannot be overridden. + * + * By default, Content-Type is set to 'application/json'. + * + * The size of the headers must be less than 80KB. + */ + headers?: Record; +} + +/** + * Type representing experimental (beta) task options. + */ +export interface TaskOptionsExperimental { + /** + * The full URL path that the request will be sent to. Must be a valid URL. + * @beta + */ + uri?: string; +} diff --git a/src/functions/functions.ts b/src/functions/functions.ts new file mode 100644 index 0000000000..f5a3ce6153 --- /dev/null +++ b/src/functions/functions.ts @@ -0,0 +1,114 @@ +/*! + * @license + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { App } from '../app'; +import { FirebaseFunctionsError, FunctionsApiClient } from './functions-api-client-internal'; +import { TaskOptions } from './functions-api'; +import * as validator from '../utils/validator'; + +/** + * The Firebase `Functions` service interface. + */ +export class Functions { + + private readonly client: FunctionsApiClient; + + /** + * @param app - The app for this `Functions` service. + * @constructor + * @internal + */ + constructor(readonly app: App) { + this.client = new FunctionsApiClient(app); + } + + /** + * Creates a reference to a {@link TaskQueue} for a given function name. + * The function name can be either: + * + * 1) A fully qualified function resource name: + * `projects/{project}/locations/{location}/functions/{functionName}` + * + * 2) A partial resource name with location and function name, in which case + * the runtime project ID is used: + * `locations/{location}/functions/{functionName}` + * + * 3) A partial function name, in which case the runtime project ID and the default location, + * `us-central1`, is used: + * `{functionName}` + * + * @param functionName - The name of the function. + * @param extensionId - Optional Firebase extension ID. + * @returns A promise that fulfills with a `TaskQueue`. + */ + public taskQueue>(functionName: string, extensionId?: string): TaskQueue { + return new TaskQueue(functionName, this.client, extensionId); + } +} + +/** + * The `TaskQueue` interface. + */ +export class TaskQueue> { + + /** + * @param functionName - The name of the function. + * @param client - The `FunctionsApiClient` instance. + * @param extensionId - Optional canonical ID of the extension. + * @constructor + * @internal + */ + constructor(private readonly functionName: string, private readonly client: FunctionsApiClient, + private readonly extensionId?: string) { + if (!validator.isNonEmptyString(functionName)) { + throw new FirebaseFunctionsError( + 'invalid-argument', + '`functionName` must be a non-empty string.'); + } + if (!validator.isNonNullObject(client) || !('enqueue' in client)) { + throw new FirebaseFunctionsError( + 'invalid-argument', + 'Must provide a valid FunctionsApiClient instance to create a new TaskQueue.'); + } + if (typeof extensionId !== 'undefined' && !validator.isString(extensionId)) { + throw new FirebaseFunctionsError( + 'invalid-argument', + '`extensionId` must be a string.'); + } + } + + /** + * Creates a task and adds it to the queue. Tasks cannot be updated after creation. + * This action requires `cloudtasks.tasks.create` IAM permission on the service account. + * + * @param data - The data payload of the task. + * @param opts - Optional options when enqueuing a new task. + * @returns A promise that resolves when the task has successfully been added to the queue. + */ + public enqueue(data: Args, opts?: TaskOptions): Promise { + return this.client.enqueue(data, this.functionName, this.extensionId, opts); + } + + /** + * Deletes an enqueued task if it has not yet completed. + * @param id - the ID of the task, relative to this queue. + * @returns A promise that resolves when the task has been deleted. + */ + public delete(id: string): Promise { + return this.client.delete(id, this.functionName, this.extensionId); + } +} diff --git a/src/functions/index.ts b/src/functions/index.ts new file mode 100644 index 0000000000..11e05c6782 --- /dev/null +++ b/src/functions/index.ts @@ -0,0 +1,73 @@ +/*! + * @license + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Firebase Functions service. + * + * @packageDocumentation + */ + +import { App, getApp } from '../app'; +import { FirebaseApp } from '../app/firebase-app'; +import { Functions } from './functions'; + +export { + DelayDelivery, + AbsoluteDelivery, + DeliverySchedule, + TaskOptions, + TaskOptionsExperimental, +} from './functions-api'; +export { + Functions, + TaskQueue +} from './functions'; + +/** + * Gets the {@link Functions} service for the default app + * or a given app. + * + * `getFunctions()` can be called with no arguments to access the default + * app's `Functions` service or as `getFunctions(app)` to access the + * `Functions` service associated with a specific app. + * + * @example + * ```javascript + * // Get the `Functions` service for the default app + * const defaultFunctions = getFunctions(); + * ``` + * + * @example + * ```javascript + * // Get the `Functions` service for a given app + * const otherFunctions = getFunctions(otherApp); + * ``` + * + * @param app - Optional app for which to return the `Functions` service. + * If not provided, the default `Functions` service is returned. + * + * @returns The default `Functions` service if no app is provided, or the `Functions` + * service associated with the provided app. + */ +export function getFunctions(app?: App): Functions { + if (typeof app === 'undefined') { + app = getApp(); + } + + const firebaseApp: FirebaseApp = app as FirebaseApp; + return firebaseApp.getOrInitService('functions', (app) => new Functions(app)); +} diff --git a/src/index.d.ts b/src/index.d.ts index 25eb6fc0bf..b893c88183 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"); @@ -14,514 +15,7 @@ * limitations under the License. */ -import {Bucket} from '@google-cloud/storage'; -import * as _firestore from '@google-cloud/firestore'; - -declare namespace admin { - interface FirebaseError { - code: string; - message: string; - stack: string; - - toJSON(): Object; - } - - type FirebaseArrayIndexError = { - index: number; - error: FirebaseError; - } - - interface ServiceAccount { - projectId?: string; - clientEmail?: string; - privateKey?: string; - } - - interface GoogleOAuthAccessToken { - access_token: string; - expires_in: number; - } - - interface AppOptions { - credential?: admin.credential.Credential; - databaseAuthVariableOverride?: Object; - databaseURL?: string; - storageBucket?: string; - projectId?: string; - } - - var SDK_VERSION: string; - var apps: (admin.app.App|null)[]; - - function app(name?: string): admin.app.App; - function auth(app?: admin.app.App): admin.auth.Auth; - function database(app?: admin.app.App): admin.database.Database; - function messaging(app?: admin.app.App): admin.messaging.Messaging; - function storage(app?: admin.app.App): admin.storage.Storage; - function firestore(app?: admin.app.App): admin.firestore.Firestore; - function instanceId(app?: admin.app.App): admin.instanceId.InstanceId; - function initializeApp(options?: admin.AppOptions, name?: string): admin.app.App; -} - -declare namespace admin.app { - interface App { - name: string; - options: admin.AppOptions; - - auth(): admin.auth.Auth; - database(url?: string): admin.database.Database; - firestore(): admin.firestore.Firestore; - instanceId(): admin.instanceId.InstanceId; - messaging(): admin.messaging.Messaging; - storage(): admin.storage.Storage; - delete(): Promise; - } -} - -declare namespace admin.auth { - interface UserMetadata { - lastSignInTime: string; - creationTime: string; - - toJSON(): Object; - } - - interface UserInfo { - uid: string; - displayName: string; - email: string; - phoneNumber: string; - photoURL: string; - providerId: string; - - toJSON(): Object; - } - - interface UserRecord { - uid: string; - email: string; - emailVerified: boolean; - displayName: string; - phoneNumber: string; - photoURL: string; - disabled: boolean; - metadata: admin.auth.UserMetadata; - providerData: admin.auth.UserInfo[]; - passwordHash?: string; - passwordSalt?: string; - customClaims?: Object; - tokensValidAfterTime?: string; - - toJSON(): Object; - } - - interface UpdateRequest { - displayName?: string; - email?: string; - emailVerified?: boolean; - phoneNumber?: string; - photoURL?: string; - disabled?: boolean; - password?: string; - } - - interface CreateRequest extends UpdateRequest { - uid?: string; - } - - interface DecodedIdToken { - aud: string; - auth_time: number; - exp: number; - firebase: { - identities: { - [key: string]: any; - }; - sign_in_provider: string; - [key: string]: any; - }; - iat: number; - iss: string; - sub: string; - uid: string; - [key: string]: any; - } - - interface ListUsersResult { - users: admin.auth.UserRecord[]; - pageToken?: string; - } - - interface Auth { - app: admin.app.App; - - createCustomToken(uid: string, developerClaims?: Object): Promise; - createUser(properties: admin.auth.CreateRequest): Promise; - deleteUser(uid: string): Promise; - getUser(uid: string): Promise; - getUserByEmail(email: string): Promise; - getUserByPhoneNumber(phoneNumber: string): Promise; - listUsers(maxResults?: number, pageToken?: string): Promise; - updateUser(uid: string, properties: admin.auth.UpdateRequest): Promise; - verifyIdToken(idToken: string, checkRevoked?: boolean): Promise; - setCustomUserClaims(uid: string, customUserClaims: Object): Promise; - revokeRefreshTokens(uid: string): Promise; - } -} - -declare namespace admin.credential { - interface Credential { - getAccessToken(): Promise; - } - - function applicationDefault(): admin.credential.Credential; - function cert(serviceAccountPathOrObject: string|admin.ServiceAccount): admin.credential.Credential; - function refreshToken(refreshTokenPathOrObject: string|Object): admin.credential.Credential; -} - -declare namespace admin.database { - interface Database { - app: admin.app.App; - - goOffline(): void; - goOnline(): void; - ref(path?: string | admin.database.Reference): admin.database.Reference; - refFromURL(url: string): admin.database.Reference; - } - - interface DataSnapshot { - key: string|null; - ref: admin.database.Reference; - - child(path: string): admin.database.DataSnapshot; - exists(): boolean; - exportVal(): any; - forEach(action: (a: admin.database.DataSnapshot) => boolean): boolean; - getPriority(): string|number|null; - hasChild(path: string): boolean; - hasChildren(): boolean; - numChildren(): number; - toJSON(): Object | null; - val(): any; - } - - interface OnDisconnect { - cancel(onComplete?: (a: Error|null) => any): Promise; - remove(onComplete?: (a: Error|null) => any): Promise; - set(value: any, onComplete?: (a: Error|null) => any): Promise; - setWithPriority( - value: any, - priority: number|string|null, - 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'; - - interface Query { - ref: admin.database.Reference; - - endAt(value: number|string|boolean|null, key?: string): admin.database.Query; - equalTo(value: number|string|boolean|null, key?: string): admin.database.Query; - isEqual(other: admin.database.Query|null): boolean; - limitToFirst(limit: number): admin.database.Query; - limitToLast(limit: number): admin.database.Query; - off( - eventType?: admin.database.EventType, - callback?: (a: admin.database.DataSnapshot, b?: string|null) => any, - context?: Object|null - ): void; - on( - eventType: admin.database.EventType, - callback: (a: admin.database.DataSnapshot|null, b?: string) => any, - cancelCallbackOrContext?: Object|null, - context?: Object|null - ): (a: admin.database.DataSnapshot|null, b?: string) => any; - once( - eventType: admin.database.EventType, - successCallback?: (a: admin.database.DataSnapshot, b?: string) => any, - failureCallbackOrContext?: Object|null, - context?: Object|null - ): Promise; - orderByChild(path: string): admin.database.Query; - orderByKey(): admin.database.Query; - orderByPriority(): admin.database.Query; - orderByValue(): admin.database.Query; - startAt(value: number|string|boolean|null, key?: string): admin.database.Query; - toJSON(): Object; - toString(): string; - } - - interface Reference extends admin.database.Query { - key: string|null; - parent: admin.database.Reference|null; - root: admin.database.Reference; - path: string; - - child(path: string): admin.database.Reference; - onDisconnect(): admin.database.OnDisconnect; - push(value?: any, onComplete?: (a: Error|null) => any): admin.database.ThenableReference; - remove(onComplete?: (a: Error|null) => any): Promise; - set(value: any, onComplete?: (a: Error|null) => any): Promise; - setPriority( - priority: string|number|null, - onComplete: (a: Error|null) => any - ): Promise; - setWithPriority( - newVal: any, newPriority: string|number|null, - onComplete?: (a: Error|null) => any - ): Promise; - 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 - }>; - update(values: Object, onComplete?: (a: Error|null) => any): Promise; - } - - interface ThenableReference extends admin.database.Reference, PromiseLike {} - - function enableLogging(logger?: boolean|((message: string) => any), persistent?: boolean): any; -} - -declare namespace admin.database.ServerValue { - var TIMESTAMP: number; -} - -type BaseMessage = { - data?: {[key: string]: string}; - notification?: admin.messaging.Notification; - android?: admin.messaging.AndroidConfig; - webpush?: admin.messaging.WebpushConfig; - apns?: admin.messaging.ApnsConfig; -}; - -interface TokenMessage extends BaseMessage { - token: string; -} - -interface TopicMessage extends BaseMessage { - topic: string; -} - -interface ConditionMessage extends BaseMessage { - condition: string; -} - -declare namespace admin.messaging { - type Message = TokenMessage | TopicMessage | ConditionMessage; - - type AndroidConfig = { - collapseKey?: string; - priority?: ('high'|'normal'); - ttl?: number; - restrictedPackageName?: string; - data?: {[key: string]: string}; - notification?: AndroidNotification; - }; - - type AndroidNotification = { - title?: string; - body?: string; - icon?: string; - color?: string; - sound?: string; - tag?: string; - clickAction?: string; - bodyLocKey?: string; - bodyLocArgs?: string[]; - titleLocKey?: string; - titleLocArgs?: string[]; - }; - - type ApnsConfig = { - headers?: {[key: string]: string}; - payload?: ApnsPayload; - }; - - type ApnsPayload = { - aps: Aps; - [customData: string]: object; - }; - - type Aps = { - alert?: string | ApsAlert; - badge?: number; - sound?: string; - contentAvailable?: boolean; - category?: string; - threadId?: string; - }; - - type ApsAlert = { - title?: string; - body?: string; - locKey?: string; - locArgs?: string[]; - titleLocKey?: string; - titleLocArgs?: string[]; - actionLocKey?: string; - launchImage?: string; - }; - - type Notification = { - title?: string; - body?: string; - }; - - type WebpushConfig = { - headers?: {[key: string]: string}; - data?: {[key: string]: string}; - notification?: WebpushNotification; - }; - - type WebpushNotification = { - title?: string; - body?: string; - icon?: string; - }; - - type DataMessagePayload = { - [key: string]: string; - }; - - type NotificationMessagePayload = { - tag?: string; - body?: string; - icon?: string; - badge?: string; - color?: string; - sound?: string; - title?: string; - bodyLocKey?: string; - bodyLocArgs?: string; - clickAction?: string; - titleLocKey?: string; - titleLocArgs?: string; - [key: string]: string | undefined; - }; - - type MessagingPayload = { - data?: admin.messaging.DataMessagePayload; - notification?: admin.messaging.NotificationMessagePayload; - }; - - type MessagingOptions = { - dryRun?: boolean; - priority?: string; - timeToLive?: number; - collapseKey?: string; - mutableContent?: boolean; - contentAvailable?: boolean; - restrictedPackageName?: string; - [key: string]: any | undefined; - }; - - type MessagingDeviceResult = { - error?: admin.FirebaseError; - messageId?: string; - canonicalRegistrationToken?: string; - }; - - type MessagingDevicesResponse = { - canonicalRegistrationTokenCount: number; - failureCount: number; - multicastId: number; - results: admin.messaging.MessagingDeviceResult[]; - successCount: number; - }; - - type MessagingDeviceGroupResponse = { - successCount: number; - failureCount: number; - failedRegistrationTokens: string[]; - }; - - type MessagingTopicResponse = { - messageId: number; - }; - - type MessagingConditionResponse = { - messageId: number; - }; - - type MessagingTopicManagementResponse = { - failureCount: number; - successCount: number; - errors: admin.FirebaseArrayIndexError[]; - }; - - interface Messaging { - app: admin.app.App; - - send(message: admin.messaging.Message, dryRun?: boolean): Promise; - sendToDevice( - registrationToken: string | string[], - payload: admin.messaging.MessagingPayload, - options?: admin.messaging.MessagingOptions - ): Promise; - sendToDeviceGroup( - notificationKey: string, - payload: admin.messaging.MessagingPayload, - options?: admin.messaging.MessagingOptions - ): Promise; - sendToTopic( - topic: string, - payload: admin.messaging.MessagingPayload, - options?: admin.messaging.MessagingOptions - ): Promise; - sendToCondition( - condition: string, - payload: admin.messaging.MessagingPayload, - options?: admin.messaging.MessagingOptions - ): Promise; - subscribeToTopic( - registrationToken: string, - topic: string - ): Promise; - subscribeToTopic( - registrationTokens: string[], - topic: string - ): Promise; - unsubscribeFromTopic( - registrationToken: string, - topic: string - ): Promise; - unsubscribeFromTopic( - registrationTokens: string[], - topic: string - ): Promise; - } -} - -declare namespace admin.storage { - interface Storage { - app: admin.app.App; - bucket(name?: string): Bucket; - } -} - -declare namespace admin.firestore { - 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 setLogFunction = _firestore.setLogFunction; -} - -declare namespace admin.instanceId { - interface InstanceId { - app: admin.app.App; - - deleteInstanceId(instanceId: string): Promise; - } -} +import * as admin from './default-namespace'; declare module 'firebase-admin' { } diff --git a/src/index.ts b/src/index.ts index be339fd158..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"); @@ -16,11 +17,22 @@ import * as firebase from './default-namespace'; -// Register the Database service -// For historical reasons, the database code is included as minified code and registers itself -// as a side effect of requiring the file. -/* tslint:disable:no-var-requires */ -// require('./database/database'); -/* tslint:enable:no-var-requires */ +// Only Node.js has a process variable that is of [[Class]] process +const processGlobal = typeof process !== 'undefined' ? process : 0; +if (Object.prototype.toString.call(processGlobal) !== '[object process]') { + const message = ` +======== WARNING! ======== + +firebase-admin appears to have been installed in an unsupported environment. +This package should only be used in server-side or backend Node.js environments, +and should not be used in web browsers or other client-side environments. + +Use the Firebase JS SDK for client-side Firebase integrations: + +https://firebase.google.com/docs/web/setup +`; + // tslint:disable-next-line:no-console + console.error(message); +} export = firebase; diff --git a/src/installations/index.ts b/src/installations/index.ts new file mode 100644 index 0000000000..1d6a39dc35 --- /dev/null +++ b/src/installations/index.ts @@ -0,0 +1,65 @@ +/*! + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Firebase Instance ID service. + * + * @packageDocumentation + */ + +import { App, getApp } from '../app/index'; +import { Installations } from './installations'; +import { FirebaseApp } from '../app/firebase-app'; + +export { Installations }; + +/** + * Gets the {@link Installations} service for the default app or a given app. + * + * `getInstallations()` can be called with no arguments to access the default + * app's `Installations` service or as `getInstallations(app)` to access the + * `Installations` service associated with a specific app. + * + * @example + * ```javascript + * // Get the Installations service for the default app + * const defaultInstallations = getInstallations(); + * ``` + * + * @example + * ```javascript + * // Get the Installations service for a given app + * const otherInstallations = getInstallations(otherApp); + *``` + * + * @param app - Optional app whose `Installations` service to + * return. If not provided, the default `Installations` service will be + * returned. + * + * @returns The default `Installations` service if + * no app is provided or the `Installations` service associated with the + * provided app. + */ +export function getInstallations(app?: App): Installations { + if (typeof app === 'undefined') { + app = getApp(); + } + + const firebaseApp: FirebaseApp = app as FirebaseApp; + return firebaseApp.getOrInitService('installations', (app) => new Installations(app)); +} + +export { FirebaseInstallationsError, InstallationsClientErrorCode } from '../utils/error'; diff --git a/src/installations/installations-namespace.ts b/src/installations/installations-namespace.ts new file mode 100644 index 0000000000..1bdca2fbce --- /dev/null +++ b/src/installations/installations-namespace.ts @@ -0,0 +1,58 @@ +/*! + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { App } from '../app/index'; +import { Installations as TInstallations } from './installations'; + +/** + * Gets the {@link firebase-admin.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 firebase-admin.installations#Installations} service or as + * `admin.installations(app)` to access the + * {@link firebase-admin.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. + * + * @returns The default `Installations` service if + * no app is provided or the `Installations` service associated with the + * provided app. + */ +export declare function installations(app?: App): installations.Installations; + +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace installations { + /** + * Type alias to {@link firebase-admin.installations#Installations}. + */ + export type Installations = TInstallations; +} diff --git a/src/installations/installations-request-handler.ts b/src/installations/installations-request-handler.ts new file mode 100644 index 0000000000..f6043c46d0 --- /dev/null +++ b/src/installations/installations-request-handler.ts @@ -0,0 +1,132 @@ +/*! + * @license + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { App } from '../app/index'; +import { FirebaseApp } from '../app/firebase-app'; +import { FirebaseInstallationsError, InstallationsClientErrorCode } from '../utils/error'; +import { + ApiSettings, AuthorizedHttpClient, HttpRequestConfig, HttpError, +} from '../utils/api-request'; + +import * as utils from '../utils/index'; +import * as validator from '../utils/validator'; + +/** Firebase IID backend host. */ +const FIREBASE_IID_HOST = 'console.firebase.google.com'; +/** Firebase IID backend path. */ +const FIREBASE_IID_PATH = '/v1/'; +/** Firebase IID request timeout duration in milliseconds. */ +const FIREBASE_IID_TIMEOUT = 10000; + +/** HTTP error codes raised by the backend server. */ +const ERROR_CODES: {[key: number]: string} = { + 400: 'Malformed installation ID argument.', + 401: 'Request not authorized.', + 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.', + 503: 'Backend servers are over capacity. Try again later.', +}; + +/** + * Class that provides mechanism to send requests to the FIS backend endpoints. + */ +export class FirebaseInstallationsRequestHandler { + + private readonly host: string = FIREBASE_IID_HOST; + private readonly timeout: number = FIREBASE_IID_TIMEOUT; + private readonly httpClient: AuthorizedHttpClient; + private path: string; + + /** + * @param app - The app used to fetch access tokens to sign API requests. + * + * @constructor + */ + constructor(private readonly app: App) { + this.httpClient = new AuthorizedHttpClient(app as FirebaseApp); + } + + 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(fid, 'DELETE')); + } + + /** + * Invokes the request handler based on the API settings object passed. + * + * @param apiSettings - The API endpoint settings to apply to request and response. + * @returns A promise that resolves when the request is complete. + */ + private invokeRequestHandler(apiSettings: ApiSettings): Promise { + return this.getPathPrefix() + .then((path) => { + const req: HttpRequestConfig = { + url: `https://${this.host}${path}${apiSettings.getEndpoint()}`, + method: apiSettings.getHttpMethod(), + timeout: this.timeout, + }; + return this.httpClient.send(req); + }) + .then(() => { + // return nothing on success + }) + .catch((err) => { + if (err instanceof HttpError) { + const response = err.response; + const errorMessage: string = (response.isJson() && 'error' in response.data) ? + response.data.error : response.text; + const template: string = ERROR_CODES[response.status]; + const message: string = template ? + `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. + throw err; + }); + } + + private getPathPrefix(): Promise { + if (this.path) { + return Promise.resolve(this.path); + } + + return utils.findProjectId(this.app) + .then((projectId) => { + if (!validator.isNonEmptyString(projectId)) { + // Assert for an explicit projct ID (either via AppOptions or the cert itself). + 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.', + ); + } + + this.path = FIREBASE_IID_PATH + `project/${projectId}/instanceId/`; + return this.path; + }); + } +} diff --git a/src/installations/installations.ts b/src/installations/installations.ts new file mode 100644 index 0000000000..c5408299da --- /dev/null +++ b/src/installations/installations.ts @@ -0,0 +1,66 @@ +/*! + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { App } from '../app/index'; +import { FirebaseInstallationsError, InstallationsClientErrorCode } from '../utils/error'; +import { FirebaseInstallationsRequestHandler } from './installations-request-handler'; +import * as validator from '../utils/validator'; + +/** + * The `Installations` service for the current app. + */ +export class Installations { + + private app_: App; + private requestHandler: FirebaseInstallationsRequestHandler; + + /** + * @param app - The app for this Installations service. + * @constructor + * @internal + */ + constructor(app: App) { + 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. + * + * @returns 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. + * + * @returns The app associated with this Installations instance. + */ + get app(): App { + return this.app_; + } +} diff --git a/src/instance-id/index.ts b/src/instance-id/index.ts new file mode 100644 index 0000000000..1a9c06d3bc --- /dev/null +++ b/src/instance-id/index.ts @@ -0,0 +1,74 @@ +/*! + * 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. + */ + +/** + * Firebase Instance ID service. + * + * @packageDocumentation + */ + +import { App, getApp } from '../app/index'; +import { InstanceId } from './instance-id'; +import { FirebaseApp } from '../app/firebase-app'; + +export { InstanceId }; + +/** + * Gets the {@link InstanceId} service for the default app or a given app. + * + * This API is deprecated. Developers are advised to use the + * {@link firebase-admin.installations#getInstallations} + * API to delete their instance IDs and Firebase installation IDs. + * + * `getInstanceId()` can be called with no arguments to access the default + * app's `InstanceId` service or as `getInstanceId(app)` to access the + * `InstanceId` service associated with a specific app. + * + * @example + * ```javascript + * // Get the Instance ID service for the default app + * const defaultInstanceId = getInstanceId(); + * ``` + * + * @example + * ```javascript + * // Get the Instance ID service for a given app + * const otherInstanceId = getInstanceId(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. + * + * @returns The default `InstanceId` service if + * no app is provided or the `InstanceId` service associated with the + * provided app. + * + * @deprecated Use {@link firebase-admin.installations#getInstallations} instead. + */ +export function getInstanceId(app?: App): InstanceId { + if (typeof app === 'undefined') { + app = getApp(); + } + + const firebaseApp: FirebaseApp = app as FirebaseApp; + return firebaseApp.getOrInitService('instanceId', (app) => new InstanceId(app)); +} + +export { FirebaseInstanceIdError, InstanceIdClientErrorCode } from '../utils/error'; diff --git a/src/instance-id/instance-id-namespace.ts b/src/instance-id/instance-id-namespace.ts new file mode 100644 index 0000000000..285828e472 --- /dev/null +++ b/src/instance-id/instance-id-namespace.ts @@ -0,0 +1,40 @@ +import { App } from '../app/index'; +import { InstanceId as TInstanceId } from './instance-id'; + +/** + * Gets the {@link firebase-admin.instance-id#InstanceId} service for the + * default app or a given app. + * + * `admin.instanceId()` can be called with no arguments to access the default + * app's `InstanceId` service or as `admin.instanceId(app)` to access the + * `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. + * + * @returns The default `InstanceId` service if + * no app is provided or the `InstanceId` service associated with the + * provided app. + */ +export declare function instanceId(app?: App): instanceId.InstanceId; + +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace instanceId { + /** + * Type alias to {@link firebase-admin.instance-id#InstanceId}. + */ + export type InstanceId = TInstanceId; +} diff --git a/src/instance-id/instance-id-request.ts b/src/instance-id/instance-id-request.ts deleted file mode 100644 index c3ad47b3ae..0000000000 --- a/src/instance-id/instance-id-request.ts +++ /dev/null @@ -1,110 +0,0 @@ -/*! - * 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 {FirebaseApp} from '../firebase-app'; -import {FirebaseError, FirebaseInstanceIdError, InstanceIdClientErrorCode} from '../utils/error'; -import { - HttpMethod, SignedApiRequestHandler, ApiSettings, -} from '../utils/api-request'; - -import * as validator from '../utils/validator'; - -/** Firebase IID backend host. */ -const FIREBASE_IID_HOST = 'console.firebase.google.com'; -/** Firebase IID backend port number. */ -const FIREBASE_IID_PORT = 443; -/** Firebase IID backend path. */ -const FIREBASE_IID_PATH = '/v1/'; -/** Firebase IID request timeout duration in milliseconds. */ -const FIREBASE_IID_TIMEOUT = 10000; - -/** HTTP error codes raised by the backend server. */ -const ERROR_CODES = { - 400: 'Malformed instance 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.', - 409: 'Already deleted.', - 429: 'Request throttled out by the backend server.', - 500: 'Internal server error.', - 503: 'Backend servers are over capacity. Try again later.', -}; - -/** - * Class that provides mechanism to send requests to the Firebase Instance ID backend endpoints. - */ -export class FirebaseInstanceIdRequestHandler { - - private host: string = FIREBASE_IID_HOST; - private port: number = FIREBASE_IID_PORT; - private timeout: number = FIREBASE_IID_TIMEOUT; - private signedApiRequestHandler: SignedApiRequestHandler; - private path: string; - - /** - * @param {FirebaseApp} app The app used to fetch access tokens to sign API requests. - * @param {string} projectId A Firebase project ID string. - * - * @constructor - */ - constructor(app: FirebaseApp, projectId: string) { - this.signedApiRequestHandler = new SignedApiRequestHandler(app); - this.path = FIREBASE_IID_PATH + `project/${projectId}/instanceId/`; - } - - 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.', - )); - } - return this.invokeRequestHandler(new ApiSettings(instanceId, '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 with the response. - */ - private invokeRequestHandler(apiSettings: ApiSettings): Promise { - const path: string = this.path + apiSettings.getEndpoint(); - const httpMethod: HttpMethod = apiSettings.getHttpMethod(); - return Promise.resolve() - .then(() => { - return this.signedApiRequestHandler.sendRequest( - this.host, this.port, path, httpMethod, undefined, undefined, this.timeout); - }) - .then((response) => { - return response; - }) - .catch((response) => { - const error = (typeof response === 'object' && 'error' in response) ? - response.error : response; - if (error instanceof FirebaseError) { - // In case of timeouts and other network errors, the API request handler returns a - // FirebaseError wrapped in the response. Simply throw it here. - throw error; - } - - const template: string = ERROR_CODES[response.statusCode]; - const message: string = template ? - `Instance ID "${apiSettings.getEndpoint()}": ${template}` : JSON.stringify(error); - throw new FirebaseInstanceIdError(InstanceIdClientErrorCode.API_ERROR, message); - }); - } -} diff --git a/src/instance-id/instance-id.ts b/src/instance-id/instance-id.ts index 3968a76e6c..e5dc2102b0 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. @@ -14,83 +14,76 @@ * 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 * as utils from '../utils/index'; +import { getInstallations } from '../installations'; +import { App } from '../app/index'; +import { + FirebaseInstallationsError, FirebaseInstanceIdError, + InstallationsClientErrorCode, InstanceIdClientErrorCode, +} from '../utils/error'; import * as validator from '../utils/validator'; /** - * Internals of an InstanceId service instance. + * The `InstanceId` service enables deleting the Firebase instance IDs + * associated with Firebase client app instances. + * + * @deprecated Use {@link firebase-admin.installations#Installations} instead. */ -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(); +export class InstanceId { - private app_: FirebaseApp; - private requestHandler: FirebaseInstanceIdRequestHandler; + private app_: App; /** - * @param {FirebaseApp} app The app for this InstanceId service. + * @param app - The app for this InstanceId service. * @constructor + * @internal */ - constructor(app: FirebaseApp) { + constructor(app: App) { 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.', - ); - } - - const projectId: string = utils.getProjectId(app); - 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 ' - + 'SDK with service account credentials or set project ID as an app option. ' - + 'Alternatively set the GCLOUD_PROJECT environment variable.', + 'First argument passed to instanceId() must be a valid Firebase app instance.', ); } this.app_ = app; - this.requestHandler = new FirebaseInstanceIdRequestHandler(app, projectId); } /** - * 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. + * 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 + * {@link https://firebase.google.com/support/privacy/manage-iids#delete_an_instance_id | + * Delete an Instance ID} + * for more information. + * + * @param instanceId - The instance ID to be deleted. + * + * @returns A promise fulfilled when the instance ID is deleted. */ public deleteInstanceId(instanceId: string): Promise { - return this.requestHandler.deleteInstanceId(instanceId) - .then((result) => { - // Return nothing on success + return getInstallations(this.app).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. + * @returns The app associated with this InstanceId instance. */ - get app(): FirebaseApp { + get app(): App { return this.app_; } } diff --git a/src/machine-learning/index.ts b/src/machine-learning/index.ts new file mode 100644 index 0000000000..433832c358 --- /dev/null +++ b/src/machine-learning/index.ts @@ -0,0 +1,73 @@ +/*! + * 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. + */ + +/** + * Firebase Machine Learning. + * + * @packageDocumentation + */ + +import { App, getApp } from '../app'; +import { FirebaseApp } from '../app/firebase-app'; +import { MachineLearning } from './machine-learning'; + +export { + MachineLearning, + ListModelsResult, + Model, + TFLiteModel, +} from './machine-learning'; +export { + GcsTfliteModelOptions, + ListModelsOptions, + ModelOptions, + ModelOptionsBase, +} from './machine-learning-api-client'; + +/** + * Gets the {@link MachineLearning} service for the default app or a given app. + * + * `getMachineLearning()` can be called with no arguments to access the + * default app's `MachineLearning` service or as `getMachineLearning(app)` to access + * the `MachineLearning` service associated with a specific app. + * + * @example + * ```javascript + * // Get the MachineLearning service for the default app + * const defaultMachineLearning = getMachineLearning(); + * ``` + * + * @example + * ```javascript + * // Get the MachineLearning service for a given app + * const otherMachineLearning = getMachineLearning(otherApp); + * ``` + * + * @param app - Optional app whose `MachineLearning` service to + * return. If not provided, the default `MachineLearning` service + * will be returned. + * + * @returns The default `MachineLearning` service if no app is provided or the + * `MachineLearning` service associated with the provided app. + */ +export function getMachineLearning(app?: App): MachineLearning { + if (typeof app === 'undefined') { + app = getApp(); + } + + const firebaseApp: FirebaseApp = app as FirebaseApp; + return firebaseApp.getOrInitService('machineLearning', (app) => new MachineLearning(app)); +} diff --git a/src/machine-learning/machine-learning-api-client.ts b/src/machine-learning/machine-learning-api-client.ts new file mode 100644 index 0000000000..7ae1c3436a --- /dev/null +++ b/src/machine-learning/machine-learning-api-client.ts @@ -0,0 +1,455 @@ +/*! + * 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 '../app'; +import { FirebaseApp } from '../app/firebase-app'; +import { + HttpRequestConfig, HttpClient, HttpError, AuthorizedHttpClient, ExponentialBackoffPoller +} from '../utils/api-request'; +import { PrefixedFirebaseError } from '../utils/error'; +import * as utils from '../utils/index'; +import * as validator from '../utils/validator'; +import { FirebaseMachineLearningError, MachineLearningErrorCode } from './machine-learning-utils'; + +/** + * Firebase ML Model input objects + */ +export interface ModelOptionsBase { + displayName?: string; + tags?: string[]; +} + +export interface GcsTfliteModelOptions extends ModelOptionsBase { + tfliteModel: { + gcsTfliteUri: string; + }; +} + +export type ModelOptions = ModelOptionsBase | GcsTfliteModelOptions; + +/** + * 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; +} + +const ML_V1BETA2_API = 'https://firebaseml.googleapis.com/v1beta2'; +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; +} + + +export type ModelUpdateOptions = ModelOptions & { state?: { published?: boolean }}; + +export function isGcsTfliteModelOptions(options: ModelOptions): options is GcsTfliteModelOptions { + const gcsUri = (options as GcsTfliteModelOptions)?.tfliteModel?.gcsTfliteUri; + return typeof gcsUri !== 'undefined' +} + +export interface ModelContent { + readonly displayName?: string; + readonly tags?: string[]; + readonly state?: { + readonly validationError?: StatusErrorResponse; + readonly published?: boolean; + }; + readonly tfliteModel?: { + readonly gcsTfliteUri?: string; + + readonly sizeBytes: number; + }; +} + +export interface ModelResponse extends ModelContent { + readonly name: string; + readonly createTime: string; + readonly updateTime: string; + readonly etag: string; + readonly modelHash?: string; + readonly activeOperations?: OperationResponse[]; +} + +export interface ListModelsResponse { + readonly models?: ModelResponse[]; + readonly nextPageToken?: string; +} + +export interface OperationResponse { + readonly name?: string; + readonly metadata?: {[key: string]: any}; + readonly done: boolean; + readonly error?: StatusErrorResponse; + readonly response?: ModelResponse; +} + + +/** + * Class that facilitates sending requests to the Firebase ML backend API. + * + * @internal + */ +export class MachineLearningApiClient { + + private readonly httpClient: HttpClient; + private projectIdPrefix?: string; + + constructor(private readonly app: App) { + if (!validator.isNonNullObject(app) || !('options' in app)) { + throw new FirebaseMachineLearningError( + 'invalid-argument', + 'First argument passed to admin.machineLearning() must be a valid ' + + 'Firebase app instance.'); + } + + this.httpClient = new AuthorizedHttpClient(app as FirebaseApp); + } + + public createModel(model: ModelOptions): Promise { + if (!validator.isNonNullObject(model) || + !validator.isNonEmptyString(model.displayName)) { + const err = new FirebaseMachineLearningError('invalid-argument', 'Invalid model content.'); + return Promise.reject(err); + } + return this.getProjectUrl() + .then((url) => { + const request: HttpRequestConfig = { + method: 'POST', + url: `${url}/models`, + data: model, + }; + return this.sendRequest(request); + }); + } + + public updateModel(modelId: string, model: ModelUpdateOptions, updateMask: string[]): Promise { + if (!validator.isNonEmptyString(modelId) || + !validator.isNonNullObject(model) || + !validator.isNonEmptyArray(updateMask)) { + const err = new FirebaseMachineLearningError('invalid-argument', 'Invalid model or mask content.'); + return Promise.reject(err); + } + return this.getProjectUrl() + .then((url) => { + const request: HttpRequestConfig = { + method: 'PATCH', + url: `${url}/models/${modelId}?updateMask=${updateMask.join()}`, + data: model, + }; + return this.sendRequest(request); + }); + } + + public getModel(modelId: string): Promise { + return Promise.resolve() + .then(() => { + return this.getModelName(modelId); + }) + .then((modelName) => { + return this.getResourceWithShortName(modelName); + }); + } + + public getOperation(operationName: string): Promise { + return Promise.resolve() + .then(() => { + return this.getResourceWithFullName(operationName); + }); + } + + public listModels(options: ListModelsOptions = {}): Promise { + if (!validator.isNonNullObject(options)) { + const err = new FirebaseMachineLearningError('invalid-argument', 'Invalid ListModelsOptions'); + return Promise.reject(err); + } + if (typeof options.filter !== 'undefined' && !validator.isNonEmptyString(options.filter)) { + const err = new FirebaseMachineLearningError('invalid-argument', 'Invalid list filter.'); + return Promise.reject(err); + } + if (typeof options.pageSize !== 'undefined') { + if (!validator.isNumber(options.pageSize)) { + const err = new FirebaseMachineLearningError('invalid-argument', 'Invalid page size.'); + return Promise.reject(err); + } + if (options.pageSize < 1 || options.pageSize > 100) { + const err = new FirebaseMachineLearningError( + 'invalid-argument', 'Page size must be between 1 and 100.'); + return Promise.reject(err); + } + } + if (typeof options.pageToken !== 'undefined' && !validator.isNonEmptyString(options.pageToken)) { + const err = new FirebaseMachineLearningError( + 'invalid-argument', 'Next page token must be a non-empty string.'); + return Promise.reject(err); + } + return this.getProjectUrl() + .then((url) => { + const request: HttpRequestConfig = { + method: 'GET', + url: `${url}/models`, + data: options, + }; + return this.sendRequest(request); + }); + } + + public deleteModel(modelId: string): Promise { + return this.getProjectUrl() + .then((url) => { + const modelName = this.getModelName(modelId); + const request: HttpRequestConfig = { + method: 'DELETE', + url: `${url}/${modelName}`, + }; + return this.sendRequest(request); + }); + } + + /** + * 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 Short name of the resource to get. e.g. 'models/12345' + * @returns {Promise} A promise that fulfills with the resource. + */ + private getResourceWithShortName(name: string): Promise { + return this.getProjectUrl() + .then((url) => { + const request: HttpRequestConfig = { + method: 'GET', + url: `${url}/${name}`, + }; + return this.sendRequest(request); + }); + } + + /** + * 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) + .then((resp) => { + return resp.data as T; + }) + .catch((err) => { + throw this.toFirebaseError(err); + }); + } + + private toFirebaseError(err: HttpError): PrefixedFirebaseError { + if (err instanceof PrefixedFirebaseError) { + return err; + } + + const response = err.response; + if (!response.isJson()) { + return new FirebaseMachineLearningError( + 'unknown-error', + `Unexpected response with status: ${response.status} and body: ${response.text}`); + } + + const error: Error = (response.data as ErrorResponse).error || {}; + let code: MachineLearningErrorCode = '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 FirebaseMachineLearningError(code, message); + } + + private getProjectUrl(): Promise { + return this.getProjectIdPrefix() + .then((projectIdPrefix) => { + return `${ML_V1BETA2_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 FirebaseMachineLearningError( + 'invalid-argument', + '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 getModelName(modelId: string): string { + if (!validator.isNonEmptyString(modelId)) { + throw new FirebaseMachineLearningError( + 'invalid-argument', 'Model ID must be a non-empty string.'); + } + + if (modelId.indexOf('/') !== -1) { + throw new FirebaseMachineLearningError( + 'invalid-argument', 'Model ID must not contain any "/" characters.'); + } + + return `models/${modelId}`; + } +} + +interface ErrorResponse { + error?: Error; +} + +interface Error { + code?: number; + message?: string; + status?: string; +} + +const ERROR_CODE_MAPPING: {[key: string]: MachineLearningErrorCode} = { + INVALID_ARGUMENT: 'invalid-argument', + NOT_FOUND: 'not-found', + RESOURCE_EXHAUSTED: 'resource-exhausted', + UNAUTHENTICATED: 'authentication-error', + UNKNOWN: 'unknown-error', +}; + +function extractModelId(resourceName: string): string { + return resourceName.split('/').pop()!; +} diff --git a/src/machine-learning/machine-learning-namespace.ts b/src/machine-learning/machine-learning-namespace.ts new file mode 100644 index 0000000000..7c5786fdbb --- /dev/null +++ b/src/machine-learning/machine-learning-namespace.ts @@ -0,0 +1,101 @@ +/*! + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { App } from '../app'; +import { + ListModelsResult as TListModelsResult, + MachineLearning as TMachineLearning, + Model as TModel, + TFLiteModel as TTFLiteModel, +} from './machine-learning'; +import { + GcsTfliteModelOptions as TGcsTfliteModelOptions, + ListModelsOptions as TListModelsOptions, + ModelOptions as TModelOptions, + ModelOptionsBase as TModelOptionsBase, +} from './machine-learning-api-client'; + +/** + * Gets the {@link firebase-admin.machine-learning#MachineLearning} service for the + * default app or a given app. + * + * `admin.machineLearning()` can be called with no arguments to access the + * default app's `MachineLearning` service or as `admin.machineLearning(app)` to access + * the `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. + * + * @returns The default `MachineLearning` service if no app is provided or the + * `MachineLearning` service associated with the provided app. + */ +export declare function machineLearning(app?: App): machineLearning.MachineLearning; + +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace machineLearning { + /** + * Type alias to {@link firebase-admin.machine-learning#ListModelsResult}. + */ + export type ListModelsResult = TListModelsResult; + + /** + * Type alias to {@link firebase-admin.machine-learning#MachineLearning}. + */ + export type MachineLearning = TMachineLearning; + + /** + * Type alias to {@link firebase-admin.machine-learning#Model}. + */ + export type Model = TModel; + + /** + * Type alias to {@link firebase-admin.machine-learning#TFLiteModel}. + */ + export type TFLiteModel = TTFLiteModel; + + /** + * Type alias to {@link firebase-admin.machine-learning#GcsTfliteModelOptions}. + */ + export type GcsTfliteModelOptions = TGcsTfliteModelOptions; + + /** + * Type alias to {@link firebase-admin.machine-learning#ListModelsOptions}. + */ + export type ListModelsOptions = TListModelsOptions; + + /** + * Type alias to {@link firebase-admin.machine-learning#ModelOptions}. + */ + export type ModelOptions = TModelOptions; + + /** + * Type alias to {@link firebase-admin.machine-learning#ModelOptionsBase}. + */ + export type ModelOptionsBase = TModelOptionsBase; +} diff --git a/src/machine-learning/machine-learning-utils.ts b/src/machine-learning/machine-learning-utils.ts new file mode 100644 index 0000000000..1202314e93 --- /dev/null +++ b/src/machine-learning/machine-learning-utils.ts @@ -0,0 +1,64 @@ +/*! + * 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 MachineLearningErrorCode = + 'already-exists' + | 'authentication-error' + | 'internal-error' + | 'invalid-argument' + | 'invalid-server-response' + | 'not-found' + | 'resource-exhausted' + | 'service-unavailable' + | 'unknown-error' + | 'cancelled' + | 'deadline-exceeded' + | 'permission-denied' + | 'failed-precondition' + | 'aborted' + | 'out-of-range' + | 'data-loss' + | 'unauthenticated'; + +export class FirebaseMachineLearningError extends PrefixedFirebaseError { + public static fromOperationError(code: number, message: string): FirebaseMachineLearningError { + switch (code) { + case 1: return new FirebaseMachineLearningError('cancelled', message); + case 2: return new FirebaseMachineLearningError('unknown-error', message); + case 3: return new FirebaseMachineLearningError('invalid-argument', message); + case 4: return new FirebaseMachineLearningError('deadline-exceeded', message); + case 5: return new FirebaseMachineLearningError('not-found', message); + case 6: return new FirebaseMachineLearningError('already-exists', message); + case 7: return new FirebaseMachineLearningError('permission-denied', message); + case 8: return new FirebaseMachineLearningError('resource-exhausted', message); + case 9: return new FirebaseMachineLearningError('failed-precondition', message); + case 10: return new FirebaseMachineLearningError('aborted', message); + case 11: return new FirebaseMachineLearningError('out-of-range', message); + case 13: return new FirebaseMachineLearningError('internal-error', message); + case 14: return new FirebaseMachineLearningError('service-unavailable', message); + case 15: return new FirebaseMachineLearningError('data-loss', message); + case 16: return new FirebaseMachineLearningError('unauthenticated', message); + default: + return new FirebaseMachineLearningError('unknown-error', message); + } + } + + constructor(code: MachineLearningErrorCode, message: string) { + super('machine-learning', code, message); + } +} diff --git a/src/machine-learning/machine-learning.ts b/src/machine-learning/machine-learning.ts new file mode 100644 index 0000000000..98b956c85c --- /dev/null +++ b/src/machine-learning/machine-learning.ts @@ -0,0 +1,410 @@ +/*! + * 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 '../app'; +import { getStorage } from '../storage/index'; +import { FirebaseError } from '../utils/error'; +import * as validator from '../utils/validator'; +import { deepCopy } from '../utils/deep-copy'; +import * as utils from '../utils'; +import { + MachineLearningApiClient, ModelResponse, ModelUpdateOptions, isGcsTfliteModelOptions, + ListModelsOptions, ModelOptions, +} from './machine-learning-api-client'; +import { FirebaseMachineLearningError } from './machine-learning-utils'; + +/** 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; +} + +/** + * A TensorFlow Lite Model output object + */ +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 Firebase `MachineLearning` service interface. + */ +export class MachineLearning { + + private readonly client: MachineLearningApiClient; + private readonly appInternal: App; + + /** + * @param app - The app for this ML service. + * @constructor + * @internal + */ + constructor(app: App) { + if (!validator.isNonNullObject(app) || !('options' in app)) { + throw new FirebaseError({ + code: 'machine-learning/invalid-argument', + message: 'First argument passed to admin.machineLearning() must be a ' + + 'valid Firebase app instance.', + }); + } + + this.appInternal = app; + this.client = new MachineLearningApiClient(app); + } + + /** + * The {@link firebase-admin.app#App} associated with the current `MachineLearning` + * service instance. + */ + public get app(): App { + return this.appInternal; + } + + /** + * Creates a model in the current Firebase project. + * + * @param model - The model to create. + * + * @returns A Promise fulfilled with the created model. + */ + public createModel(model: ModelOptions): Promise { + return this.signUrlIfPresent(model) + .then((modelContent) => this.client.createModel(modelContent)) + .then((operation) => this.client.handleOperation(operation)) + .then((modelResponse) => new Model(modelResponse, this.client)); + } + + /** + * Updates a model's metadata or model file. + * + * @param modelId - The ID of the model to update. + * @param model - The model fields to update. + * + * @returns A Promise fulfilled with the updated model. + */ + public updateModel(modelId: string, model: ModelOptions): Promise { + const updateMask = utils.generateUpdateMask(model); + return this.signUrlIfPresent(model) + .then((modelContent) => this.client.updateModel(modelId, modelContent, updateMask)) + .then((operation) => this.client.handleOperation(operation)) + .then((modelResponse) => new Model(modelResponse, this.client)); + } + + /** + * Publishes a Firebase ML model. + * + * A published model can be downloaded to client apps. + * + * @param modelId - The ID of the model to publish. + * + * @returns A Promise fulfilled with the published model. + */ + public publishModel(modelId: string): Promise { + return this.setPublishStatus(modelId, true); + } + + /** + * Unpublishes a Firebase ML model. + * + * @param modelId - The ID of the model to unpublish. + * + * @returns A Promise fulfilled with the unpublished model. + */ + public unpublishModel(modelId: string): Promise { + return this.setPublishStatus(modelId, false); + } + + /** + * Gets the model specified by the given ID. + * + * @param modelId - The ID of the model to get. + * + * @returns A Promise fulfilled with the model object. + */ + public getModel(modelId: string): Promise { + return this.client.getModel(modelId) + .then((modelResponse) => new Model(modelResponse, this.client)); + } + + /** + * Lists the current project's models. + * + * @param options - The listing options. + * + * @returns 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. + */ + public listModels(options: ListModelsOptions = {}): Promise { + return this.client.listModels(options) + .then((resp) => { + if (!validator.isNonNullObject(resp)) { + throw new FirebaseMachineLearningError( + 'invalid-argument', + `Invalid ListModels response: ${JSON.stringify(resp)}`); + } + let models: Model[] = []; + if (resp.models) { + models = resp.models.map((rs) => new Model(rs, this.client)); + } + const result: { models: Model[]; pageToken?: string } = { models }; + if (resp.nextPageToken) { + result.pageToken = resp.nextPageToken; + } + return result; + }); + } + + /** + * Deletes a model from the current project. + * + * @param modelId - The ID of the model to delete. + */ + public deleteModel(modelId: string): Promise { + return this.client.deleteModel(modelId); + } + + private setPublishStatus(modelId: string, publish: boolean): Promise { + const updateMask = ['state.published']; + const options: ModelUpdateOptions = { state: { published: publish } }; + return this.client.updateModel(modelId, options, updateMask) + .then((operation) => this.client.handleOperation(operation)) + .then((modelResponse) => new Model(modelResponse, this.client)); + } + + private signUrlIfPresent(options: ModelOptions): Promise { + const modelOptions = deepCopy(options); + if (isGcsTfliteModelOptions(modelOptions)) { + return this.signUrl(modelOptions.tfliteModel.gcsTfliteUri) + .then((uri: string) => { + modelOptions.tfliteModel.gcsTfliteUri = uri; + return modelOptions; + }) + .catch((err: Error) => { + throw new FirebaseMachineLearningError( + 'internal-error', + `Error during signing upload url: ${err.message}`); + }); + } + return Promise.resolve(modelOptions); + } + + private signUrl(unsignedUrl: string): Promise { + const MINUTES_IN_MILLIS = 60 * 1000; + const URL_VALID_DURATION = 10 * MINUTES_IN_MILLIS; + + const gcsRegex = /^gs:\/\/([a-z0-9_.-]{3,63})\/(.+)$/; + const matches = gcsRegex.exec(unsignedUrl); + if (!matches) { + throw new FirebaseMachineLearningError( + 'invalid-argument', + `Invalid unsigned url: ${unsignedUrl}`); + } + const bucketName = matches[1]; + const blobName = matches[2]; + const bucket = getStorage(this.app).bucket(bucketName); + const blob = bucket.file(blobName); + return blob.getSignedUrl({ + action: 'read', + expires: Date.now() + URL_VALID_DURATION, + }).then((signUrl) => signUrl[0]); + } +} + +/** + * A Firebase ML Model output object. + */ +export class Model { + private model: ModelResponse; + private readonly client?: MachineLearningApiClient; + + /** + * @internal + */ + constructor(model: ModelResponse, client: MachineLearningApiClient) { + this.model = Model.validateAndClone(model); + this.client = client; + } + + /** The ID of the model. */ + get modelId(): string { + return extractModelId(this.model.name); + } + + /** + * The model's name. This is the name you use from your app to load the + * model. + */ + get displayName(): string { + return this.model.displayName!; + } + + /** + * The model's tags, which can be used to group or filter models in list + * operations. + */ + get tags(): string[] { + return this.model.tags || []; + } + + /** The timestamp of the model's creation. */ + get createTime(): string { + return new Date(this.model.createTime).toUTCString(); + } + + /** The timestamp of the model's most recent update. */ + get updateTime(): string { + return new Date(this.model.updateTime).toUTCString(); + } + + /** Error message when model validation fails. */ + get validationError(): string | undefined { + return this.model.state?.validationError?.message; + } + + /** True if the model is published. */ + get published(): boolean { + return this.model.state?.published || false; + } + + /** + * The ETag identifier of the current version of the model. This value + * changes whenever you update any of the model's properties. + */ + get etag(): string { + return this.model.etag; + } + + /** + * The hash of the model's `tflite` file. This value changes only when + * you upload a new TensorFlow Lite model. + */ + get modelHash(): string | undefined { + return this.model.modelHash; + } + + /** Metadata about the model's TensorFlow Lite model file. */ + get tfliteModel(): TFLiteModel | undefined { + // Make a copy so people can't directly modify the private this.model object. + return deepCopy(this.model.tfliteModel); + } + + /** + * True if the model is locked by a server-side operation. You can't make + * changes to a locked model. See {@link Model.waitForUnlocked}. + */ + public get locked(): boolean { + return (this.model.activeOperations?.length ?? 0) > 0; + } + + /** + * Return the model as a JSON object. + */ + 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; + } + + if (this.modelHash) { + jsonModel['modelHash'] = this.modelHash; + } + + if (this.tfliteModel) { + jsonModel['tfliteModel'] = this.tfliteModel; + } + + return jsonModel; + } + + /** + * Wait for the model to be unlocked. + * + * @param maxTimeMillis - The maximum time in milliseconds to wait. + * If not specified, a default maximum of 2 minutes is used. + * + * @returns A promise that resolves when the model is unlocked + * or the maximum wait time has passed. + */ + 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 of {gcsTfliteUri} + if (model.tfliteModel && + !validator.isNonEmptyString(model.tfliteModel.gcsTfliteUri)) { + // 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; + } +} + +function extractModelId(resourceName: string): string { + return resourceName.split('/').pop()!; +} diff --git a/src/messaging/batch-request-internal.ts b/src/messaging/batch-request-internal.ts new file mode 100644 index 0000000000..d3f18c9942 --- /dev/null +++ b/src/messaging/batch-request-internal.ts @@ -0,0 +1,141 @@ +/*! + * 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 { + HttpClient, HttpRequestConfig, HttpResponse, parseHttpResponse, +} from '../utils/api-request'; +import { FirebaseAppError, AppErrorCodes } from '../utils/error'; + +const PART_BOUNDARY = '__END_OF_PART__'; +const TEN_SECONDS_IN_MILLIS = 15000; + +/** + * Represents a request that can be sent as part of an HTTP batch request. + */ +export interface SubRequest { + url: string; + body: object; + headers?: {[key: string]: any}; +} + +/** + * An HTTP client that can be used to make batch requests. This client is not tied to any service + * (FCM or otherwise). Therefore it can be used to make batch requests to any service that allows + * it. If this requirement ever arises we can move this implementation to the utils module + * where it can be easily shared among other modules. + */ +export class BatchRequestClient { + + /** + * @param {HttpClient} httpClient The client that will be used to make HTTP calls. + * @param {string} batchUrl The URL that accepts batch requests. + * @param {object=} commonHeaders Optional headers that will be included in all requests. + * + * @constructor + */ + constructor( + private readonly httpClient: HttpClient, + private readonly batchUrl: string, + private readonly commonHeaders?: object) { + } + + /** + * Sends the given array of sub requests as a single batch, and parses the results into an array + * of HttpResponse objects. + * + * @param requests - An array of sub requests to send. + * @returns A promise that resolves when the send operation is complete. + */ + public send(requests: SubRequest[]): Promise { + requests = requests.map((req) => { + req.headers = Object.assign({}, this.commonHeaders, req.headers); + return req; + }); + const requestHeaders = { + 'Content-Type': `multipart/mixed; boundary=${PART_BOUNDARY}`, + }; + const request: HttpRequestConfig = { + method: 'POST', + url: this.batchUrl, + data: this.getMultipartPayload(requests), + headers: Object.assign({}, this.commonHeaders, requestHeaders), + timeout: TEN_SECONDS_IN_MILLIS, + }; + return this.httpClient.send(request).then((response) => { + if (!response.multipart) { + throw new FirebaseAppError(AppErrorCodes.INTERNAL_ERROR, 'Expected a multipart response.'); + } + return response.multipart.map((buff) => { + return parseHttpResponse(buff, request); + }); + }); + } + + private getMultipartPayload(requests: SubRequest[]): Buffer { + let buffer = ''; + requests.forEach((request: SubRequest, idx: number) => { + buffer += createPart(request, PART_BOUNDARY, idx); + }); + buffer += `--${PART_BOUNDARY}--\r\n`; + return Buffer.from(buffer, 'utf-8'); + } +} + +/** + * Creates a single part in a multipart HTTP request body. The part consists of several headers + * followed by the serialized sub request as the body. As per the requirements of the FCM batch + * API, sets the content-type header to application/http, and the content-transfer-encoding to + * binary. + * + * @param request - A sub request that will be used to populate the part. + * @param boundary - Multipart boundary string. + * @param idx - An index number that is used to set the content-id header. + * @returns The part as a string that can be included in the HTTP body. + */ +function createPart(request: SubRequest, boundary: string, idx: number): string { + const serializedRequest: string = serializeSubRequest(request); + let part = `--${boundary}\r\n`; + part += `Content-Length: ${serializedRequest.length}\r\n`; + part += 'Content-Type: application/http\r\n'; + part += `content-id: ${idx + 1}\r\n`; + part += 'content-transfer-encoding: binary\r\n'; + part += '\r\n'; + part += `${serializedRequest}\r\n`; + return part; +} + +/** + * Serializes a sub request into a string that can be embedded in a multipart HTTP request. The + * format of the string is the wire format of a typical HTTP request, consisting of a header and a + * body. + * + * @param request - The sub request to be serialized. + * @returns String representation of the SubRequest. + */ +function serializeSubRequest(request: SubRequest): string { + const requestBody: string = JSON.stringify(request.body); + let messagePayload = `POST ${request.url} HTTP/1.1\r\n`; + messagePayload += `Content-Length: ${requestBody.length}\r\n`; + messagePayload += 'Content-Type: application/json; charset=UTF-8\r\n'; + if (request.headers) { + Object.keys(request.headers).forEach((key) => { + messagePayload += `${key}: ${request.headers![key]}\r\n`; + }); + } + messagePayload += '\r\n'; + messagePayload += requestBody; + return messagePayload; +} diff --git a/src/messaging/index.ts b/src/messaging/index.ts new file mode 100644 index 0000000000..298a2d5f10 --- /dev/null +++ b/src/messaging/index.ts @@ -0,0 +1,104 @@ +/*! + * 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. + */ + +/** + * Firebase Cloud Messaging (FCM). + * + * @packageDocumentation + */ + +import { App, getApp } from '../app'; +import { FirebaseApp } from '../app/firebase-app'; +import { Messaging } from './messaging'; + +export { + Messaging, +} from './messaging'; + +export { + AndroidConfig, + AndroidFcmOptions, + AndroidNotification, + ApnsConfig, + ApnsFcmOptions, + ApnsPayload, + Aps, + ApsAlert, + BaseMessage, + BatchResponse, + CriticalSound, + ConditionMessage, + FcmOptions, + LightSettings, + Message, + MessagingTopicManagementResponse, + MulticastMessage, + Notification, + SendResponse, + TokenMessage, + TopicMessage, + WebpushConfig, + WebpushFcmOptions, + WebpushNotification, + + // Legacy APIs + DataMessagePayload, + MessagingConditionResponse, + MessagingDeviceGroupResponse, + MessagingDeviceResult, + MessagingDevicesResponse, + MessagingOptions, + MessagingPayload, + MessagingTopicResponse, + NotificationMessagePayload, +} from './messaging-api'; + +/** + * Gets the {@link Messaging} service for the default app or a given app. + * + * `admin.messaging()` can be called with no arguments to access the default + * app's `Messaging` service or as `admin.messaging(app)` to access the + * `Messaging` service associated with aspecific app. + * + * @example + * ```javascript + * // Get the Messaging service for the default app + * const defaultMessaging = getMessaging(); + * ``` + * + * @example + * ```javascript + * // Get the Messaging service for a given app + * const otherMessaging = getMessaging(otherApp); + * ``` + * + * @param app - Optional app whose `Messaging` service to + * return. If not provided, the default `Messaging` service will be returned. + * + * @returns The default `Messaging` service if no + * app is provided or the `Messaging` service associated with the provided + * app. + */ +export function getMessaging(app?: App): Messaging { + if (typeof app === 'undefined') { + app = getApp(); + } + + const firebaseApp: FirebaseApp = app as FirebaseApp; + return firebaseApp.getOrInitService('messaging', (app) => new Messaging(app)); +} + +export { FirebaseMessagingError, MessagingClientErrorCode } from '../utils/error'; diff --git a/src/messaging/messaging-api-request-internal.ts b/src/messaging/messaging-api-request-internal.ts new file mode 100644 index 0000000000..90be03181f --- /dev/null +++ b/src/messaging/messaging-api-request-internal.ts @@ -0,0 +1,174 @@ +/*! + * @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 '../app'; +import { FirebaseApp } from '../app/firebase-app'; +import { + HttpMethod, AuthorizedHttpClient, HttpRequestConfig, HttpError, HttpResponse, +} from '../utils/api-request'; +import { createFirebaseError, getErrorCode } from './messaging-errors-internal'; +import { SubRequest, BatchRequestClient } from './batch-request-internal'; +import { getSdkVersion } from '../utils/index'; +import { SendResponse, BatchResponse } from './messaging-api'; + + +// FCM backend constants +const FIREBASE_MESSAGING_TIMEOUT = 15000; +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/${getSdkVersion()}`, +}; +const LEGACY_FIREBASE_MESSAGING_HEADERS = { + 'X-Firebase-Client': `fire-admin-node/${getSdkVersion()}`, + 'access_token_auth': 'true', +}; + + +/** + * Class that provides a mechanism to send requests to the Firebase Cloud Messaging backend. + */ +export class FirebaseMessagingRequestHandler { + private readonly httpClient: AuthorizedHttpClient; + private readonly batchClient: BatchRequestClient; + + /** + * @param app - The app used to fetch access tokens to sign API requests. + * @constructor + */ + constructor(app: App) { + this.httpClient = new AuthorizedHttpClient(app as FirebaseApp); + this.batchClient = new BatchRequestClient( + this.httpClient, FIREBASE_MESSAGING_BATCH_URL, FIREBASE_MESSAGING_HEADERS); + } + + /** + * Invokes the request handler with the provided request data. + * + * @param host - The host to which to send the request. + * @param path - The path to which to send the request. + * @param requestData - The request data. + * @returns A promise that resolves with the response. + */ + public invokeRequestHandler(host: string, path: string, requestData: object): Promise { + const request: HttpRequestConfig = { + method: FIREBASE_MESSAGING_HTTP_METHOD, + url: `https://${host}${path}`, + data: requestData, + headers: LEGACY_FIREBASE_MESSAGING_HEADERS, + timeout: FIREBASE_MESSAGING_TIMEOUT, + }; + return this.httpClient.send(request).then((response) => { + // Send non-JSON responses to the catch() below where they will be treated as errors. + if (!response.isJson()) { + throw new HttpError(response); + } + + // Check for backend errors in the response. + const errorCode = getErrorCode(response.data); + if (errorCode) { + throw new HttpError(response); + } + + // Return entire response. + return response.data; + }) + .catch((err) => { + if (err instanceof HttpError) { + throw createFirebaseError(err); + } + // Re-throw the error if it already has the proper format. + throw err; + }); + } + + /** + * Invokes the request handler with the provided request data. + * + * @param host - The host to which to send the request. + * @param path - The path to which to send the request. + * @param requestData - The request data. + * @returns A promise that resolves with the {@link SendResponse}. + */ + public invokeRequestHandlerForSendResponse(host: string, path: string, requestData: object): Promise { + const request: HttpRequestConfig = { + method: FIREBASE_MESSAGING_HTTP_METHOD, + url: `https://${host}${path}`, + data: requestData, + headers: LEGACY_FIREBASE_MESSAGING_HEADERS, + timeout: FIREBASE_MESSAGING_TIMEOUT, + }; + return this.httpClient.send(request).then((response) => { + return this.buildSendResponse(response); + }) + .catch((err) => { + if (err instanceof HttpError) { + return this.buildSendResponseFromError(err); + } + // Re-throw the error if it already has the proper format. + throw err; + }); + } + + /** + * Sends the given array of sub requests as a single batch to FCM, and parses the result into + * a BatchResponse object. + * + * @param requests - An array of sub requests to send. + * @returns A promise that resolves when the send operation is complete. + */ + public sendBatchRequest(requests: SubRequest[]): Promise { + return this.batchClient.send(requests) + .then((responses: HttpResponse[]) => { + return responses.map((part: HttpResponse) => { + return this.buildSendResponse(part); + }); + }).then((responses: SendResponse[]) => { + const successCount: number = responses.filter((resp) => resp.success).length; + return { + responses, + successCount, + failureCount: responses.length - successCount, + }; + }).catch((err) => { + if (err instanceof HttpError) { + throw createFirebaseError(err); + } + // Re-throw the error if it already has the proper format. + throw err; + }); + } + + private buildSendResponse(response: HttpResponse): SendResponse { + const result: SendResponse = { + success: response.status === 200, + }; + if (result.success) { + result.messageId = response.data.name; + } else { + result.error = createFirebaseError(new HttpError(response)); + } + return result; + } + + private buildSendResponseFromError(err: HttpError): SendResponse { + return { + success: false, + error: createFirebaseError(err) + }; + } +} diff --git a/src/messaging/messaging-api-request.ts b/src/messaging/messaging-api-request.ts deleted file mode 100644 index d9e523aac2..0000000000 --- a/src/messaging/messaging-api-request.ts +++ /dev/null @@ -1,162 +0,0 @@ -/*! - * 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 {FirebaseApp} from '../firebase-app'; -import {HttpMethod, SignedApiRequestHandler} from '../utils/api-request'; -import {FirebaseError, FirebaseMessagingError, MessagingClientErrorCode} from '../utils/error'; - -import * as validator from '../utils/validator'; - -// FCM backend constants -const FIREBASE_MESSAGING_PORT = 443; -const FIREBASE_MESSAGING_TIMEOUT = 10000; -const FIREBASE_MESSAGING_HTTP_METHOD: HttpMethod = 'POST'; -const FIREBASE_MESSAGING_HEADERS = { - 'Content-Type': 'application/json', - 'Sdk-Version': 'Node/Admin/', - 'access_token_auth': 'true', -}; - - -/** - * Class that provides a mechanism to send requests to the Firebase Cloud Messaging backend. - */ -export class FirebaseMessagingRequestHandler { - private signedApiRequestHandler: SignedApiRequestHandler; - - /** - * @param {object} response The response to check for errors. - * @return {string|null} The error code if present; null otherwise. - */ - private static getErrorCode(response: any): string | null { - if (validator.isNonNullObject(response) && 'error' in response) { - if (typeof response.error === 'string') { - return response.error; - } else if ('status' in response.error) { - return response.error.status; - } else { - return response.error.message; - } - } - - return null; - } - - /** - * Extracts error message from the given response object. - * - * @param {object} response The response to check for errors. - * @return {string|null} The error message if present; null otherwise. - */ - private static getErrorMessage(response: any): string | null { - if (validator.isNonNullObject(response) && - 'error' in response && - validator.isNonEmptyString(response.error.message)) { - return response.error.message; - } - return null; - } - - /** - * @param {FirebaseApp} app The app used to fetch access tokens to sign API requests. - * @constructor - */ - constructor(app: FirebaseApp) { - this.signedApiRequestHandler = new SignedApiRequestHandler(app); - } - - /** - * Invokes the request handler with the provided request data. - * - * @param {string} host The host to which to send the request. - * @param {string} path The path to which to send the request. - * @param {object} requestData The request data. - * @return {Promise} A promise that resolves with the response. - */ - public invokeRequestHandler(host: string, path: string, requestData: object): Promise { - return this.signedApiRequestHandler.sendRequest( - host, - FIREBASE_MESSAGING_PORT, - path, - FIREBASE_MESSAGING_HTTP_METHOD, - requestData, - FIREBASE_MESSAGING_HEADERS, - FIREBASE_MESSAGING_TIMEOUT, - ).then((response) => { - // Send non-JSON responses to the catch() below where they will be treated as errors. - if (typeof response === 'string') { - return Promise.reject({ - error: response, - statusCode: 200, - }); - } - - // Check for backend errors in the response. - const errorCode = FirebaseMessagingRequestHandler.getErrorCode(response); - if (errorCode) { - return Promise.reject({ - error: response, - statusCode: 200, - }); - } - - // Return entire response. - return response; - }) - .catch((response: { statusCode: number, error: string | object }) => { - // Re-throw the error if it already has the proper format. - if (response instanceof FirebaseError) { - throw response; - } else if (response.error instanceof FirebaseError) { - throw response.error; - } - - // Add special handling for non-JSON responses. - if (typeof response.error === 'string') { - let error; - switch (response.statusCode) { - case 400: - error = MessagingClientErrorCode.INVALID_ARGUMENT; - break; - case 401: - case 403: - error = MessagingClientErrorCode.AUTHENTICATION_ERROR; - break; - case 500: - error = MessagingClientErrorCode.INTERNAL_ERROR; - break; - case 503: - error = MessagingClientErrorCode.SERVER_UNAVAILABLE; - break; - default: - // Treat non-JSON responses with unexpected status codes as unknown errors. - error = MessagingClientErrorCode.UNKNOWN_ERROR; - } - - throw new FirebaseMessagingError({ - code: error.code, - message: `${ error.message } Raw server response: "${ response.error }". Status code: ` + - `${ response.statusCode }.`, - }); - } - - // For JSON responses, map the server response to a client-side error. - const errorCode = FirebaseMessagingRequestHandler.getErrorCode(response.error); - const errorMessage = FirebaseMessagingRequestHandler.getErrorMessage(response.error); - throw FirebaseMessagingError.fromServerError(errorCode, errorMessage, response.error); - }); - } -} diff --git a/src/messaging/messaging-api.ts b/src/messaging/messaging-api.ts new file mode 100644 index 0000000000..29f7f1fdc9 --- /dev/null +++ b/src/messaging/messaging-api.ts @@ -0,0 +1,1118 @@ +/*! + * @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 { FirebaseArrayIndexError, FirebaseError } from '../app/index'; + +export interface BaseMessage { + data?: { [key: string]: string }; + notification?: Notification; + android?: AndroidConfig; + webpush?: WebpushConfig; + apns?: ApnsConfig; + fcmOptions?: FcmOptions; +} + +export interface TokenMessage extends BaseMessage { + token: string; +} + +export interface TopicMessage extends BaseMessage { + topic: string; +} + +export interface ConditionMessage extends BaseMessage { + condition: string; +} + +/** + * Payload for the {@link 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 {@link 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[]; +} + +/** + * A notification that can be included in {@link 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 Message}. + */ +export interface WebpushConfig { + + /** + * A collection of WebPush headers. Header values must be strings. + * + * See {@link https://tools.ietf.org/html/rfc8030#section-5 | WebPush specification} + * 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 WebpushConfig}. This supports most of the standard + * options as defined in the Web Notification + * {@link https://developer.mozilla.org/en-US/docs/Web/API/notification/Notification | specification}. + */ +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 Message}. Refer to + * {@link https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingwithAPNs.html | + * Apple documentation} 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]: any; +} + +/** + * Represents the {@link https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html | + * aps dictionary} 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 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 + * {@link 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 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 {@link https://developer.android.com/training/wearables/notifications/bridger#existing-method-of-preventing-bridging | + * Wear OS guides}. + */ + 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 {@link https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/config.xml | + * 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 {@link https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/config.xml | + * 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 {@link https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/config.xml | + * 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 {@link https://developer.android.com/training/notify-user/badges | + * NotificationBadge}. + * 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 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 {@link https://firebase.google.com/docs/cloud-messaging/send-message | Build send requests} + * 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 {@link https://firebase.google.com/docs/cloud-messaging/send-message | Build send requests} + * 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 + * {@link https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html | + * Payload Key Reference} and + * {@link https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CreatingtheNotificationPayload.html#//apple_ref/doc/uid/TP40008194-CH10-SW9 | + * Localizing the Content of Your Remote Notifications} for more information. + * + * **Android:** See + * {@link http://developer.android.com/guide/topics/resources/string-resource.html | String Resources} + * 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 + * {@link https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html | + * Payload Key Reference} and + * {@link https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CreatingtheNotificationPayload.html#//apple_ref/doc/uid/TP40008194-CH10-SW9 | + * Localizing the Content of Your Remote Notifications} for more information. + * + * **Android:** See + * {@link http://developer.android.com/guide/topics/resources/string-resource.html#FormattingAndStyling | + * Formatting and Styling} 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 + * {@link https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html | + * Payload Key Reference} and + * {@link https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CreatingtheNotificationPayload.html#//apple_ref/doc/uid/TP40008194-CH10-SW9 | + * Localizing the Content of Your Remote Notifications} for more information. + * + * **Android:** See + * {@link http://developer.android.com/guide/topics/resources/string-resource.html | String Resources} + * 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 + * {@link https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html | + * Payload Key Reference} and + * {@link https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CreatingtheNotificationPayload.html#//apple_ref/doc/uid/TP40008194-CH10-SW9 | + * Localizing the Content of Your Remote Notifications} for more information. + * + * **Android:** See + * {@link http://developer.android.com/guide/topics/resources/string-resource.html#FormattingAndStyling | + * Formatting and Styling} 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 {@link https://firebase.google.com/docs/cloud-messaging/send-message | Build send requests} + * 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 {@link https://firebase.google.com/docs/cloud-messaging/send-message | Build send requests} + * 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 + * {@link https://firebase.google.com/docs/cloud-messaging/concept-options#setting-the-priority-of-a-message | + * 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 + * {@link https://firebase.google.com/docs/cloud-messaging/concept-options#ttl | Setting the lifespan of a message}. + * + * **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 + * {@link https://developer.apple.com/reference/usernotifications/unnotificationserviceextension | + * Notification Service app extension}. + * + * 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 + * + * @deprecated Returned by {@link Messaging#sendToDevice}, which is also deprecated. + */ +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 + * {@link https://firebase.google.com/docs/cloud-messaging/admin/send-messages#send_to_individual_devices | + * Send to individual devices} for code samples and detailed documentation. + * + * @deprecated Returned by {@link Messaging.sendToDevice}, which is also deprecated. + */ +export interface MessagingDevicesResponse { + canonicalRegistrationTokenCount: number; + failureCount: number; + multicastId: number; + results: MessagingDeviceResult[]; + successCount: number; +} + +/** + * Interface representing the server response from the {@link Messaging.sendToDeviceGroup} + * method. + * + * See + * {@link https://firebase.google.com/docs/cloud-messaging/send-message?authuser=0#send_messages_to_device_groups | + * Send messages to device groups} for code samples and detailed documentation. + * + * @deprecated Returned by {@link Messaging.sendToDeviceGroup}, which is also deprecated. + */ +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.sendToTopic} method. + * + * See + * {@link https://firebase.google.com/docs/cloud-messaging/admin/send-messages#send_to_a_topic | + * 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.sendToCondition} method. + * + * See + * {@link https://firebase.google.com/docs/cloud-messaging/admin/send-messages#send_to_a_condition | + * 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.subscribeToTopic} and {@link Messaging.unsubscribeFromTopic} + * methods. + * + * See + * {@link https://firebase.google.com/docs/cloud-messaging/manage-topics | + * Manage topics from the server} 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 {@link MessagingTopicManagementResponse.failureCount}. + */ + errors: FirebaseArrayIndexError[]; +} + +/** + * Interface representing the server response from the + * {@link Messaging.sendAll} and {@link Messaging.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-errors-internal.ts b/src/messaging/messaging-errors-internal.ts new file mode 100644 index 0000000000..a9dd8794af --- /dev/null +++ b/src/messaging/messaging-errors-internal.ts @@ -0,0 +1,105 @@ +/*! + * 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 { HttpError } from '../utils/api-request'; +import { FirebaseMessagingError, MessagingClientErrorCode } from '../utils/error'; +import * as validator from '../utils/validator'; + +/** + * Creates a new FirebaseMessagingError by extracting the error code, message and other relevant + * details from an HTTP error response. + * + * @param err - The HttpError to convert into a Firebase error + * @returns A Firebase error that can be returned to the user. + */ +export function createFirebaseError(err: HttpError): FirebaseMessagingError { + if (err.response.isJson()) { + // For JSON responses, map the server response to a client-side error. + const json = err.response.data; + const errorCode = getErrorCode(json); + const errorMessage = getErrorMessage(json); + return FirebaseMessagingError.fromServerError(errorCode, errorMessage, json); + } + + // Non-JSON response + let error: {code: string; message: string}; + switch (err.response.status) { + case 400: + error = MessagingClientErrorCode.INVALID_ARGUMENT; + break; + case 401: + case 403: + error = MessagingClientErrorCode.AUTHENTICATION_ERROR; + break; + case 500: + error = MessagingClientErrorCode.INTERNAL_ERROR; + break; + case 503: + error = MessagingClientErrorCode.SERVER_UNAVAILABLE; + break; + default: + // Treat non-JSON responses with unexpected status codes as unknown errors. + error = MessagingClientErrorCode.UNKNOWN_ERROR; + } + return new FirebaseMessagingError({ + code: error.code, + message: `${ error.message } Raw server response: "${ err.response.text }". Status code: ` + + `${ err.response.status }.`, + }); +} + +/** + * @param response - The response to check for errors. + * @returns The error code if present; null otherwise. + */ +export function getErrorCode(response: any): string | null { + if (validator.isNonNullObject(response) && 'error' in response) { + const error = response.error; + if (validator.isString(error)) { + return error; + } + if (validator.isArray(error.details)) { + const fcmErrorType = 'type.googleapis.com/google.firebase.fcm.v1.FcmError'; + for (const element of error.details) { + if (element['@type'] === fcmErrorType) { + return element.errorCode; + } + } + } + if ('status' in error) { + return error.status; + } else { + return error.message; + } + } + + return null; +} + +/** + * Extracts error message from the given response object. + * + * @param response - The response to check for errors. + * @returns The error message if present; null otherwise. + */ +function getErrorMessage(response: any): string | null { + if (validator.isNonNullObject(response) && + 'error' in response && + validator.isNonEmptyString((response as any).error.message)) { + return (response as any).error.message; + } + return null; +} diff --git a/src/messaging/messaging-internal.ts b/src/messaging/messaging-internal.ts new file mode 100644 index 0000000000..c0379fac70 --- /dev/null +++ b/src/messaging/messaging-internal.ts @@ -0,0 +1,581 @@ +/*! + * 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, transformMillisecondsToSecondsString } from '../utils/index'; +import { MessagingClientErrorCode, FirebaseMessagingError, } from '../utils/error'; +import * as validator from '../utils/validator'; + +import { + AndroidConfig, AndroidFcmOptions, AndroidNotification, ApsAlert, ApnsConfig, + ApnsFcmOptions, ApnsPayload, Aps, CriticalSound, FcmOptions, LightSettings, Message, + Notification, WebpushConfig, +} from './messaging-api'; + +// 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 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'); + } +} diff --git a/src/messaging/messaging-namespace.ts b/src/messaging/messaging-namespace.ts new file mode 100644 index 0000000000..8171e9c212 --- /dev/null +++ b/src/messaging/messaging-namespace.ts @@ -0,0 +1,253 @@ +/*! + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { App } from '../app'; +import { Messaging as TMessaging } from './messaging'; +import { + AndroidConfig as TAndroidConfig, + AndroidFcmOptions as TAndroidFcmOptions, + AndroidNotification as TAndroidNotification, + ApnsConfig as TApnsConfig, + ApnsFcmOptions as TApnsFcmOptions, + ApnsPayload as TApnsPayload, + Aps as TAps, + ApsAlert as TApsAlert, + BatchResponse as TBatchResponse, + CriticalSound as TCriticalSound, + ConditionMessage as TConditionMessage, + FcmOptions as TFcmOptions, + LightSettings as TLightSettings, + Message as TMessage, + MessagingTopicManagementResponse as TMessagingTopicManagementResponse, + MulticastMessage as TMulticastMessage, + Notification as TNotification, + SendResponse as TSendResponse, + TokenMessage as TTokenMessage, + TopicMessage as TTopicMessage, + WebpushConfig as TWebpushConfig, + WebpushFcmOptions as TWebpushFcmOptions, + WebpushNotification as TWebpushNotification, + + // Legacy APIs + DataMessagePayload as TDataMessagePayload, + MessagingConditionResponse as TMessagingConditionResponse, + MessagingDeviceGroupResponse as TMessagingDeviceGroupResponse, + MessagingDeviceResult as TMessagingDeviceResult, + MessagingDevicesResponse as TMessagingDevicesResponse, + MessagingOptions as TMessagingOptions, + MessagingPayload as TMessagingPayload, + MessagingTopicResponse as TMessagingTopicResponse, + NotificationMessagePayload as TNotificationMessagePayload, +} from './messaging-api'; + +/** + * Gets the {@link firebase-admin.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 `Messaging` service or as `admin.messaging(app)` to access the + * `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. + * + * @returns The default `Messaging` service if no + * app is provided or the `Messaging` service associated with the provided + * app. + */ +export declare function messaging(app?: App): messaging.Messaging; + +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace messaging { + /** + * Type alias to {@link firebase-admin.messaging#Messaging}. + */ + export type Messaging = TMessaging; + + /** + * Type alias to {@link firebase-admin.messaging#AndroidConfig}. + */ + export type AndroidConfig = TAndroidConfig; + + /** + * Type alias to {@link firebase-admin.messaging#AndroidFcmOptions}. + */ + export type AndroidFcmOptions = TAndroidFcmOptions; + + /** + * Type alias to {@link firebase-admin.messaging#AndroidNotification}. + */ + export type AndroidNotification = TAndroidNotification; + + /** + * Type alias to {@link firebase-admin.messaging#ApnsConfig}. + */ + export type ApnsConfig = TApnsConfig; + + /** + * Type alias to {@link firebase-admin.messaging#ApnsFcmOptions}. + */ + export type ApnsFcmOptions = TApnsFcmOptions; + + /** + * Type alias to {@link firebase-admin.messaging#ApnsPayload}. + */ + export type ApnsPayload = TApnsPayload; + + /** + * Type alias to {@link firebase-admin.messaging#Aps}. + */ + export type Aps = TAps; + + /** + * Type alias to {@link firebase-admin.messaging#ApsAlert}. + */ + export type ApsAlert = TApsAlert; + + /** + * Type alias to {@link firebase-admin.messaging#BatchResponse}. + */ + export type BatchResponse = TBatchResponse; + + /** + * Type alias to {@link firebase-admin.messaging#CriticalSound}. + */ + export type CriticalSound = TCriticalSound; + + /** + * Type alias to {@link firebase-admin.messaging#ConditionMessage}. + */ + export type ConditionMessage = TConditionMessage; + + /** + * Type alias to {@link firebase-admin.messaging#FcmOptions}. + */ + export type FcmOptions = TFcmOptions; + + /** + * Type alias to {@link firebase-admin.messaging#LightSettings}. + */ + export type LightSettings = TLightSettings; + + /** + * Type alias to {@link firebase-admin.messaging#Message}. + */ + export type Message = TMessage; + + /** + * Type alias to {@link firebase-admin.messaging#MessagingTopicManagementResponse}. + */ + export type MessagingTopicManagementResponse = TMessagingTopicManagementResponse; + + /** + * Type alias to {@link firebase-admin.messaging#MulticastMessage}. + */ + export type MulticastMessage = TMulticastMessage; + + /** + * Type alias to {@link firebase-admin.messaging#Notification}. + */ + export type Notification = TNotification; + + /** + * Type alias to {@link firebase-admin.messaging#SendResponse}. + */ + export type SendResponse = TSendResponse; + + /** + * Type alias to {@link firebase-admin.messaging#TokenMessage}. + */ + export type TokenMessage = TTokenMessage; + + /** + * Type alias to {@link firebase-admin.messaging#TopicMessage}. + */ + export type TopicMessage = TTopicMessage; + + /** + * Type alias to {@link firebase-admin.messaging#WebpushConfig}. + */ + export type WebpushConfig = TWebpushConfig; + + /** + * Type alias to {@link firebase-admin.messaging#WebpushFcmOptions}. + */ + export type WebpushFcmOptions = TWebpushFcmOptions; + + /** + * Type alias to {@link firebase-admin.messaging#WebpushNotification}. + */ + export type WebpushNotification = TWebpushNotification; + + // Legacy APIs + + /** + * Type alias to {@link firebase-admin.messaging#DataMessagePayload}. + */ + export type DataMessagePayload = TDataMessagePayload; + + /** + * Type alias to {@link firebase-admin.messaging#MessagingConditionResponse}. + */ + export type MessagingConditionResponse = TMessagingConditionResponse; + + /** + * Type alias to {@link firebase-admin.messaging#MessagingDeviceGroupResponse}. + */ + export type MessagingDeviceGroupResponse = TMessagingDeviceGroupResponse; + + /** + * Type alias to {@link firebase-admin.messaging#MessagingDeviceResult}. + */ + export type MessagingDeviceResult = TMessagingDeviceResult; + + /** + * Type alias to {@link firebase-admin.messaging#MessagingDevicesResponse}. + */ + export type MessagingDevicesResponse = TMessagingDevicesResponse; + + /** + * Type alias to {@link firebase-admin.messaging#MessagingOptions}. + */ + export type MessagingOptions = TMessagingOptions; + + /** + * Type alias to {@link firebase-admin.messaging#MessagingPayload}. + */ + export type MessagingPayload = TMessagingPayload; + + /** + * Type alias to {@link firebase-admin.messaging#MessagingTopicResponse}. + */ + export type MessagingTopicResponse = TMessagingTopicResponse; + + /** + * Type alias to {@link firebase-admin.messaging#NotificationMessagePayload}. + */ + export type NotificationMessagePayload = TNotificationMessagePayload; +} diff --git a/src/messaging/messaging.ts b/src/messaging/messaging.ts index fa4e93ba0b..f7a2a14ddf 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"); @@ -14,17 +15,32 @@ * limitations under the License. */ -import {FirebaseApp} from '../firebase-app'; -import {renameProperties} from '../utils/index'; -import {deepCopy, deepExtend} from '../utils/deep-copy'; -import {FirebaseMessagingRequestHandler} from './messaging-api-request'; -import {FirebaseServiceInterface, FirebaseServiceInternalsInterface} from '../firebase-service'; -import { - ErrorInfo, FirebaseError, FirebaseArrayIndexError, MessagingClientErrorCode, FirebaseMessagingError, -} from '../utils/error'; - +import { App } from '../app'; +import { deepCopy, deepExtend } from '../utils/deep-copy'; +import { SubRequest } from './batch-request-internal'; +import { ErrorInfo, MessagingClientErrorCode, FirebaseMessagingError } from '../utils/error'; import * as utils from '../utils'; import * as validator from '../utils/validator'; +import { validateMessage, BLACKLISTED_DATA_PAYLOAD_KEYS, BLACKLISTED_OPTIONS_KEYS } from './messaging-internal'; +import { FirebaseMessagingRequestHandler } from './messaging-api-request-internal'; + +import { + BatchResponse, + Message, + MessagingTopicManagementResponse, + MulticastMessage, + + // Legacy API types + MessagingDevicesResponse, + MessagingDeviceGroupResponse, + MessagingPayload, + MessagingOptions, + MessagingTopicResponse, + MessagingConditionResponse, + DataMessagePayload, + NotificationMessagePayload, + SendResponse, +} from './messaging-api'; // FCM endpoints const FCM_SEND_HOST = 'fcm.googleapis.com'; @@ -33,6 +49,8 @@ const FCM_TOPIC_MANAGEMENT_HOST = 'iid.googleapis.com'; const FCM_TOPIC_MANAGEMENT_ADD_PATH = '/iid/v1:batchAdd'; const FCM_TOPIC_MANAGEMENT_REMOVE_PATH = '/iid/v1:batchRemove'; +// Maximum messages that can be included in a batch request. +const FCM_MAX_BATCH_SIZE = 500; // Key renames for the messaging notification payload object. const CAMELCASED_NOTIFICATION_PAYLOAD_KEYS_MAP = { @@ -84,470 +102,18 @@ 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', -]; - -interface BaseMessage { - data?: {[key: string]: string}; - notification?: Notification; - android?: AndroidConfig; - webpush?: WebpushConfig; - apns?: ApnsConfig; -} - -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; - -export interface Notification { - title?: string; - body?: string; -} - -export interface WebpushConfig { - headers?: {[key: string]: string}; - data?: {[key: string]: string}; - notification?: WebpushNotification; -} - -export interface WebpushNotification { - title?: string; - body?: string; - icon?: string; -} - -export interface ApnsConfig { - headers?: {[key: string]: string}; - payload?: ApnsPayload; -} - -export interface ApnsPayload { - aps: Aps; - [customData: string]: object; -} - -export interface Aps { - alert?: string | ApsAlert; - badge?: number; - sound?: string; - contentAvailable?: boolean; - category?: string; - threadId?: string; -} - -export interface ApsAlert { - title?: string; - body?: string; - locKey?: string; - locArgs?: string[]; - titleLocKey?: string; - titleLocArgs?: string[]; - actionLocKey?: string; - launchImage?: string; -} - -export interface AndroidConfig { - collapseKey?: string; - priority?: ('high' | 'normal'); - ttl?: number; - restrictedPackageName?: string; - data?: {[key: string]: string}; - notification?: AndroidNotification; -} - -export interface AndroidNotification { - title?: string; - body?: string; - icon?: string; - color?: string; - sound?: string; - tag?: string; - clickAction?: string; - bodyLocKey?: string; - bodyLocArgs?: string[]; - titleLocKey?: string; - titleLocArgs?: string[]; -} - -/** - * 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: object, label: string) { - 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) { - 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) { - 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); -} - -/** - * 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) { - 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) { - 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); - - const propertyMappings = { - contentAvailable: 'content-available', - threadId: 'thread-id', - }; - renameProperties(aps, propertyMappings); - - if (typeof aps['content-available'] !== 'undefined') { - if (aps['content-available'] === true) { - aps['content-available'] = 1; - } else { - delete aps['content-available']; - } - } -} - -/** - * 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) { - 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'); - } - - const propertyMappings = { - locKey: 'loc-key', - locArgs: 'loc-args', - titleLocKey: 'title-loc-key', - titleLocArgs: 'title-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) { - 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 seconds = Math.floor(config.ttl / 1000); - const nanos = (config.ttl - seconds * 1000) * 1000000; - let duration: string; - if (nanos > 0) { - let nanoString = nanos.toString(); - while (nanoString.length < 9) { - nanoString = '0' + nanoString; - } - duration = `${seconds}.${nanoString}s`; - } else { - duration = `${seconds}s`; - } - (config as any).ttl = duration; - } - validateStringMap(config.data, 'android.data'); - validateAndroidNotification(config.notification); - - 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) { - 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'); - } - - const propertyMappings = { - clickAction: 'click_action', - bodyLocKey: 'body_loc_key', - bodyLocArgs: 'body_loc_args', - titleLocKey: 'title_loc_key', - titleLocArgs: 'title_loc_args', - }; - renameProperties(notification, propertyMappings); -} - -/** - * 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. - */ -function validateMessage(message: Message) { - 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); -} - -/* Payload for data messages */ -export interface DataMessagePayload { - [key: string]: string; -} - -/* Payload for notification messages */ -export interface NotificationMessagePayload { - tag?: string; - body?: string; - icon?: string; - badge?: string; - color?: string; - sound?: string; - title?: string; - bodyLocKey?: string; - bodyLocArgs?: string; - clickAction?: string; - titleLocKey?: string; - titleLocArgs?: string; - [other: string]: string; -} - -/* Composite messaging payload (data and notification payloads are both optional) */ -export interface MessagingPayload { - data?: DataMessagePayload; - notification?: NotificationMessagePayload; -} - -/* Options that can passed along with messages */ -export interface MessagingOptions { - dryRun?: boolean; - priority?: string; - timeToLive?: number; - collapseKey?: string; - mutableContent?: boolean; - contentAvailable?: boolean; - restrictedPackageName?: string; - [other: string]: any; -} - -/* Individual status response payload from single devices */ -export interface MessagingDeviceResult { - error?: FirebaseError; - messageId?: string; - canonicalRegistrationToken?: string; -} - -/* Response payload from sending to a single device ID or array of device IDs */ -export interface MessagingDevicesResponse { - canonicalRegistrationTokenCount: number; - failureCount: number; - multicastId: number; - results: MessagingDeviceResult[]; - 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[]; -} - - /** * Maps a raw FCM server response to a MessagingDevicesResponse object. * - * @param {object} response The raw FCM server response to map. + * @param response - The raw FCM server response to map. * - * @return {MessagingDeviceGroupResponse} The mapped MessagingDevicesResponse object. + * @returns The mapped MessagingDevicesResponse object. */ function mapRawResponseToDevicesResponse(response: object): MessagingDevicesResponse { // Rename properties on the server response utils.renameProperties(response, MESSAGING_DEVICES_RESPONSE_KEYS_MAP); if ('results' in response) { - (response as any).results.forEach((messagingDeviceResult) => { + (response as any).results.forEach((messagingDeviceResult: any) => { utils.renameProperties(messagingDeviceResult, MESSAGING_DEVICE_RESULT_KEYS_MAP); // Map the FCM server's error strings to actual error objects. @@ -566,9 +132,9 @@ function mapRawResponseToDevicesResponse(response: object): MessagingDevicesResp /** * Maps a raw FCM server response to a MessagingDeviceGroupResponse object. * - * @param {object} response The raw FCM server response to map. + * @param response - The raw FCM server response to map. * - * @return {MessagingDeviceGroupResponse} The mapped MessagingDeviceGroupResponse object. + * @returns The mapped MessagingDeviceGroupResponse object. */ function mapRawResponseToDeviceGroupResponse(response: object): MessagingDeviceGroupResponse { // Rename properties on the server response @@ -586,7 +152,7 @@ function mapRawResponseToDeviceGroupResponse(response: object): MessagingDeviceG * * @param {object} response The raw FCM server response to map. * - * @return {MessagingTopicManagementResponse} The mapped MessagingTopicManagementResponse object. + * @returns {MessagingTopicManagementResponse} The mapped MessagingTopicManagementResponse object. */ function mapRawResponseToTopicManagementResponse(response: object): MessagingTopicManagementResponse { // Add the success and failure counts. @@ -596,9 +162,8 @@ function mapRawResponseToTopicManagementResponse(response: object): MessagingTop errors: [], }; - const errors: FirebaseArrayIndexError[] = []; if ('results' in response) { - (response as any).results.forEach((tokenManagementResult, index) => { + (response as any).results.forEach((tokenManagementResult: any, index: number) => { // Map the FCM server's error strings to actual error objects. if ('error' in tokenManagementResult) { result.failureCount += 1; @@ -619,38 +184,19 @@ 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 { - - public INTERNAL: MessagingInternals = new MessagingInternals(); +export class Messaging { private urlPath: string; - private appInternal: FirebaseApp; - private messagingRequestHandler: FirebaseMessagingRequestHandler; + private readonly appInternal: App; + private readonly messagingRequestHandler: FirebaseMessagingRequestHandler; /** - * @param {FirebaseApp} app The app for this Messaging service. - * @constructor + * @internal */ - constructor(app: FirebaseApp) { + constructor(app: App) { if (!validator.isNonNullObject(app) || !('options' in app)) { throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_ARGUMENT, @@ -658,38 +204,32 @@ export class Messaging implements FirebaseServiceInterface { ); } - const projectId: string = utils.getProjectId(app); - if (!validator.isNonEmptyString(projectId)) { - // Assert for an explicit projct ID (either via AppOptions or the cert itself). - throw new FirebaseMessagingError( - MessagingClientErrorCode.INVALID_ARGUMENT, - 'Failed to determine project ID for Messaging. Initialize the ' - + 'SDK with service account credentials or set project ID as an app option. ' - + 'Alternatively set the GCLOUD_PROJECT environment variable.', - ); - } - - this.urlPath = `/v1/projects/${projectId}/messages:send`; this.appInternal = app; this.messagingRequestHandler = new FirebaseMessagingRequestHandler(app); } /** - * Returns the app associated with this Messaging instance. + * The {@link firebase-admin.app#App} associated with the current `Messaging` service + * instance. * - * @return {FirebaseApp} The app associated with this Messaging instance. + * @example + * ```javascript + * var app = messaging.app; + * ``` */ - get app(): FirebaseApp { + get app(): App { return this.appInternal; } /** - * Sends a message via Firebase Cloud Messaging (FCM). + * Sends the given message via FCM. * - * @param {Message} message The message to be sent. - * @param {boolean=} dryRun Whether to send the message in the dry-run (validation only) mode. - * - * @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. + * @returns 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); @@ -698,13 +238,13 @@ export class Messaging implements FirebaseServiceInterface { throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_ARGUMENT, 'dryRun must be a boolean'); } - return Promise.resolve() - .then(() => { - const request: {message: Message, validate_only?: boolean} = {message: copy}; + return this.getUrlPath() + .then((urlPath) => { + const request: { message: Message; validate_only?: boolean } = { message: copy }; if (dryRun) { request.validate_only = true; } - return this.messagingRequestHandler.invokeRequestHandler(FCM_SEND_HOST, this.urlPath, request); + return this.messagingRequestHandler.invokeRequestHandler(FCM_SEND_HOST, urlPath, request); }) .then((response) => { return (response as any).name; @@ -712,21 +252,261 @@ export class Messaging implements FirebaseServiceInterface { } /** - * Sends an FCM message to a single device or an array of devices. + * Sends each message in the given array via Firebase Cloud Messaging. + * + * Unlike {@link Messaging.sendAll}, this method makes a single RPC call for each message + * in the given array. + * + * The responses list obtained from the return value corresponds to the order of `messages`. + * An error from this method or a `BatchResponse` with all failures indicates a total failure, + * meaning that none of the messages in the list could be sent. Partial failures or no + * failures are only 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. + * @returns A Promise fulfilled with an object representing the result of the + * send operation. + */ + public sendEach(messages: Message[], dryRun?: boolean): Promise { + if (validator.isArray(messages) && messages.constructor !== Array) { + // In more recent JS specs, an array-like object might have a constructor that is not of + // Array type. Our deepCopy() method doesn't handle them properly. Convert such objects to + // a regular array here before calling deepCopy(). See issue #566 for details. + messages = Array.from(messages); + } + + const copy: Message[] = deepCopy(messages); + if (!validator.isNonEmptyArray(copy)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_ARGUMENT, 'messages must be a non-empty array'); + } + if (copy.length > FCM_MAX_BATCH_SIZE) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_ARGUMENT, + `messages list must not contain more than ${FCM_MAX_BATCH_SIZE} items`); + } + if (typeof dryRun !== 'undefined' && !validator.isBoolean(dryRun)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_ARGUMENT, 'dryRun must be a boolean'); + } + + return this.getUrlPath() + .then((urlPath) => { + const requests: Promise[] = copy.map(async (message) => { + validateMessage(message); + const request: { message: Message; validate_only?: boolean } = { message }; + if (dryRun) { + request.validate_only = true; + } + return this.messagingRequestHandler.invokeRequestHandlerForSendResponse(FCM_SEND_HOST, urlPath, request); + }); + return Promise.allSettled(requests); + }).then((results) => { + const responses: SendResponse[] = []; + results.forEach(result => { + if (result.status === 'fulfilled') { + responses.push(result.value); + } else { // rejected + responses.push({ success: false, error: result.reason }) + } + }) + const successCount: number = responses.filter((resp) => resp.success).length; + return { + responses, + successCount, + failureCount: responses.length - successCount, + }; + }); + } + + /** + * Sends the given multicast message to all the FCM registration tokens + * specified in it. + * + * This method uses the {@link Messaging.sendEach} 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 or a `BatchResponse` with all failures indicates a total + * failure, meaning that the messages in the list could be sent. Partial failures or + * failures are only 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. + * @returns A Promise fulfilled with an object representing the result of the + * send operation. + */ + public sendEachForMulticast(message: MulticastMessage, dryRun?: boolean): Promise { + const copy: MulticastMessage = deepCopy(message); + if (!validator.isNonNullObject(copy)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_ARGUMENT, 'MulticastMessage must be a non-null object'); + } + if (!validator.isNonEmptyArray(copy.tokens)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_ARGUMENT, 'tokens must be a non-empty array'); + } + if (copy.tokens.length > FCM_MAX_BATCH_SIZE) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_ARGUMENT, + `tokens list must not contain more than ${FCM_MAX_BATCH_SIZE} items`); + } + + const messages: Message[] = copy.tokens.map((token) => { + return { + token, + android: copy.android, + apns: copy.apns, + data: copy.data, + notification: copy.notification, + webpush: copy.webpush, + fcmOptions: copy.fcmOptions, + }; + }); + return this.sendEach(messages, dryRun); + } + + /** + * 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, meaning that 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. + * @returns A Promise fulfilled with an object representing the result of the + * send operation. + * + * @deprecated Use {@link Messaging.sendEach} instead. + */ + public sendAll(messages: Message[], dryRun?: boolean): Promise { + if (validator.isArray(messages) && messages.constructor !== Array) { + // In more recent JS specs, an array-like object might have a constructor that is not of + // Array type. Our deepCopy() method doesn't handle them properly. Convert such objects to + // a regular array here before calling deepCopy(). See issue #566 for details. + messages = Array.from(messages); + } + + const copy: Message[] = deepCopy(messages); + if (!validator.isNonEmptyArray(copy)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_ARGUMENT, 'messages must be a non-empty array'); + } + if (copy.length > FCM_MAX_BATCH_SIZE) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_ARGUMENT, + `messages list must not contain more than ${FCM_MAX_BATCH_SIZE} items`); + } + if (typeof dryRun !== 'undefined' && !validator.isBoolean(dryRun)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_ARGUMENT, 'dryRun must be a boolean'); + } + + return this.getUrlPath() + .then((urlPath) => { + const requests: SubRequest[] = copy.map((message) => { + validateMessage(message); + const request: { message: Message; validate_only?: boolean } = { message }; + if (dryRun) { + request.validate_only = true; + } + return { + url: `https://${FCM_SEND_HOST}${urlPath}`, + body: request, + }; + }); + return this.messagingRequestHandler.sendBatchRequest(requests); + }); + } + + /** + * 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, meaning that 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. + * @returns A Promise fulfilled with an object representing the result of the + * send operation. + * + * @deprecated Use {@link Messaging.sendEachForMulticast} instead. + */ + public sendMulticast(message: MulticastMessage, dryRun?: boolean): Promise { + const copy: MulticastMessage = deepCopy(message); + if (!validator.isNonNullObject(copy)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_ARGUMENT, 'MulticastMessage must be a non-null object'); + } + if (!validator.isNonEmptyArray(copy.tokens)) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_ARGUMENT, 'tokens must be a non-empty array'); + } + if (copy.tokens.length > FCM_MAX_BATCH_SIZE) { + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_ARGUMENT, + `tokens list must not contain more than ${FCM_MAX_BATCH_SIZE} items`); + } + + const messages: Message[] = copy.tokens.map((token) => { + return { + token, + android: copy.android, + apns: copy.apns, + data: copy.data, + notification: copy.notification, + webpush: copy.webpush, + fcmOptions: copy.fcmOptions, + }; + }); + return this.sendAll(messages, dryRun); + } + + /** + * Sends an FCM message to a single device corresponding to the provided + * registration token. + * + * See {@link https://firebase.google.com/docs/cloud-messaging/admin/legacy-fcm#send_to_individual_devices | + * 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 {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. + * @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 {Promise} A Promise fulfilled - * with the server's response after the message has been sent. + * @returns A promise fulfilled with the server's response after the message + * has been sent. + * + * @deprecated Use {@link Messaging.send} instead. */ public sendToDevice( 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( @@ -764,27 +544,40 @@ 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: [], + } } }); } /** - * Sends an FCM message to a device group. + * Sends an FCM message to a device group corresponding to the provided + * notification key. + * + * See {@link https://firebase.google.com/docs/cloud-messaging/admin/legacy-fcm#send_to_a_device_group | + * 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. * - * @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. + * @returns A promise fulfilled with the server's response after the message + * has been sent. * - * @return {Promise} A Promise fulfilled - * with the server's response after the message has been sent. + * @deprecated Use {@link Messaging.send} instead. */ public sendToDeviceGroup( notificationKey: string, payload: MessagingPayload, options: MessagingOptions = {}, - ): Promise { + ): Promise { if (!validator.isNonEmptyString(notificationKey)) { throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_RECIPIENT, @@ -836,7 +629,11 @@ export class Messaging implements FirebaseServiceInterface { 'Notification key provided to sendToDeviceGroup() is invalid.', ); } else { - return mapRawResponseToDevicesResponse(response); + const devicesResponse = mapRawResponseToDevicesResponse(response); + return { + ...devicesResponse, + failedRegistrationTokens: [], + } } } @@ -847,12 +644,16 @@ 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 {@link https://firebase.google.com/docs/cloud-messaging/admin/legacy-fcm#send_to_a_topic | + * 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. + * + * @returns A promise fulfilled with the server's response after the message + * has been sent. */ public sendToTopic( topic: string, @@ -892,12 +693,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 {@link https://firebase.google.com/docs/cloud-messaging/admin/legacy-fcm#send_to_a_condition | + * 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. + * @returns A promise fulfilled with the server's response after the message + * has been sent. */ public sendToCondition( condition: string, @@ -942,14 +749,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 {@link https://firebase.google.com/docs/cloud-messaging/manage-topics#suscribe_and_unsubscribe_using_the | + * Subscribe to a topic} + * for code samples and detailed documentation. Optionally, you can provide an + * array of tokens to subscribe multiple devices. * - * @return {Promise} A Promise fulfilled with the parsed FCM - * server response. + * @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. + * + * @returns A promise fulfilled with the server's response after the device has been + * subscribed to the topic. */ public subscribeToTopic( registrationTokenOrTokens: string | string[], @@ -964,14 +776,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 {@link https://firebase.google.com/docs/cloud-messaging/admin/manage-topic-subscriptions#unsubscribe_from_a_topic | + * Unsubscribe from a topic} + * for code samples and detailed documentation. Optionally, you can provide an + * array of tokens to unsubscribe multiple devices. * - * @return {Promise} A Promise fulfilled with the parsed FCM - * server response. + * @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. + * + * @returns A promise fulfilled with the server's response after the device has been + * unsubscribed from the topic. */ public unsubscribeFromTopic( registrationTokenOrTokens: string | string[], @@ -985,16 +802,38 @@ export class Messaging implements FirebaseServiceInterface { ); } + private getUrlPath(): Promise { + if (this.urlPath) { + return Promise.resolve(this.urlPath); + } + + return utils.findProjectId(this.app) + .then((projectId) => { + if (!validator.isNonEmptyString(projectId)) { + // Assert for an explicit project ID (either via AppOptions or the cert itself). + throw new FirebaseMessagingError( + MessagingClientErrorCode.INVALID_ARGUMENT, + 'Failed to determine project ID for Messaging. Initialize the ' + + 'SDK with service account credentials or set project ID as an app option. ' + + 'Alternatively set the GOOGLE_CLOUD_PROJECT environment variable.', + ); + } + + this.urlPath = `/v1/projects/${projectId}/messages:send`; + return this.urlPath; + }); + } + /** * Helper method which sends and handles topic subscription management requests. * - * @param {string|string[]} registrationTokenOrTokens The registration token or an array of + * @param registrationTokenOrTokens - The registration token or an array of * registration tokens to unsubscribe from the topic. - * @param {string} topic The topic to which to subscribe. - * @param {string} methodName The name of the original method called. - * @param {string} path The endpoint path to use for the request. + * @param topic - The topic to which to subscribe. + * @param methodName - The name of the original method called. + * @param path - The endpoint path to use for the request. * - * @return {Promise} A Promise fulfilled with the parsed server + * @returns A Promise fulfilled with the parsed server * response. */ private sendTopicManagementRequest( @@ -1039,13 +878,13 @@ export class Messaging implements FirebaseServiceInterface { /** * Validates the types of the messaging payload and options. If invalid, an error will be thrown. * - * @param {MessagingPayload} payload The messaging payload to validate. - * @param {MessagingOptions} options The messaging options to validate. + * @param payload - The messaging payload to validate. + * @param options - The messaging options to validate. */ private validateMessagingPayloadAndOptionsTypes( payload: MessagingPayload, options: MessagingOptions, - ) { + ): void { // Validate the payload is an object if (!validator.isNonNullObject(payload)) { throw new FirebaseMessagingError( @@ -1066,12 +905,12 @@ export class Messaging implements FirebaseServiceInterface { /** * Validates the messaging payload. If invalid, an error will be thrown. * - * @param {MessagingPayload} payload The messaging payload to validate. + * @param payload - The messaging payload to validate. * - * @return {MessagingPayload} A copy of the provided payload with whitelisted properties switched + * @returns A copy of the provided payload with whitelisted properties switched * from camelCase to underscore_case. */ - private validateMessagingPayload(payload: MessagingPayload) { + private validateMessagingPayload(payload: MessagingPayload): MessagingPayload { const payloadCopy: MessagingPayload = deepCopy(payload); const payloadKeys = Object.keys(payloadCopy); @@ -1083,8 +922,8 @@ 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 ` + - `"data" and "notification".`, + `Messaging payload contains an invalid "${payloadKey}" property. Valid properties are ` + + '"data" and "notification".', ); } else { containsDataOrNotificationKey = true; @@ -1099,15 +938,13 @@ export class Messaging implements FirebaseServiceInterface { ); } - payloadKeys.forEach((payloadKey) => { - const value = payloadCopy[payloadKey]; - + const validatePayload = (payloadKey: string, value: DataMessagePayload | NotificationMessagePayload): void => { // Validate each top-level key in the payload is an object if (!validator.isNonNullObject(value)) { throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_PAYLOAD, - `Messaging payload contains an invalid value for the "${ payloadKey }" property. ` + - `Value must be an object.`, + `Messaging payload contains an invalid value for the "${payloadKey}" property. ` + + 'Value must be an object.', ); } @@ -1116,33 +953,40 @@ 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 }" ` + - `property. Values must be strings.`, + `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.`, ); } }); - }); + }; + + if (payloadCopy.data !== undefined) { + validatePayload('data', payloadCopy.data); + } + if (payloadCopy.notification !== undefined) { + validatePayload('notification', payloadCopy.notification); + } // Validate the data payload object does not contain blacklisted properties if ('data' in payloadCopy) { BLACKLISTED_DATA_PAYLOAD_KEYS.forEach((blacklistedKey) => { - if (blacklistedKey in payloadCopy.data) { + if (blacklistedKey in payloadCopy.data!) { throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_PAYLOAD, - `Messaging payload contains the blacklisted "data.${ blacklistedKey }" property.`, + `Messaging payload contains the blacklisted "data.${blacklistedKey}" property.`, ); } }); } // Convert whitelisted camelCase keys to underscore_case - if ('notification' in payloadCopy) { + if (payloadCopy.notification) { utils.renameProperties(payloadCopy.notification, CAMELCASED_NOTIFICATION_PAYLOAD_KEYS_MAP); } @@ -1152,12 +996,12 @@ export class Messaging implements FirebaseServiceInterface { /** * Validates the messaging options. If invalid, an error will be thrown. * - * @param {MessagingOptions} options The messaging options to validate. + * @param options - The messaging options to validate. * - * @return {MessagingOptions} A copy of the provided options with whitelisted properties switched + * @returns A copy of the provided options with whitelisted properties switched * from camelCase to underscore_case. */ - private validateMessagingOptions(options: MessagingOptions) { + private validateMessagingOptions(options: MessagingOptions): MessagingOptions { const optionsCopy: MessagingOptions = deepCopy(options); // Validate the options object does not contain blacklisted properties @@ -1165,7 +1009,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.`, ); } }); @@ -1178,14 +1022,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)) { @@ -1195,32 +1039,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.', ); } @@ -1231,17 +1075,17 @@ export class Messaging implements FirebaseServiceInterface { /** * Validates the type of the provided registration token(s). If invalid, an error will be thrown. * - * @param {string|string[]} registrationTokenOrTokens The registration token(s) to validate. - * @param {string} method The method name to use in error messages. - * @param {ErrorInfo?} [errorInfo] The error info to use if the registration tokens are invalid. + * @param registrationTokenOrTokens - The registration token(s) to validate. + * @param method - The method name to use in error messages. + * @param errorInfo - The error info to use if the registration tokens are invalid. */ private validateRegistrationTokensType( registrationTokenOrTokens: string | string[], methodName: string, 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 ` + @@ -1253,16 +1097,16 @@ export class Messaging implements FirebaseServiceInterface { /** * Validates the provided registration tokens. If invalid, an error will be thrown. * - * @param {string|string[]} registrationTokenOrTokens The registration token or an array of + * @param registrationTokenOrTokens - The registration token or an array of * registration tokens to validate. - * @param {string} method The method name to use in error messages. - * @param {errorInfo?} [ErrorInfo] The error info to use if the registration tokens are invalid. + * @param method - The method name to use in error messages. + * @param errorInfo - The error info to use if the registration tokens are invalid. */ private validateRegistrationTokens( registrationTokenOrTokens: string | string[], methodName: string, errorInfo: ErrorInfo = MessagingClientErrorCode.INVALID_ARGUMENT, - ) { + ): void { if (validator.isArray(registrationTokenOrTokens)) { // Validate the array contains no more than 1,000 registration tokens. if (registrationTokenOrTokens.length > 1000) { @@ -1289,15 +1133,15 @@ export class Messaging implements FirebaseServiceInterface { /** * Validates the type of the provided topic. If invalid, an error will be thrown. * - * @param {string} topic The topic to validate. - * @param {string} method The method name to use in error messages. - * @param {ErrorInfo?} [errorInfo] The error info to use if the topic is invalid. + * @param topic - The topic to validate. + * @param method - The method name to use in error messages. + * @param errorInfo - The error info to use if the topic is invalid. */ private validateTopicType( topic: string | string[], methodName: string, errorInfo: ErrorInfo = MessagingClientErrorCode.INVALID_ARGUMENT, - ) { + ): void { if (!validator.isNonEmptyString(topic)) { throw new FirebaseMessagingError( errorInfo, @@ -1310,15 +1154,15 @@ export class Messaging implements FirebaseServiceInterface { /** * Validates the provided topic. If invalid, an error will be thrown. * - * @param {string} topic The topic to validate. - * @param {string} method The method name to use in error messages. - * @param {ErrorInfo?} [errorInfo] The error info to use if the topic is invalid. + * @param topic - The topic to validate. + * @param method - The method name to use in error messages. + * @param errorInfo - The error info to use if the topic is invalid. */ private validateTopic( topic: string, methodName: string, errorInfo: ErrorInfo = MessagingClientErrorCode.INVALID_ARGUMENT, - ) { + ): void { if (!validator.isTopic(topic)) { throw new FirebaseMessagingError( errorInfo, @@ -1331,13 +1175,13 @@ export class Messaging implements FirebaseServiceInterface { /** * Normalizes the provided topic name by prepending it with '/topics/', if necessary. * - * @param {string} topic The topic name to normalize. + * @param topic - The topic name to normalize. * - * @return {string} The normalized topic name. + * @returns The normalized topic name. */ - private normalizeTopic(topic: string) { + private normalizeTopic(topic: string): string { if (!/^\/topics\//.test(topic)) { - topic = `/topics/${ topic }`; + topic = `/topics/${topic}`; } return topic; } diff --git a/src/project-management/android-app.ts b/src/project-management/android-app.ts new file mode 100644 index 0000000000..667b8be4d6 --- /dev/null +++ b/src/project-management/android-app.ts @@ -0,0 +1,250 @@ +/*! + * Copyright 2018 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 { FirebaseProjectManagementError } from '../utils/error'; +import * as validator from '../utils/validator'; +import { ProjectManagementRequestHandler, assertServerResponse } from './project-management-api-request-internal'; +import { AppMetadata, AppPlatform } from './app-metadata'; + + +/** + * 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; +} + +/** + * A reference to a Firebase Android app. + * + * Do not call this constructor directly. Instead, use {@link ProjectManagement.androidApp}. + */ +export class AndroidApp { + + private readonly resourceName: string; + + /** + * @internal + */ + constructor( + public readonly appId: string, + private readonly requestHandler: ProjectManagementRequestHandler) { + if (!validator.isNonEmptyString(appId)) { + throw new FirebaseProjectManagementError( + 'invalid-argument', 'appId must be a non-empty string.'); + } + + this.resourceName = `projects/-/androidApps/${appId}`; + } + + /** + * Retrieves metadata about this Android app. + * + * @returns A promise that resolves to the retrieved metadata about this Android app. + */ + public getMetadata(): Promise { + return this.requestHandler.getResource(this.resourceName) + .then((responseData: any) => { + assertServerResponse( + validator.isNonNullObject(responseData), + responseData, + 'getMetadata()\'s responseData must be a non-null object.'); + + const requiredFieldsList = ['name', 'appId', 'projectId', 'packageName']; + requiredFieldsList.forEach((requiredField) => { + assertServerResponse( + validator.isNonEmptyString(responseData[requiredField]), + responseData, + `getMetadata()'s responseData.${requiredField} must be a non-empty string.`); + }); + + const metadata: AndroidAppMetadata = { + platform: AppPlatform.ANDROID, + resourceName: responseData.name, + appId: responseData.appId, + displayName: responseData.displayName || null, + projectId: responseData.projectId, + packageName: responseData.packageName, + }; + return metadata; + }); + } + + /** + * Sets the optional user-assigned display name of the app. + * + * @param newDisplayName - The new display name to set. + * + * @returns 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. + * + * @returns 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) => { + assertServerResponse( + validator.isNonNullObject(responseData), + responseData, + 'getShaCertificates()\'s responseData must be a non-null object.'); + + if (!responseData.certificates) { + return []; + } + + assertServerResponse( + validator.isArray(responseData.certificates), + responseData, + '"certificates" field must be present in the getShaCertificates() response data.'); + + const requiredFieldsList = ['name', 'shaHash']; + + return responseData.certificates.map((certificateJson: any) => { + requiredFieldsList.forEach((requiredField) => { + assertServerResponse( + validator.isNonEmptyString(certificateJson[requiredField]), + responseData, + `getShaCertificates()'s responseData.certificates[].${requiredField} must be a ` + + 'non-empty string.'); + }); + + return new ShaCertificate(certificateJson.shaHash, certificateJson.name); + }); + }); + } + + /** + * Adds the given SHA certificate to this Android app. + * + * @param certificateToAdd - The SHA certificate to add. + * + * @returns 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. + * + * @returns 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( + 'invalid-argument', + 'Specified certificate does not include a resourceName. (Use AndroidApp.getShaCertificates() to retrieve ' + + 'certificates with a resourceName.'); + } + return this.requestHandler.deleteResource(certificateToDelete.resourceName); + } + + /** + * Gets the configuration artifact associated with this app. + * + * @returns 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) + .then((responseData: any) => { + assertServerResponse( + validator.isNonNullObject(responseData), + responseData, + 'getConfig()\'s responseData must be a non-null object.'); + + const base64ConfigFileContents = responseData.configFileContents; + assertServerResponse( + validator.isBase64String(base64ConfigFileContents), + responseData, + 'getConfig()\'s responseData.configFileContents must be a base64 string.'); + + return Buffer.from(base64ConfigFileContents, 'base64').toString('utf8'); + }); + } +} + +/** + * A SHA-1 or SHA-256 certificate. + * + * Do not call this constructor directly. Instead, use + * [`projectManagement.shaCertificate()`](projectManagement.ProjectManagement#shaCertificate). + */ +export class ShaCertificate { + /** + * The SHA certificate type. + * + * @example + * ```javascript + * var certType = shaCertificate.certType; + * ``` + */ + public readonly certType: ('sha1' | 'sha256'); + + /** + * Creates a ShaCertificate using the given hash. The ShaCertificate's type (eg. 'sha256') is + * 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; + * ``` + * + * @internal + */ + constructor(public readonly shaHash: string, public readonly resourceName?: string) { + if (/^[a-fA-F0-9]{40}$/.test(shaHash)) { + this.certType = 'sha1'; + } else if (/^[a-fA-F0-9]{64}$/.test(shaHash)) { + this.certType = 'sha256'; + } else { + throw new FirebaseProjectManagementError( + 'invalid-argument', 'shaHash must be either a sha256 hash or a sha1 hash.'); + } + } +} diff --git a/src/project-management/app-metadata.ts b/src/project-management/app-metadata.ts new file mode 100644 index 0000000000..8fd69255d2 --- /dev/null +++ b/src/project-management/app-metadata.ts @@ -0,0 +1,92 @@ +/*! + * 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. + */ + +/** + * 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; +} diff --git a/src/project-management/index.ts b/src/project-management/index.ts new file mode 100644 index 0000000000..cc23066681 --- /dev/null +++ b/src/project-management/index.ts @@ -0,0 +1,66 @@ +/*! + * 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. + */ + +/** + * Firebase project management. + * + * @packageDocumentation + */ + +import { App, getApp } from '../app'; +import { FirebaseApp } from '../app/firebase-app'; +import { ProjectManagement } from './project-management'; + +export { AppMetadata, AppPlatform } from './app-metadata'; +export { ProjectManagement } from './project-management'; +export { AndroidApp, AndroidAppMetadata, ShaCertificate } from './android-app'; +export { IosApp, IosAppMetadata } from './ios-app'; + +/** + * Gets the {@link ProjectManagement} service for the default app or a given app. + * + * `getProjectManagement()` can be called with no arguments to access the + * default app's `ProjectManagement` service, or as `getProjectManagement(app)` to access + * the `ProjectManagement` service associated with a specific app. + * + * @example + * ```javascript + * // Get the ProjectManagement service for the default app + * const defaultProjectManagement = getProjectManagement(); + * ``` + * + * @example + * ```javascript + * // Get the ProjectManagement service for a given app + * const otherProjectManagement = getProjectManagement(otherApp); + * ``` + * + * @param app - Optional app whose `ProjectManagement` service + * to return. If not provided, the default `ProjectManagement` service will + * be returned. * + * @returns The default `ProjectManagement` service if no app is provided or the + * `ProjectManagement` service associated with the provided app. + */ +export function getProjectManagement(app?: App): ProjectManagement { + if (typeof app === 'undefined') { + app = getApp(); + } + + const firebaseApp: FirebaseApp = app as FirebaseApp; + return firebaseApp.getOrInitService('projectManagement', (app) => new ProjectManagement(app)); +} + +export { FirebaseProjectManagementError, ProjectManagementErrorCode } from '../utils/error'; diff --git a/src/project-management/ios-app.ts b/src/project-management/ios-app.ts new file mode 100644 index 0000000000..0789265f29 --- /dev/null +++ b/src/project-management/ios-app.ts @@ -0,0 +1,132 @@ +/*! + * Copyright 2018 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 { FirebaseProjectManagementError } from '../utils/error'; +import * as validator from '../utils/validator'; +import { ProjectManagementRequestHandler, assertServerResponse } from './project-management-api-request-internal'; +import { AppMetadata, AppPlatform } from './app-metadata'; + +/** + * 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 iOS app. + * + * Do not call this constructor directly. Instead, use {@link ProjectManagement.iosApp}. + */ +export class IosApp { + + private readonly resourceName: string; + + /** + * @internal + */ + constructor( + public readonly appId: string, + private readonly requestHandler: ProjectManagementRequestHandler) { + if (!validator.isNonEmptyString(appId)) { + throw new FirebaseProjectManagementError( + 'invalid-argument', 'appId must be a non-empty string.'); + } + + this.resourceName = `projects/-/iosApps/${appId}`; + } + + /** + * Retrieves metadata about this iOS app. + * + * @returns A promise that + * resolves to the retrieved metadata about this iOS app. + */ + public getMetadata(): Promise { + return this.requestHandler.getResource(this.resourceName) + .then((responseData: any) => { + assertServerResponse( + validator.isNonNullObject(responseData), + responseData, + 'getMetadata()\'s responseData must be a non-null object.'); + + const requiredFieldsList = ['name', 'appId', 'projectId', 'bundleId']; + requiredFieldsList.forEach((requiredField) => { + assertServerResponse( + validator.isNonEmptyString(responseData[requiredField]), + responseData, + `getMetadata()'s responseData.${requiredField} must be a non-empty string.`); + }); + + const metadata: IosAppMetadata = { + platform: AppPlatform.IOS, + resourceName: responseData.name, + appId: responseData.appId, + displayName: responseData.displayName || null, + projectId: responseData.projectId, + bundleId: responseData.bundleId, + }; + return metadata; + }); + } + + /** + * Sets the optional user-assigned display name of the app. + * + * @param newDisplayName - The new display name to set. + * + * @returns 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 configuration artifact associated with this app. + * + * @returns 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) + .then((responseData: any) => { + assertServerResponse( + validator.isNonNullObject(responseData), + responseData, + 'getConfig()\'s responseData must be a non-null object.'); + + const base64ConfigFileContents = responseData.configFileContents; + assertServerResponse( + validator.isBase64String(base64ConfigFileContents), + responseData, + '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 new file mode 100644 index 0000000000..d7e597d50d --- /dev/null +++ b/src/project-management/project-management-api-request-internal.ts @@ -0,0 +1,338 @@ +/*! + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { App } from '../app'; +import { FirebaseApp } from '../app/firebase-app'; +import { + AuthorizedHttpClient, HttpError, HttpMethod, HttpRequestConfig, ExponentialBackoffPoller, +} from '../utils/api-request'; +import { FirebaseProjectManagementError, ProjectManagementErrorCode } from '../utils/error'; +import { getSdkVersion } from '../utils/index'; +import * as validator from '../utils/validator'; +import { ShaCertificate } from './android-app'; + + +/** Project management backend host and port. */ +const PROJECT_MANAGEMENT_HOST_AND_PORT = 'firebase.googleapis.com:443'; +/** Project management backend path. */ +const PROJECT_MANAGEMENT_PATH = '/v1/'; +/** Project management beta backend path. */ +const PROJECT_MANAGEMENT_BETA_PATH = '/v1beta1/'; +/** Project management request header. */ +const PROJECT_MANAGEMENT_HEADERS = { + 'X-Client-Version': `Node/Admin/${getSdkVersion()}`, +}; +/** Project management request timeout duration in milliseconds. */ +const PROJECT_MANAGEMENT_TIMEOUT_MILLIS = 10000; + +const LIST_APPS_MAX_PAGE_SIZE = 100; + +const CERT_TYPE_API_MAP = { + sha1: 'SHA_1', + sha256: 'SHA_256', +}; + +export function assertServerResponse( + condition: boolean, responseData: object, message: string): void { + if (!condition) { + throw new FirebaseProjectManagementError( + 'invalid-server-response', + `${message} Response data: ${JSON.stringify(responseData, null, 2)}`); + } +} + +/** + * Class that provides mechanism to send requests to the Firebase project management backend + * endpoints. + * + * @internal + */ +export class ProjectManagementRequestHandler { + private readonly baseUrl: string = + `https://${PROJECT_MANAGEMENT_HOST_AND_PORT}${PROJECT_MANAGEMENT_PATH}`; + private readonly baseBetaUrl: string = + `https://${PROJECT_MANAGEMENT_HOST_AND_PORT}${PROJECT_MANAGEMENT_BETA_PATH}`; + private readonly httpClient: AuthorizedHttpClient; + + private static wrapAndRethrowHttpError(errStatusCode: number, errText?: string): void { + let errorCode: ProjectManagementErrorCode; + let errorMessage: string; + + switch (errStatusCode) { + case 400: + errorCode = 'invalid-argument'; + errorMessage = 'Invalid argument provided.'; + break; + case 401: + case 403: + errorCode = 'authentication-error'; + errorMessage = 'An error occurred when trying to authenticate. Make sure the credential ' + + 'used to authenticate this SDK has the proper permissions. See ' + + 'https://firebase.google.com/docs/admin/setup for setup instructions.'; + break; + case 404: + errorCode = 'not-found'; + errorMessage = 'The specified entity could not be found.'; + break; + case 409: + errorCode = 'already-exists'; + errorMessage = 'The specified entity already exists.'; + break; + case 500: + errorCode = 'internal-error'; + errorMessage = 'An internal error has occurred. Please retry the request.'; + break; + case 503: + errorCode = 'service-unavailable'; + errorMessage = 'The server could not process the request in time. See the error ' + + 'documentation for more details.'; + break; + default: + errorCode = 'unknown-error'; + errorMessage = 'An unknown server error was returned.'; + } + + if (!errText) { + errText = ''; + } + throw new FirebaseProjectManagementError( + errorCode, + `${ errorMessage } Status code: ${ errStatusCode }. Raw server response: "${ errText }".`); + } + + /** + * @param app - The app used to fetch access tokens to sign API requests. + * @constructor + */ + constructor(app: App) { + this.httpClient = new AuthorizedHttpClient(app as FirebaseApp); + } + + /** + * @param parentResourceName - Fully-qualified resource name of the project whose Android + * apps you want to list. + */ + public listAndroidApps(parentResourceName: string): Promise { + return this.invokeRequestHandler( + 'GET', + `${parentResourceName}/androidApps?page_size=${LIST_APPS_MAX_PAGE_SIZE}`, + /* requestData */ null, + 'v1beta1'); + } + + /** + * @param parentResourceName - Fully-qualified resource name of the project whose iOS apps + * you want to list. + */ + public listIosApps(parentResourceName: string): Promise { + return this.invokeRequestHandler( + 'GET', + `${parentResourceName}/iosApps?page_size=${LIST_APPS_MAX_PAGE_SIZE}`, + /* requestData */ null, + 'v1beta1'); + } + + /** + * @param parentResourceName - Fully-qualified resource name of the project whose iOS apps + * you want to list. + */ + public listAppMetadata(parentResourceName: string): Promise { + return this.invokeRequestHandler( + 'GET', + `${parentResourceName}:searchApps?page_size=${LIST_APPS_MAX_PAGE_SIZE}`, + /* requestData */ null, + 'v1beta1'); + } + + /** + * @param parentResourceName - Fully-qualified resource name of the project that you want + * to create the Android app within. + */ + public createAndroidApp( + parentResourceName: string, packageName: string, displayName?: string): Promise { + const requestData: any = { + packageName, + }; + if (validator.isNonEmptyString(displayName)) { + requestData.displayName = displayName; + } + return this + .invokeRequestHandler('POST', `${parentResourceName}/androidApps`, requestData, 'v1beta1') + .then((responseData: any) => { + assertServerResponse( + validator.isNonNullObject(responseData), + responseData, + '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.'); + return this.pollRemoteOperationWithExponentialBackoff(responseData.name); + }); + } + + /** + * @param parentResourceName - Fully-qualified resource name of the project that you want + * to create the iOS app within. + */ + public createIosApp( + parentResourceName: string, bundleId: string, displayName?: string): Promise { + const requestData: any = { + bundleId, + }; + if (validator.isNonEmptyString(displayName)) { + requestData.displayName = displayName; + } + return this + .invokeRequestHandler('POST', `${parentResourceName}/iosApps`, requestData, 'v1beta1') + .then((responseData: any) => { + assertServerResponse( + validator.isNonNullObject(responseData), + responseData, + '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.'); + return this.pollRemoteOperationWithExponentialBackoff(responseData.name); + }); + } + + /** + * @param resourceName - Fully-qualified resource name of the entity whose display name you + * want to set. + */ + public setDisplayName(resourceName: string, newDisplayName: string): Promise { + const requestData = { + displayName: newDisplayName, + }; + return this + .invokeRequestHandler( + 'PATCH', `${resourceName}?update_mask=display_name`, requestData, 'v1beta1') + .then(() => undefined); + } + + /** + * @param parentResourceName - Fully-qualified resource name of the Android app whose SHA + * certificates you want to get. + */ + public getAndroidShaCertificates(parentResourceName: string): Promise { + return this.invokeRequestHandler( + 'GET', `${parentResourceName}/sha`, /* requestData */ null, 'v1beta1'); + } + + /** + * @param parentResourceName - Fully-qualified resource name of the Android app that you + * want to add the given SHA certificate to. + */ + public addAndroidShaCertificate( + parentResourceName: string, certificate: ShaCertificate): Promise { + const requestData = { + shaHash: certificate.shaHash, + certType: CERT_TYPE_API_MAP[certificate.certType], + }; + return this + .invokeRequestHandler('POST', `${parentResourceName}/sha`, requestData, 'v1beta1') + .then(() => undefined); + } + + /** + * @param parentResourceName - Fully-qualified resource name of the app whose config you + * want to get. + */ + public getConfig(parentResourceName: string): Promise { + return this.invokeRequestHandler( + 'GET', `${parentResourceName}/config`, /* requestData */ null, 'v1beta1'); + } + + /** + * @param parentResourceName - Fully-qualified resource name of the entity that you want to + * get. + */ + public getResource(parentResourceName: string): Promise { + return this.invokeRequestHandler('GET', parentResourceName, /* requestData */ null, 'v1beta1'); + } + + /** + * @param resourceName - Fully-qualified resource name of the entity that you want to + * delete. + */ + public deleteResource(resourceName: string): Promise { + return this + .invokeRequestHandler('DELETE', resourceName, /* requestData */ null, 'v1beta1') + .then(() => undefined); + } + + private pollRemoteOperationWithExponentialBackoff( + operationResourceName: string): Promise { + const poller = new ExponentialBackoffPoller(); + + return poller.poll(() => { + return this.invokeRequestHandler('GET', operationResourceName, /* requestData */ null) + .then((responseData: any) => { + if (responseData.error) { + const errStatusCode: number = responseData.error.code || 500; + const errText: string = + responseData.error.message || JSON.stringify(responseData.error); + ProjectManagementRequestHandler.wrapAndRethrowHttpError(errStatusCode, errText); + } + + if (!responseData.done) { + // Continue polling. + return null; + } + + // Polling complete. Resolve with operation response JSON. + return responseData.response; + }); + }); + } + + /** + * Invokes the request handler with the provided request data. + */ + private invokeRequestHandler( + method: HttpMethod, + path: string, + requestData: object | null, + apiVersion: ('v1' | 'v1beta1') = 'v1'): Promise { + const baseUrlToUse = (apiVersion === 'v1') ? this.baseUrl : this.baseBetaUrl; + const request: HttpRequestConfig = { + method, + url: `${baseUrlToUse}${path}`, + headers: PROJECT_MANAGEMENT_HEADERS, + data: requestData, + timeout: PROJECT_MANAGEMENT_TIMEOUT_MILLIS, + }; + + return this.httpClient.send(request) + .then((response) => { + // Send non-JSON responses to the catch() below, where they will be treated as errors. + if (!response.isJson()) { + throw new HttpError(response); + } + + return response.data; + }) + .catch((err) => { + if (err instanceof HttpError) { + ProjectManagementRequestHandler.wrapAndRethrowHttpError( + err.response.status, err.response.text); + } + throw err; + }); + } +} diff --git a/src/project-management/project-management-namespace.ts b/src/project-management/project-management-namespace.ts new file mode 100644 index 0000000000..f3d48d1fe7 --- /dev/null +++ b/src/project-management/project-management-namespace.ts @@ -0,0 +1,102 @@ +/*! + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { App } from '../app'; +import { + AppMetadata as TAppMetadata, + AppPlatform as TAppPlatform, +} from './app-metadata'; +import { ProjectManagement as TProjectManagement } from './project-management'; +import { + AndroidApp as TAndroidApp, + AndroidAppMetadata as TAndroidAppMetadata, + ShaCertificate as TShaCertificate, +} from './android-app'; +import { + IosApp as TIosApp, + IosAppMetadata as TIosAppMetadata, +} from './ios-app'; + +/** + * Gets the {@link firebase-admin.project-management#ProjectManagement} service for the + * default app or a given app. + * + * `admin.projectManagement()` can be called with no arguments to access the + * default app's `ProjectManagement` service, or as `admin.projectManagement(app)` to access + * the `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. * + * @returns The default `ProjectManagement` service if no app is provided or the + * `ProjectManagement` service associated with the provided app. + */ +export declare function projectManagement(app?: App): projectManagement.ProjectManagement; + +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace projectManagement { + /** + * Type alias to {@link firebase-admin.project-management#AppMetadata}. + */ + export type AppMetadata = TAppMetadata; + + /** + * Type alias to {@link firebase-admin.project-management#AppPlatform}. + */ + export type AppPlatform = TAppPlatform; + + /** + * Type alias to {@link firebase-admin.project-management#ProjectManagement}. + */ + export type ProjectManagement = TProjectManagement; + + /** + * Type alias to {@link firebase-admin.project-management#IosApp}. + */ + export type IosApp = TIosApp; + + /** + * Type alias to {@link firebase-admin.project-management#IosAppMetadata}. + */ + export type IosAppMetadata = TIosAppMetadata; + + /** + * Type alias to {@link firebase-admin.project-management#AndroidApp}. + */ + export type AndroidApp = TAndroidApp; + + /** + * Type alias to {@link firebase-admin.project-management#AndroidAppMetadata}. + */ + export type AndroidAppMetadata = TAndroidAppMetadata; + + /** + * Type alias to {@link firebase-admin.project-management#ShaCertificate}. + */ + export type ShaCertificate = TShaCertificate; +} diff --git a/src/project-management/project-management.ts b/src/project-management/project-management.ts new file mode 100644 index 0000000000..ac74004555 --- /dev/null +++ b/src/project-management/project-management.ts @@ -0,0 +1,299 @@ +/*! + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { App } from '../app'; +import { FirebaseProjectManagementError } from '../utils/error'; +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-internal'; +import { AppMetadata, AppPlatform } from './app-metadata'; + +/** + * The Firebase ProjectManagement service interface. + */ +export class ProjectManagement { + + private readonly requestHandler: ProjectManagementRequestHandler; + private projectId: string; + + /** + * @param app - The app for this ProjectManagement service. + * @constructor + * @internal + */ + constructor(readonly app: App) { + if (!validator.isNonNullObject(app) || !('options' in app)) { + throw new FirebaseProjectManagementError( + 'invalid-argument', + 'First argument passed to admin.projectManagement() must be a valid Firebase app ' + + 'instance.'); + } + + this.requestHandler = new ProjectManagementRequestHandler(app); + } + + /** + * Lists up to 100 Firebase Android apps associated with this Firebase project. + * + * @returns The list of Android apps. + */ + public listAndroidApps(): Promise { + return this.listPlatformApps('android', 'listAndroidApps()'); + } + + /** + * Lists up to 100 Firebase iOS apps associated with this Firebase project. + * + * @returns The list of iOS apps. + */ + public listIosApps(): Promise { + return this.listPlatformApps('ios', 'listIosApps()'); + } + + /** + * 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. + * + * @returns An `AndroidApp` object that references the specified Firebase Android app. + */ + public androidApp(appId: string): AndroidApp { + return new AndroidApp(appId, this.requestHandler); + } + + /** + * 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. + * + * @returns An `iOSApp` object that references the specified Firebase iOS app. + */ + public iosApp(appId: string): IosApp { + return new IosApp(appId, this.requestHandler); + } + + /** + * Creates a `ShaCertificate` object. + * + * This method does not perform an RPC. + * + * @param shaHash - The SHA-1 or SHA-256 hash for this certificate. + * + * @returns 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. + * + * @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. + * + * @returns A promise that resolves to the newly created Android app. + */ + public createAndroidApp(packageName: string, displayName?: string): Promise { + return this.getResourceName() + .then((resourceName) => { + return this.requestHandler.createAndroidApp(resourceName, packageName, displayName); + }) + .then((responseData: any) => { + assertServerResponse( + validator.isNonNullObject(responseData), + responseData, + 'createAndroidApp()\'s responseData must be a non-null object.'); + + assertServerResponse( + validator.isNonEmptyString(responseData.appId), + responseData, + '"responseData.appId" field must be present in createAndroidApp()\'s response data.'); + return new AndroidApp(responseData.appId, this.requestHandler); + }); + } + + /** + * 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. + * + * @returns A promise that resolves to the newly created iOS app. + */ + public createIosApp(bundleId: string, displayName?: string): Promise { + return this.getResourceName() + .then((resourceName) => { + return this.requestHandler.createIosApp(resourceName, bundleId, displayName); + }) + .then((responseData: any) => { + assertServerResponse( + validator.isNonNullObject(responseData), + responseData, + 'createIosApp()\'s responseData must be a non-null object.'); + + assertServerResponse( + validator.isNonEmptyString(responseData.appId), + responseData, + '"responseData.appId" field must be present in createIosApp()\'s response data.'); + return new IosApp(responseData.appId, this.requestHandler); + }); + } + + /** + * Lists up to 100 Firebase apps associated with this Firebase project. + * + * @returns A promise that resolves to the metadata list of the apps. + */ + public listAppMetadata(): Promise { + return this.getResourceName() + .then((resourceName) => { + return this.requestHandler.listAppMetadata(resourceName); + }) + .then((responseData) => { + return this.getProjectId() + .then((projectId) => { + return this.transformResponseToAppMetadata(responseData, projectId); + }); + }); + } + + /** + * Update the display name of this Firebase project. + * + * @param newDisplayName - The new display name to be updated. + * + * @returns A promise that resolves when the project display name has been updated. + */ + public setDisplayName(newDisplayName: string): Promise { + return this.getResourceName() + .then((resourceName) => { + return this.requestHandler.setDisplayName(resourceName, newDisplayName); + }); + } + + private transformResponseToAppMetadata(responseData: any, projectId: string): AppMetadata[] { + this.assertListAppsResponseData(responseData, 'listAppMetadata()'); + + if (!responseData.apps) { + return []; + } + + return responseData.apps.map((appJson: any) => { + assertServerResponse( + validator.isNonEmptyString(appJson.appId), + responseData, + '"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.'); + const metadata: AppMetadata = { + appId: appJson.appId, + platform: (AppPlatform as any)[appJson.platform] || AppPlatform.PLATFORM_UNKNOWN, + projectId, + resourceName: appJson.name, + }; + if (appJson.displayName) { + metadata.displayName = appJson.displayName; + } + return metadata; + }); + } + + private getResourceName(): Promise { + return this.getProjectId() + .then((projectId) => { + return `projects/${projectId}`; + }); + } + + private getProjectId(): Promise { + if (this.projectId) { + return Promise.resolve(this.projectId); + } + + return utils.findProjectId(this.app) + .then((projectId) => { + // Assert that a specific project ID was provided within the app. + if (!validator.isNonEmptyString(projectId)) { + throw new FirebaseProjectManagementError( + 'invalid-project-id', + 'Failed to determine project ID. Initialize the SDK with service account credentials, or ' + + 'set project ID as an app option. Alternatively, set the GOOGLE_CLOUD_PROJECT ' + + 'environment variable.'); + } + + this.projectId = projectId; + return this.projectId; + }); + } + + /** + * Lists up to 100 Firebase apps for a specified platform, associated with this Firebase project. + */ + private listPlatformApps(platform: 'android' | 'ios', callerName: string): Promise { + return this.getResourceName() + .then((resourceName) => { + return (platform === 'android') ? + this.requestHandler.listAndroidApps(resourceName) + : this.requestHandler.listIosApps(resourceName); + }) + .then((responseData: any) => { + this.assertListAppsResponseData(responseData, callerName); + + if (!responseData.apps) { + return []; + } + + return responseData.apps.map((appJson: any) => { + assertServerResponse( + validator.isNonEmptyString(appJson.appId), + responseData, + `"apps[].appId" field must be present in the ${callerName} response data.`); + if (platform === 'android') { + return new AndroidApp(appJson.appId, this.requestHandler); + } else { + return new IosApp(appJson.appId, this.requestHandler); + } + }); + }); + } + + private assertListAppsResponseData(responseData: any, callerName: string): void { + assertServerResponse( + validator.isNonNullObject(responseData), + responseData, + `${callerName}'s responseData must be a non-null object.`); + + if (responseData.apps) { + assertServerResponse( + validator.isArray(responseData.apps), + responseData, + `"apps" field must be present in the ${callerName} response data.`); + } + } +} diff --git a/src/remote-config/condition-evaluator-internal.ts b/src/remote-config/condition-evaluator-internal.ts new file mode 100644 index 0000000000..b23958cd77 --- /dev/null +++ b/src/remote-config/condition-evaluator-internal.ts @@ -0,0 +1,178 @@ +/*! + * Copyright 2024 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 { + AndCondition, + OneOfCondition, + EvaluationContext, + NamedCondition, + OrCondition, + PercentCondition, + PercentConditionOperator +} from './remote-config-api'; +import * as farmhash from 'farmhash'; +import long = require('long'); + +/** + * Encapsulates condition evaluation logic to simplify organization and + * facilitate testing. + * + * @internal + */ +export class ConditionEvaluator { + private static MAX_CONDITION_RECURSION_DEPTH = 10; + + public evaluateConditions( + namedConditions: NamedCondition[], + context: EvaluationContext): Map { + // The order of the conditions is significant. + // A JS Map preserves the order of insertion ("Iteration happens in insertion order" + // - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map#description). + const evaluatedConditions = new Map(); + + for (const namedCondition of namedConditions) { + evaluatedConditions.set( + namedCondition.name, + this.evaluateCondition(namedCondition.condition, context)); + } + + return evaluatedConditions; + } + + private evaluateCondition( + condition: OneOfCondition, + context: EvaluationContext, + nestingLevel = 0): boolean { + if (nestingLevel >= ConditionEvaluator.MAX_CONDITION_RECURSION_DEPTH) { + // TODO: add logging once we have a wrapped logger. + return false; + } + if (condition.orCondition) { + return this.evaluateOrCondition(condition.orCondition, context, nestingLevel + 1) + } + if (condition.andCondition) { + return this.evaluateAndCondition(condition.andCondition, context, nestingLevel + 1) + } + if (condition.true) { + return true; + } + if (condition.false) { + return false; + } + if (condition.percent) { + return this.evaluatePercentCondition(condition.percent, context); + } + // TODO: add logging once we have a wrapped logger. + return false; + } + + private evaluateOrCondition( + orCondition: OrCondition, + context: EvaluationContext, + nestingLevel: number): boolean { + + const subConditions = orCondition.conditions || []; + + for (const subCondition of subConditions) { + // Recursive call. + const result = this.evaluateCondition( + subCondition, context, nestingLevel + 1); + + // Short-circuit the evaluation result for true. + if (result) { + return result; + } + } + return false; + } + + private evaluateAndCondition( + andCondition: AndCondition, + context: EvaluationContext, + nestingLevel: number): boolean { + + const subConditions = andCondition.conditions || []; + + for (const subCondition of subConditions) { + // Recursive call. + const result = this.evaluateCondition( + subCondition, context, nestingLevel + 1); + + // Short-circuit the evaluation result for false. + if (!result) { + return result; + } + } + return true; + } + + private evaluatePercentCondition( + percentCondition: PercentCondition, + context: EvaluationContext + ): boolean { + if (!context.randomizationId) { + // TODO: add logging once we have a wrapped logger. + return false; + } + + // This is the entry point for processing percent condition data from the response. + // We're not using a proto library, so we can't assume undefined fields have + // default values. + const { seed, percentOperator, microPercent, microPercentRange } = percentCondition; + + if (!percentOperator) { + // TODO: add logging once we have a wrapped logger. + return false; + } + + const normalizedMicroPercent = microPercent || 0; + const normalizedMicroPercentUpperBound = microPercentRange?.microPercentUpperBound || 0; + const normalizedMicroPercentLowerBound = microPercentRange?.microPercentLowerBound || 0; + + const seedPrefix = seed && seed.length > 0 ? `${seed}.` : ''; + const stringToHash = `${seedPrefix}${context.randomizationId}`; + + + // Using a 64-bit long for consistency with the Remote Config fetch endpoint. + let hash64 = long.fromString(farmhash.fingerprint64(stringToHash)); + + // Negate the hash if its value is less than 0. We handle this manually because the + // Long library doesn't provided an absolute value method. + if (hash64.lt(0)) { + hash64 = hash64.negate(); + } + + const instanceMicroPercentile = hash64.mod(100 * 1_000_000); + + switch (percentOperator) { + case PercentConditionOperator.LESS_OR_EQUAL: + return instanceMicroPercentile.lte(normalizedMicroPercent); + case PercentConditionOperator.GREATER_THAN: + return instanceMicroPercentile.gt(normalizedMicroPercent); + case PercentConditionOperator.BETWEEN: + return instanceMicroPercentile.gt(normalizedMicroPercentLowerBound) + && instanceMicroPercentile.lte(normalizedMicroPercentUpperBound); + case PercentConditionOperator.UNKNOWN: + default: + break; + } + + // TODO: add logging once we have a wrapped logger. + return false; + } +} diff --git a/src/remote-config/index.ts b/src/remote-config/index.ts new file mode 100644 index 0000000000..9198284b0e --- /dev/null +++ b/src/remote-config/index.ts @@ -0,0 +1,94 @@ +/*! + * 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. + */ + +/** + * Firebase Remote Config. + * + * @packageDocumentation + */ + +import { App, getApp } from '../app'; +import { FirebaseApp } from '../app/firebase-app'; +import { RemoteConfig } from './remote-config'; + +export { + AndCondition, + DefaultConfig, + EvaluationContext, + ExplicitParameterValue, + GetServerTemplateOptions, + InAppDefaultValue, + InitServerTemplateOptions, + ListVersionsOptions, + ListVersionsResult, + MicroPercentRange, + NamedCondition, + OneOfCondition, + OrCondition, + ParameterValueType, + PercentConditionOperator, + PercentCondition, + RemoteConfigCondition, + RemoteConfigParameter, + RemoteConfigParameterGroup, + RemoteConfigParameterValue, + RemoteConfigTemplate, + RemoteConfigUser, + ServerConfig, + ServerTemplate, + ServerTemplateData, + ServerTemplateDataType, + TagColor, + Value, + ValueSource, + Version, +} from './remote-config-api'; +export { RemoteConfig } from './remote-config'; + +/** + * Gets the {@link RemoteConfig} service for the default app or a given app. + * + * `getRemoteConfig()` can be called with no arguments to access the default + * app's `RemoteConfig` service or as `getRemoteConfig(app)` to access the + * `RemoteConfig` service associated with a specific app. + * + * @example + * ```javascript + * // Get the `RemoteConfig` service for the default app + * const defaultRemoteConfig = getRemoteConfig(); + * ``` + * + * @example + * ```javascript + * // Get the `RemoteConfig` service for a given app + * const otherRemoteConfig = getRemoteConfig(otherApp); + * ``` + * + * @param app - Optional app for which to return the `RemoteConfig` service. + * If not provided, the default `RemoteConfig` service is returned. + * + * @returns The default `RemoteConfig` service if no + * app is provided, or the `RemoteConfig` service associated with the provided + * app. + */ +export function getRemoteConfig(app?: App): RemoteConfig { + if (typeof app === 'undefined') { + app = getApp(); + } + + const firebaseApp: FirebaseApp = app as FirebaseApp; + return firebaseApp.getOrInitService('remoteConfig', (app) => new RemoteConfig(app)); +} diff --git a/src/remote-config/internal/value-impl.ts b/src/remote-config/internal/value-impl.ts new file mode 100644 index 0000000000..6d71476538 --- /dev/null +++ b/src/remote-config/internal/value-impl.ts @@ -0,0 +1,61 @@ +/*! + * Copyright 2024 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 { + Value, + ValueSource, +} from '../remote-config-api'; + +/** + * Implements type-safe getters for parameter values. + * + * Visible for testing. + * + * @internal + */ +export class ValueImpl implements Value { + public static readonly DEFAULT_VALUE_FOR_BOOLEAN = false; + public static readonly DEFAULT_VALUE_FOR_STRING = ''; + public static readonly DEFAULT_VALUE_FOR_NUMBER = 0; + public static readonly BOOLEAN_TRUTHY_VALUES = ['1', 'true', 't', 'yes', 'y', 'on']; + constructor( + private readonly source: ValueSource, + private readonly value = ValueImpl.DEFAULT_VALUE_FOR_STRING) { } + asString(): string { + return this.value; + } + asBoolean(): boolean { + if (this.source === 'static') { + return ValueImpl.DEFAULT_VALUE_FOR_BOOLEAN; + } + return ValueImpl.BOOLEAN_TRUTHY_VALUES.indexOf(this.value.toLowerCase()) >= 0; + } + asNumber(): number { + if (this.source === 'static') { + return ValueImpl.DEFAULT_VALUE_FOR_NUMBER; + } + const num = Number(this.value); + if (isNaN(num)) { + return ValueImpl.DEFAULT_VALUE_FOR_NUMBER; + } + return num; + } + getSource(): ValueSource { + return this.source; + } +} 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..f1a0ad1c10 --- /dev/null +++ b/src/remote-config/remote-config-api-client-internal.ts @@ -0,0 +1,491 @@ +/*! + * 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 '../app'; +import { FirebaseApp } from '../app/firebase-app'; +import { HttpRequestConfig, HttpClient, HttpError, AuthorizedHttpClient, HttpResponse } from '../utils/api-request'; +import { PrefixedFirebaseError } from '../utils/error'; +import * as utils from '../utils/index'; +import * as validator from '../utils/validator'; +import { deepCopy } from '../utils/deep-copy'; +import { + ListVersionsOptions, + ListVersionsResult, + RemoteConfigTemplate, + ServerTemplateData +} from './remote-config-api'; + +// Remote Config backend constants +/** + * Allows the `FIREBASE_REMOTE_CONFIG_URL_BASE` environment + * variable to override the default API endpoint URL. + */ +const FIREBASE_REMOTE_CONFIG_URL_BASE = process.env.FIREBASE_REMOTE_CONFIG_URL_BASE || 'https://firebaseremoteconfig.googleapis.com'; +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. + * + * @internal + */ +export class RemoteConfigApiClient { + private readonly httpClient: HttpClient; + private projectIdPrefix?: string; + + constructor(private readonly app: App) { + 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 as FirebaseApp); + } + + 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); + }); + } + + public getServerTemplate(): Promise { + return this.getUrl() + .then((url) => { + const request: HttpRequestConfig = { + method: 'GET', + url: `${url}/namespaces/firebase-server/serverRemoteConfig`, + headers: FIREBASE_REMOTE_CONFIG_HEADERS + }; + return this.httpClient.send(request); + }) + .then((resp) => { + return this.toRemoteConfigServerTemplate(resp); + }) + .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_URL_BASE}/v1/${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, + }; + } + + /** + * Creates a RemoteConfigServerTemplate 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 toRemoteConfigServerTemplate(resp: HttpResponse, customEtag?: string): ServerTemplateData { + const etag = (typeof customEtag === 'undefined') ? resp.headers['etag'] : customEtag; + this.validateEtag(etag); + return { + conditions: resp.data.conditions, + parameters: resp.data.parameters, + 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. + * + * @returns {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.ts b/src/remote-config/remote-config-api.ts new file mode 100644 index 0000000000..4a9a2cfc4e --- /dev/null +++ b/src/remote-config/remote-config-api.ts @@ -0,0 +1,648 @@ +/*! + * 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. + */ + +/** + * Colors that are associated with conditions for display purposes. + */ +export type TagColor = 'BLUE' | 'BROWN' | 'CYAN' | 'DEEP_ORANGE' | 'GREEN' | + 'INDIGO' | 'LIME' | 'ORANGE' | 'PINK' | 'PURPLE' | 'TEAL'; + +/** + * Type representing a Remote Config parameter value data type. + * Defaults to `STRING` if unspecified. + */ +export type ParameterValueType = 'STRING' | 'BOOLEAN' | 'NUMBER' | 'JSON' + +/** + * 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; +} + +/** + * Represents a Remote Config condition in the dataplane. + * A condition targets a specific group of users. A list of these conditions + * comprise part of a Remote Config template. + */ +export interface NamedCondition { + + /** + * 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. + */ + condition: OneOfCondition; +} + +/** + * Represents a condition that may be one of several types. + * Only the first defined field will be processed. + */ +export interface OneOfCondition { + + /** + * Makes this condition an OR condition. + */ + orCondition?: OrCondition; + + /** + * Makes this condition an AND condition. + */ + andCondition?: AndCondition; + + /** + * Makes this condition a constant true. + */ + true?: Record; + + /** + * Makes this condition a constant false. + */ + false?: Record; + + /** + * Makes this condition a percent condition. + */ + percent?: PercentCondition; +} + +/** + * Represents a collection of conditions that evaluate to true if all are true. + */ +export interface AndCondition { + + /** + * The collection of conditions. + */ + conditions?: Array; +} + +/** + * Represents a collection of conditions that evaluate to true if any are true. + */ +export interface OrCondition { + + /** + * The collection of conditions. + */ + conditions?: Array; +} + +/** + * Defines supported operators for percent conditions. + */ +export enum PercentConditionOperator { + + /** + * A catchall error case. + */ + UNKNOWN = 'UNKNOWN', + + /** + * Target percentiles less than or equal to the target percent. + * A condition using this operator must specify microPercent. + */ + LESS_OR_EQUAL = 'LESS_OR_EQUAL', + + /** + * Target percentiles greater than the target percent. + * A condition using this operator must specify microPercent. + */ + GREATER_THAN = 'GREATER_THAN', + + /** + * Target percentiles within an interval defined by a lower bound and an + * upper bound. The lower bound is an exclusive (open) bound and the + * micro_percent_range_upper_bound is an inclusive (closed) bound. + * A condition using this operator must specify microPercentRange. + */ + BETWEEN = 'BETWEEN' +} + +/** + * Represents the limit of percentiles to target in micro-percents. + * The value must be in the range [0 and 100000000] + */ +export interface MicroPercentRange { + + /** + * The lower limit of percentiles to target in micro-percents. + * The value must be in the range [0 and 100000000]. + */ + microPercentLowerBound?: number; + + /** + * The upper limit of percentiles to target in micro-percents. + * The value must be in the range [0 and 100000000]. + */ + microPercentUpperBound?: number; +} + +/** + * Represents a condition that compares the instance pseudo-random + * percentile to a given limit. + */ +export interface PercentCondition { + + /** + * The choice of percent operator to determine how to compare targets + * to percent(s). + */ + percentOperator?: PercentConditionOperator; + + /** + * The limit of percentiles to target in micro-percents when + * using the LESS_OR_EQUAL and GREATER_THAN operators. The value must + * be in the range [0 and 100000000]. + */ + microPercent?: number; + + /** + * The seed used when evaluating the hash function to map an instance to + * a value in the hash space. This is a string which can have 0 - 32 + * characters and can contain ASCII characters [-_.0-9a-zA-Z].The string + * is case-sensitive. + */ + seed?: string; + + /** + * The micro-percent interval to be used with the + * BETWEEN operator. + */ + microPercentRange?: MicroPercentRange; +} + +/** + * 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; + + /** + * The data type for all values of this parameter in the current version of the template. + * Defaults to `ParameterValueType.STRING` if unspecified. + */ + valueType?: ParameterValueType; +} + +/** + * 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 }; +} + +/** + * Represents a Remote Config client 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; +} + +/** + * Represents the data in a Remote Config server template. + */ +export interface ServerTemplateData { + /** + * A list of conditions in descending order by priority. + */ + conditions: NamedCondition[]; + + /** + * Map of parameter keys to their optional default values and optional conditional values. + */ + parameters: { [key: string]: RemoteConfigParameter }; + + /** + * Current Remote Config template ETag (read-only). + */ + readonly etag: string; + + /** + * Version information for the current Remote Config template. + */ + version?: Version; +} + +/** + * Represents optional arguments that can be used when instantiating {@link ServerTemplate}. + */ +export interface GetServerTemplateOptions { + + /** + * Defines in-app default parameter values, so that your app behaves as + * intended before it connects to the Remote Config backend, and so that + * default values are available if none are set on the backend. + */ + defaultConfig?: DefaultConfig; +} + +/** + * Represents the type of a Remote Config server template that can be set on + * {@link ServerTemplate}. This can either be a {@link ServerTemplateData} object + * or a template JSON string. + */ +export type ServerTemplateDataType = ServerTemplateData | string; + +/** + * Represents optional arguments that can be used when instantiating + * {@link ServerTemplate} synchronously. + */ +export interface InitServerTemplateOptions extends GetServerTemplateOptions { + + /** + * Enables integrations to use template data loaded independently. For + * example, customers can reduce initialization latency by pre-fetching and + * caching template data and then using this option to initialize the SDK with + * that data. + */ + template?: ServerTemplateDataType, +} + +/** + * Represents a stateful abstraction for a Remote Config server template. + */ +export interface ServerTemplate { + /** + * Evaluates the current template to produce a {@link ServerConfig}. + */ + evaluate(context?: EvaluationContext): ServerConfig; + + /** + * Fetches and caches the current active version of the + * project's {@link ServerTemplate}. + */ + load(): Promise; + + /** + * Sets and caches a {@link ServerTemplateData} or a JSON string representing + * the server template + */ + set(template: ServerTemplateDataType): void; + + /** + * Returns a JSON representation of {@link ServerTemplateData} + */ + toJSON(): ServerTemplateData; +} + +/** + * Represents template evaluation input signals. + */ +export type EvaluationContext = { + + /** + * Defines the identifier to use when splitting a group. For example, + * this is used by the percent condition. + */ + randomizationId?: 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; +} + +/** + * 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; +} + +/** + * Represents the configuration produced by evaluating a server template. + */ +export interface ServerConfig { + + /** + * Gets the value for the given key as a boolean. + * + * Convenience method for calling serverConfig.getValue(key).asBoolean(). + * + * @param key - The name of the parameter. + * + * @returns The value for the given key as a boolean. + */ + getBoolean(key: string): boolean; + + /** + * Gets the value for the given key as a number. + * + * Convenience method for calling serverConfig.getValue(key).asNumber(). + * + * @param key - The name of the parameter. + * + * @returns The value for the given key as a number. + */ + getNumber(key: string): number; + + /** + * Gets the value for the given key as a string. + * Convenience method for calling serverConfig.getValue(key).asString(). + * + * @param key - The name of the parameter. + * + * @returns The value for the given key as a string. + */ + getString(key: string): string; + + /** + * Gets the {@link Value} for the given key. + * + * Ensures application logic will always have a type-safe reference, + * even if the parameter is removed remotely. + * + * @param key - The name of the parameter. + * + * @returns The value for the given key. + */ + getValue(key: string): Value; +} + +/** + * Wraps a parameter value with metadata and type-safe getters. + * + * Type-safe getters insulate application logic from remote + * changes to parameter names and types. + */ +export interface Value { + + /** + * Gets the value as a boolean. + * + * The following values (case insensitive) are interpreted as true: + * "1", "true", "t", "yes", "y", "on". Other values are interpreted as false. + */ + asBoolean(): boolean; + + /** + * Gets the value as a number. Comparable to calling Number(value) || 0. + */ + asNumber(): number; + + /** + * Gets the value as a string. + */ + asString(): string; + + /** + * Gets the {@link ValueSource} for the given key. + */ + getSource(): ValueSource; +} + +/** + * Indicates the source of a value. + * + *
    + *
  • "static" indicates the value was defined by a static constant.
  • + *
  • "default" indicates the value was defined by default config.
  • + *
  • "remote" indicates the value was defined by config produced by + * evaluating a template.
  • + *
+ */ +export type ValueSource = 'static' | 'default' | 'remote'; + +/** + * Defines the format for in-app default parameter values. + */ +export type DefaultConfig = { [key: string]: string | number | boolean }; diff --git a/src/remote-config/remote-config-namespace.ts b/src/remote-config/remote-config-namespace.ts new file mode 100644 index 0000000000..4efb3baee1 --- /dev/null +++ b/src/remote-config/remote-config-namespace.ts @@ -0,0 +1,135 @@ +/*! + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { App } from '../app'; +import { + ExplicitParameterValue as TExplicitParameterValue, + InAppDefaultValue as TInAppDefaultValue, + ListVersionsOptions as TListVersionsOptions, + ListVersionsResult as TListVersionsResult, + ParameterValueType as TParameterValueType, + RemoteConfigCondition as TRemoteConfigCondition, + RemoteConfigParameter as TRemoteConfigParameter, + RemoteConfigParameterGroup as TRemoteConfigParameterGroup, + RemoteConfigParameterValue as TRemoteConfigParameterValue, + RemoteConfigTemplate as TRemoteConfigTemplate, + RemoteConfigUser as TRemoteConfigUser, + TagColor as TTagColor, + Version as TVersion, +} from './remote-config-api'; +import { RemoteConfig as TRemoteConfig } from './remote-config'; + +/** + * Gets the {@link firebase-admin.remote-config#RemoteConfig} service for the + * default app or a given app. + * + * `admin.remoteConfig()` can be called with no arguments to access the default + * app's `RemoteConfig` service or as `admin.remoteConfig(app)` to access the + * `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. + * + * @returns The default `RemoteConfig` service if no + * app is provided, or the `RemoteConfig` service associated with the provided + * app. + */ +export declare function remoteConfig(app?: App): remoteConfig.RemoteConfig; + +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace remoteConfig { + /** + * Type alias to {@link firebase-admin.remote-config#ExplicitParameterValue}. + */ + export type ExplicitParameterValue = TExplicitParameterValue; + + /** + * Type alias to {@link firebase-admin.remote-config#InAppDefaultValue}. + */ + export type InAppDefaultValue = TInAppDefaultValue; + + /** + * Type alias to {@link firebase-admin.remote-config#ListVersionsOptions}. + */ + export type ListVersionsOptions = TListVersionsOptions; + + /** + * Type alias to {@link firebase-admin.remote-config#ListVersionsResult}. + */ + export type ListVersionsResult = TListVersionsResult; + + /** + * Type alias to {@link firebase-admin.remote-config#ParameterValueType}. + */ + export type ParameterValueType = TParameterValueType; + + /** + * Type alias to {@link firebase-admin.remote-config#RemoteConfig}. + */ + export type RemoteConfig = TRemoteConfig; + + /** + * Type alias to {@link firebase-admin.remote-config#RemoteConfigCondition}. + */ + export type RemoteConfigCondition = TRemoteConfigCondition; + + /** + * Type alias to {@link firebase-admin.remote-config#RemoteConfigParameter}. + */ + export type RemoteConfigParameter = TRemoteConfigParameter; + + /** + * Type alias to {@link firebase-admin.remote-config#RemoteConfigParameterGroup}. + */ + export type RemoteConfigParameterGroup = TRemoteConfigParameterGroup; + + /** + * Type alias to {@link firebase-admin.remote-config#RemoteConfigParameterValue}. + */ + export type RemoteConfigParameterValue = TRemoteConfigParameterValue; + + /** + * Type alias to {@link firebase-admin.remote-config#RemoteConfigTemplate}. + */ + export type RemoteConfigTemplate = TRemoteConfigTemplate; + + /** + * Type alias to {@link firebase-admin.remote-config#RemoteConfigUser}. + */ + export type RemoteConfigUser = TRemoteConfigUser; + + /** + * Type alias to {@link firebase-admin.remote-config#TagColor}. + */ + export type TagColor = TTagColor; + + /** + * Type alias to {@link firebase-admin.remote-config#Version}. + */ + export type Version = TVersion; +} diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts new file mode 100644 index 0000000000..c529501315 --- /dev/null +++ b/src/remote-config/remote-config.ts @@ -0,0 +1,615 @@ +/*! + * 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 '../app'; +import * as validator from '../utils/validator'; +import { FirebaseRemoteConfigError, RemoteConfigApiClient } from './remote-config-api-client-internal'; +import { ConditionEvaluator } from './condition-evaluator-internal'; +import { ValueImpl } from './internal/value-impl'; +import { + ListVersionsOptions, + ListVersionsResult, + RemoteConfigCondition, + RemoteConfigParameter, + RemoteConfigParameterGroup, + ServerTemplate, + RemoteConfigTemplate, + RemoteConfigUser, + Version, + ExplicitParameterValue, + InAppDefaultValue, + ServerConfig, + RemoteConfigParameterValue, + EvaluationContext, + ServerTemplateData, + NamedCondition, + Value, + DefaultConfig, + GetServerTemplateOptions, + InitServerTemplateOptions, + ServerTemplateDataType, +} from './remote-config-api'; + +/** + * The Firebase `RemoteConfig` service interface. + */ +export class RemoteConfig { + + private readonly client: RemoteConfigApiClient; + + /** + * @param app - The app for this RemoteConfig service. + * @constructor + * @internal + */ + constructor(readonly app: App) { + this.client = new RemoteConfigApiClient(app); + } + + /** + * Gets the current active version of the {@link RemoteConfigTemplate} of the project. + * + * @returns A promise that fulfills with a `RemoteConfigTemplate`. + */ + public getTemplate(): Promise { + return this.client.getTemplate() + .then((templateResponse) => { + return new RemoteConfigTemplateImpl(templateResponse); + }); + } + + /** + * Gets the requested version of the {@link RemoteConfigTemplate} of the project. + * + * @param versionNumber - Version number of the Remote Config template to look up. + * + * @returns A promise that fulfills with a `RemoteConfigTemplate`. + */ + public getTemplateAtVersion(versionNumber: number | string): Promise { + return this.client.getTemplateAtVersion(versionNumber) + .then((templateResponse) => { + return new RemoteConfigTemplateImpl(templateResponse); + }); + } + + /** + * Validates a {@link RemoteConfigTemplate}. + * + * @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) + .then((templateResponse) => { + return new RemoteConfigTemplateImpl(templateResponse); + }); + } + + /** + * 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: + * - `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}. + * + * @returns A Promise that fulfills with the published `RemoteConfigTemplate`. + */ + public publishTemplate(template: RemoteConfigTemplate, options?: { force: boolean }): Promise { + return this.client.publishTemplate(template, options) + .then((templateResponse) => { + return new RemoteConfigTemplateImpl(templateResponse); + }); + } + + /** + * 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. + * @returns A promise that fulfills with the published `RemoteConfigTemplate`. + */ + public rollback(versionNumber: number | string): Promise { + return this.client.rollback(versionNumber) + .then((templateResponse) => { + return new RemoteConfigTemplateImpl(templateResponse); + }); + } + + /** + * 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 options - Optional options object for getting a list of versions. + * @returns A promise that fulfills with a `ListVersionsResult`. + */ + public listVersions(options?: ListVersionsOptions): Promise { + return this.client.listVersions(options) + .then((listVersionsResponse) => { + return { + versions: listVersionsResponse.versions?.map(version => new VersionImpl(version)) ?? [], + nextPageToken: listVersionsResponse.nextPageToken, + } + }); + } + + /** + * Creates and returns a new Remote Config template from a JSON string. + * + * @param json - The JSON string to populate a Remote Config template. + * + * @returns A new template instance. + */ + public createTemplateFromJSON(json: string): RemoteConfigTemplate { + if (!validator.isNonEmptyString(json)) { + throw new FirebaseRemoteConfigError( + 'invalid-argument', + 'JSON string must be a valid non-empty string'); + } + + let template: RemoteConfigTemplate; + try { + template = JSON.parse(json); + } catch (e) { + throw new FirebaseRemoteConfigError( + 'invalid-argument', + `Failed to parse the JSON string: ${json}. ` + e + ); + } + + return new RemoteConfigTemplateImpl(template); + } + + /** + * Instantiates {@link ServerTemplate} and then fetches and caches the latest + * template version of the project. + */ + public async getServerTemplate(options?: GetServerTemplateOptions): Promise { + const template = this.initServerTemplate(options); + await template.load(); + return template; + } + + /** + * Synchronously instantiates {@link ServerTemplate}. + */ + public initServerTemplate(options?: InitServerTemplateOptions): ServerTemplate { + const template = new ServerTemplateImpl( + this.client, new ConditionEvaluator(), options?.defaultConfig); + + if (options?.template) { + template.set(options?.template); + } + + return template; + } +} + +/** + * Remote Config template internal implementation. + */ +class RemoteConfigTemplateImpl implements RemoteConfigTemplate { + + public parameters: { [key: string]: RemoteConfigParameter }; + public parameterGroups: { [key: string]: RemoteConfigParameterGroup }; + public conditions: RemoteConfigCondition[]; + private readonly etagInternal: string; + public version?: Version; + + constructor(config: RemoteConfigTemplate) { + if (!validator.isNonNullObject(config) || + !validator.isNonEmptyString(config.etag)) { + throw new FirebaseRemoteConfigError( + 'invalid-argument', + `Invalid Remote Config template: ${JSON.stringify(config)}`); + } + + this.etagInternal = config.etag; + + if (typeof config.parameters !== 'undefined') { + if (!validator.isNonNullObject(config.parameters)) { + throw new FirebaseRemoteConfigError( + 'invalid-argument', + 'Remote Config parameters must be a non-null object'); + } + this.parameters = config.parameters; + } else { + this.parameters = {}; + } + + if (typeof config.parameterGroups !== 'undefined') { + if (!validator.isNonNullObject(config.parameterGroups)) { + throw new FirebaseRemoteConfigError( + 'invalid-argument', + 'Remote Config parameter groups must be a non-null object'); + } + this.parameterGroups = config.parameterGroups; + } else { + this.parameterGroups = {}; + } + + if (typeof config.conditions !== 'undefined') { + if (!validator.isArray(config.conditions)) { + throw new FirebaseRemoteConfigError( + 'invalid-argument', + 'Remote Config conditions must be an array'); + } + this.conditions = config.conditions; + } else { + this.conditions = []; + } + + if (typeof config.version !== 'undefined') { + this.version = new VersionImpl(config.version); + } + } + + /** + * Gets the ETag of the template. + * + * @returns The ETag of the Remote Config template. + */ + get etag(): string { + return this.etagInternal; + } + + /** + * Returns a JSON-serializable representation of this object. + * + * @returns A JSON-serializable representation of this object. + */ + public toJSON(): object { + return { + conditions: this.conditions, + parameters: this.parameters, + parameterGroups: this.parameterGroups, + etag: this.etag, + version: this.version, + } + } +} + +/** + * Remote Config dataplane template data implementation. + */ +class ServerTemplateImpl implements ServerTemplate { + private cache: ServerTemplateData; + private stringifiedDefaultConfig: {[key: string]: string} = {}; + + constructor( + private readonly apiClient: RemoteConfigApiClient, + private readonly conditionEvaluator: ConditionEvaluator, + public readonly defaultConfig: DefaultConfig = {} + ) { + // RC stores all remote values as string, but it's more intuitive + // to declare default values with specific types, so this converts + // the external declaration to an internal string representation. + for (const key in defaultConfig) { + this.stringifiedDefaultConfig[key] = String(defaultConfig[key]); + } + } + + /** + * Fetches and caches the current active version of the project's {@link ServerTemplate}. + */ + public load(): Promise { + return this.apiClient.getServerTemplate() + .then((template) => { + this.cache = new ServerTemplateDataImpl(template); + }); + } + + /** + * Parses a {@link ServerTemplateDataType} and caches it. + */ + public set(template: ServerTemplateDataType): void { + let parsed; + if (validator.isString(template)) { + try { + parsed = JSON.parse(template); + } catch (e) { + // Transforms JSON parse errors to Firebase error. + throw new FirebaseRemoteConfigError( + 'invalid-argument', + `Failed to parse the JSON string: ${template}. ` + e); + } + } else { + parsed = template; + } + // Throws template parse errors. + this.cache = new ServerTemplateDataImpl(parsed); + } + + /** + * Evaluates the current template in cache to produce a {@link ServerConfig}. + */ + public evaluate(context: EvaluationContext = {}): ServerConfig { + if (!this.cache) { + + // This is the only place we should throw during evaluation, since it's under the + // control of application logic. To preserve forward-compatibility, we should only + // return false in cases where the SDK is unsure how to evaluate the fetched template. + throw new FirebaseRemoteConfigError( + 'failed-precondition', + 'No Remote Config Server template in cache. Call load() before calling evaluate().'); + } + + const evaluatedConditions = this.conditionEvaluator.evaluateConditions( + this.cache.conditions, context); + + const configValues: { [key: string]: Value } = {}; + + // Initializes config Value objects with default values. + for (const key in this.stringifiedDefaultConfig) { + configValues[key] = new ValueImpl('default', this.stringifiedDefaultConfig[key]); + } + + // Overlays config Value objects derived by evaluating the template. + for (const [key, parameter] of Object.entries(this.cache.parameters)) { + const { conditionalValues, defaultValue } = parameter; + + // Supports parameters with no conditional values. + const normalizedConditionalValues = conditionalValues || {}; + + let parameterValueWrapper: RemoteConfigParameterValue | undefined = undefined; + + // Iterates in order over condition list. If there is a value associated + // with a condition, this checks if the condition is true. + for (const [conditionName, conditionEvaluation] of evaluatedConditions) { + if (normalizedConditionalValues[conditionName] && conditionEvaluation) { + parameterValueWrapper = normalizedConditionalValues[conditionName]; + break; + } + } + + if (parameterValueWrapper && (parameterValueWrapper as InAppDefaultValue).useInAppDefault) { + // TODO: add logging once we have a wrapped logger. + continue; + } + + if (parameterValueWrapper) { + const parameterValue = (parameterValueWrapper as ExplicitParameterValue).value; + configValues[key] = new ValueImpl('remote', parameterValue); + continue; + } + + if (!defaultValue) { + // TODO: add logging once we have a wrapped logger. + continue; + } + + if ((defaultValue as InAppDefaultValue).useInAppDefault) { + // TODO: add logging once we have a wrapped logger. + continue; + } + + const parameterDefaultValue = (defaultValue as ExplicitParameterValue).value; + configValues[key] = new ValueImpl('remote', parameterDefaultValue); + } + + return new ServerConfigImpl(configValues); + } + + /** + * @returns JSON representation of the server template + */ + public toJSON(): ServerTemplateData { + return this.cache; + } +} + +class ServerConfigImpl implements ServerConfig { + constructor( + private readonly configValues: { [key: string]: Value }, + ){} + getBoolean(key: string): boolean { + return this.getValue(key).asBoolean(); + } + getNumber(key: string): number { + return this.getValue(key).asNumber(); + } + getString(key: string): string { + return this.getValue(key).asString(); + } + getValue(key: string): Value { + return this.configValues[key] || new ValueImpl('static'); + } +} + +/** + * Remote Config dataplane template data implementation. + */ +class ServerTemplateDataImpl implements ServerTemplateData { + public parameters: { [key: string]: RemoteConfigParameter }; + public parameterGroups: { [key: string]: RemoteConfigParameterGroup }; + public conditions: NamedCondition[]; + public readonly etag: string; + public version?: Version; + + constructor(template: ServerTemplateData) { + if (!validator.isNonNullObject(template) || + !validator.isNonEmptyString(template.etag)) { + throw new FirebaseRemoteConfigError( + 'invalid-argument', + `Invalid Remote Config template: ${JSON.stringify(template)}`); + } + + this.etag = template.etag; + if (typeof template.parameters !== 'undefined') { + if (!validator.isNonNullObject(template.parameters)) { + throw new FirebaseRemoteConfigError( + 'invalid-argument', + 'Remote Config parameters must be a non-null object'); + } + this.parameters = template.parameters; + } else { + this.parameters = {}; + } + + if (typeof template.conditions !== 'undefined') { + if (!validator.isArray(template.conditions)) { + throw new FirebaseRemoteConfigError( + 'invalid-argument', + 'Remote Config conditions must be an array'); + } + this.conditions = template.conditions; + } else { + this.conditions = []; + } + + if (typeof template.version !== 'undefined') { + this.version = new VersionImpl(template.version); + } + } +} + +/** +* Remote Config Version internal implementation. +*/ +class VersionImpl implements Version { + public readonly versionNumber?: string; // int64 format + public readonly updateTime?: string; // in UTC + public readonly updateOrigin?: ('REMOTE_CONFIG_UPDATE_ORIGIN_UNSPECIFIED' | 'CONSOLE' | + 'REST_API' | 'ADMIN_SDK_NODE'); + public readonly updateType?: ('REMOTE_CONFIG_UPDATE_TYPE_UNSPECIFIED' | + 'INCREMENTAL_UPDATE' | 'FORCED_UPDATE' | 'ROLLBACK'); + public readonly updateUser?: RemoteConfigUser; + public readonly description?: string; + public readonly rollbackSource?: string; + public readonly isLegacy?: boolean; + + constructor(version: Version) { + if (!validator.isNonNullObject(version)) { + throw new FirebaseRemoteConfigError( + 'invalid-argument', + `Invalid Remote Config version instance: ${JSON.stringify(version)}`); + } + + if (typeof version.versionNumber !== 'undefined') { + if (!validator.isNonEmptyString(version.versionNumber) && + !validator.isNumber(version.versionNumber)) { + throw new FirebaseRemoteConfigError( + 'invalid-argument', + 'Version number must be a non-empty string in int64 format or a number'); + } + if (!Number.isInteger(Number(version.versionNumber))) { + throw new FirebaseRemoteConfigError( + 'invalid-argument', + 'Version number must be an integer or a string in int64 format'); + } + this.versionNumber = version.versionNumber; + } + + if (typeof version.updateOrigin !== 'undefined') { + if (!validator.isNonEmptyString(version.updateOrigin)) { + throw new FirebaseRemoteConfigError( + 'invalid-argument', + 'Version update origin must be a non-empty string'); + } + this.updateOrigin = version.updateOrigin; + } + + if (typeof version.updateType !== 'undefined') { + if (!validator.isNonEmptyString(version.updateType)) { + throw new FirebaseRemoteConfigError( + 'invalid-argument', + 'Version update type must be a non-empty string'); + } + this.updateType = version.updateType; + } + + if (typeof version.updateUser !== 'undefined') { + if (!validator.isNonNullObject(version.updateUser)) { + throw new FirebaseRemoteConfigError( + 'invalid-argument', + 'Version update user must be a non-null object'); + } + this.updateUser = version.updateUser; + } + + if (typeof version.description !== 'undefined') { + if (!validator.isNonEmptyString(version.description)) { + throw new FirebaseRemoteConfigError( + 'invalid-argument', + 'Version description must be a non-empty string'); + } + this.description = version.description; + } + + if (typeof version.rollbackSource !== 'undefined') { + if (!validator.isNonEmptyString(version.rollbackSource)) { + throw new FirebaseRemoteConfigError( + 'invalid-argument', + 'Version rollback source must be a non-empty string'); + } + this.rollbackSource = version.rollbackSource; + } + + if (typeof version.isLegacy !== 'undefined') { + if (!validator.isBoolean(version.isLegacy)) { + throw new FirebaseRemoteConfigError( + 'invalid-argument', + 'Version.isLegacy must be a boolean'); + } + this.isLegacy = version.isLegacy; + } + + // 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)) { + throw new FirebaseRemoteConfigError( + 'invalid-argument', + 'Version update time must be a valid date string'); + } + this.updateTime = new Date(version.updateTime).toUTCString(); + } + } + + /** + * @returns A JSON-serializable representation of this object. + */ + public toJSON(): object { + return { + versionNumber: this.versionNumber, + updateOrigin: this.updateOrigin, + updateType: this.updateType, + updateUser: this.updateUser, + description: this.description, + rollbackSource: this.rollbackSource, + isLegacy: this.isLegacy, + updateTime: this.updateTime, + } + } + + 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/src/security-rules/index.ts b/src/security-rules/index.ts new file mode 100644 index 0000000000..b3b78a265a --- /dev/null +++ b/src/security-rules/index.ts @@ -0,0 +1,67 @@ +/*! + * 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. + */ + +/** + * Security Rules for Cloud Firestore and Cloud Storage. + * + * @packageDocumentation + */ + +import { App, getApp } from '../app'; +import { FirebaseApp } from '../app/firebase-app'; +import { SecurityRules } from './security-rules'; + +export { + RulesFile, + Ruleset, + RulesetMetadata, + RulesetMetadataList, + SecurityRules, +} from './security-rules'; + +/** + * Gets the {@link SecurityRules} service for the default app or a given app. + * + * `admin.securityRules()` can be called with no arguments to access the + * default app's `SecurityRules` service, or as `admin.securityRules(app)` to access + * the `SecurityRules` service associated with a specific app. + * + * @example + * ```javascript + * // Get the SecurityRules service for the default app + * const defaultSecurityRules = getSecurityRules(); + * ``` + * + * @example + * ```javascript + * // Get the SecurityRules service for a given app + * const otherSecurityRules = getSecurityRules(otherApp); + * ``` + * + * @param app - Optional app to return the `SecurityRules` service + * for. If not provided, the default `SecurityRules` service + * is returned. + * @returns The default `SecurityRules` service if no app is provided, or the + * `SecurityRules` service associated with the provided app. + */ +export function getSecurityRules(app?: App): SecurityRules { + if (typeof app === 'undefined') { + app = getApp(); + } + + const firebaseApp: FirebaseApp = app as FirebaseApp; + return firebaseApp.getOrInitService('securityRules', (app) => new SecurityRules(app)); +} diff --git a/src/security-rules/security-rules-api-client-internal.ts b/src/security-rules/security-rules-api-client-internal.ts new file mode 100644 index 0000000000..6d089b4475 --- /dev/null +++ b/src/security-rules/security-rules-api-client-internal.ts @@ -0,0 +1,324 @@ +/*! + * 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 { HttpRequestConfig, HttpClient, HttpError, AuthorizedHttpClient } from '../utils/api-request'; +import { PrefixedFirebaseError } from '../utils/error'; +import { FirebaseSecurityRulesError, SecurityRulesErrorCode } from './security-rules-internal'; +import * as utils from '../utils/index'; +import * as validator from '../utils/validator'; +import { FirebaseApp } from '../app/firebase-app'; +import { App } from '../app'; + +const RULES_V1_API = 'https://firebaserules.googleapis.com/v1'; +const FIREBASE_VERSION_HEADER = { + 'X-Firebase-Client': `fire-admin-node/${utils.getSdkVersion()}`, +}; + +export interface Release { + readonly name: string; + readonly rulesetName: string; + readonly createTime?: string; + readonly updateTime?: string; +} + +export interface RulesetContent { + readonly source: { + readonly files: Array<{ name: string; content: string }>; + }; +} + +export interface RulesetResponse extends RulesetContent { + readonly name: string; + readonly createTime: string; +} + +export interface ListRulesetsResponse { + readonly rulesets: Array<{ name: string; createTime: string }>; + readonly nextPageToken?: string; +} + +/** + * Class that facilitates sending requests to the Firebase security rules backend API. + * + * @private + */ +export class SecurityRulesApiClient { + + private readonly httpClient: HttpClient; + private projectIdPrefix?: string; + + constructor(private readonly app: App) { + if (!validator.isNonNullObject(app) || !('options' in app)) { + throw new FirebaseSecurityRulesError( + 'invalid-argument', + 'First argument passed to admin.securityRules() must be a valid Firebase app ' + + 'instance.'); + } + + this.httpClient = new AuthorizedHttpClient(app as FirebaseApp); + } + + public getRuleset(name: string): Promise { + return Promise.resolve() + .then(() => { + return this.getRulesetName(name); + }) + .then((rulesetName) => { + return this.getResource(rulesetName); + }); + } + + public createRuleset(ruleset: RulesetContent): Promise { + if (!validator.isNonNullObject(ruleset) || + !validator.isNonNullObject(ruleset.source) || + !validator.isNonEmptyArray(ruleset.source.files)) { + + const err = new FirebaseSecurityRulesError('invalid-argument', 'Invalid rules content.'); + return Promise.reject(err); + } + + for (const rf of ruleset.source.files) { + if (!validator.isNonNullObject(rf) || + !validator.isNonEmptyString(rf.name) || + !validator.isNonEmptyString(rf.content)) { + + const err = new FirebaseSecurityRulesError( + 'invalid-argument', `Invalid rules file argument: ${JSON.stringify(rf)}`); + return Promise.reject(err); + } + } + + return this.getUrl() + .then((url) => { + const request: HttpRequestConfig = { + method: 'POST', + url: `${url}/rulesets`, + data: ruleset, + }; + return this.sendRequest(request); + }); + } + + public deleteRuleset(name: string): Promise { + return this.getUrl() + .then((url) => { + const rulesetName = this.getRulesetName(name); + const request: HttpRequestConfig = { + method: 'DELETE', + url: `${url}/${rulesetName}`, + }; + return this.sendRequest(request); + }); + } + + public listRulesets(pageSize = 100, pageToken?: string): Promise { + if (!validator.isNumber(pageSize)) { + const err = new FirebaseSecurityRulesError('invalid-argument', 'Invalid page size.'); + return Promise.reject(err); + } + if (pageSize < 1 || pageSize > 100) { + const err = new FirebaseSecurityRulesError( + 'invalid-argument', 'Page size must be between 1 and 100.'); + return Promise.reject(err); + } + if (typeof pageToken !== 'undefined' && !validator.isNonEmptyString(pageToken)) { + const err = new FirebaseSecurityRulesError( + 'invalid-argument', 'Next page token must be a non-empty string.'); + return Promise.reject(err); + } + + const data = { + pageSize, + pageToken, + }; + if (!pageToken) { + delete data.pageToken; + } + + return this.getUrl() + .then((url) => { + const request: HttpRequestConfig = { + method: 'GET', + url: `${url}/rulesets`, + data, + }; + return this.sendRequest(request); + }); + } + + public getRelease(name: string): Promise { + return this.getResource(`releases/${name}`); + } + + public updateOrCreateRelease(name: string, rulesetName: string): Promise { + return this.updateRelease(name, rulesetName).catch((error) => { + // if ruleset update failed with a NOT_FOUND error, attempt to create instead. + if (error.code === `security-rules/${ERROR_CODE_MAPPING.NOT_FOUND}`) { + return this.createRelease(name, rulesetName); + } + throw error; + }); + } + + public updateRelease(name: string, rulesetName: string): Promise { + return this.getUrl() + .then((url) => { + return this.getReleaseDescription(name, rulesetName) + .then((release) => { + const request: HttpRequestConfig = { + method: 'PATCH', + url: `${url}/releases/${name}`, + data: { release }, + }; + return this.sendRequest(request); + }); + }); + } + + public createRelease(name: string, rulesetName: string): Promise { + return this.getUrl() + .then((url) => { + return this.getReleaseDescription(name, rulesetName) + .then((release) => { + const request: HttpRequestConfig = { + method: 'POST', + url: `${url}/releases`, + data: release, + }; + return this.sendRequest(request); + }); + }); + } + + private getUrl(): Promise { + return this.getProjectIdPrefix() + .then((projectIdPrefix) => { + return `${RULES_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 FirebaseSecurityRulesError( + 'invalid-argument', + '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; + }); + } + + /** + * Gets the specified resource from the rules API. Resource names must be the short names without project + * ID prefix (e.g. `rulesets/ruleset-name`). + * + * @param {string} name Full qualified name of the resource to get. + * @returns {Promise} A promise that fulfills with the resource. + */ + private getResource(name: string): Promise { + return this.getUrl() + .then((url) => { + const request: HttpRequestConfig = { + method: 'GET', + url: `${url}/${name}`, + }; + return this.sendRequest(request); + }); + } + + private getReleaseDescription(name: string, rulesetName: string): Promise { + return this.getProjectIdPrefix() + .then((projectIdPrefix) => { + return { + name: `${projectIdPrefix}/releases/${name}`, + rulesetName: `${projectIdPrefix}/${this.getRulesetName(rulesetName)}`, + }; + }); + } + + private getRulesetName(name: string): string { + if (!validator.isNonEmptyString(name)) { + throw new FirebaseSecurityRulesError( + 'invalid-argument', 'Ruleset name must be a non-empty string.'); + } + + if (name.indexOf('/') !== -1) { + throw new FirebaseSecurityRulesError( + 'invalid-argument', 'Ruleset name must not contain any "/" characters.'); + } + + return `rulesets/${name}`; + } + + private sendRequest(request: HttpRequestConfig): Promise { + request.headers = FIREBASE_VERSION_HEADER; + return this.httpClient.send(request) + .then((resp) => { + return resp.data as T; + }) + .catch((err) => { + throw this.toFirebaseError(err); + }); + } + + private toFirebaseError(err: HttpError): PrefixedFirebaseError { + if (err instanceof PrefixedFirebaseError) { + return err; + } + + const response = err.response; + if (!response.isJson()) { + return new FirebaseSecurityRulesError( + 'unknown-error', + `Unexpected response with status: ${response.status} and body: ${response.text}`); + } + + const error: Error = (response.data as ErrorResponse).error || {}; + let code: SecurityRulesErrorCode = 'unknown-error'; + if (error.status && error.status in ERROR_CODE_MAPPING) { + code = ERROR_CODE_MAPPING[error.status]; + } + const message = error.message || `Unknown server error: ${response.text}`; + return new FirebaseSecurityRulesError(code, message); + } +} + +interface ErrorResponse { + error?: Error; +} + +interface Error { + code?: number; + message?: string; + status?: string; +} + +const ERROR_CODE_MAPPING: { [key: string]: SecurityRulesErrorCode } = { + INVALID_ARGUMENT: 'invalid-argument', + NOT_FOUND: 'not-found', + RESOURCE_EXHAUSTED: 'resource-exhausted', + UNAUTHENTICATED: 'authentication-error', + UNKNOWN: 'unknown-error', +}; diff --git a/src/security-rules/security-rules-internal.ts b/src/security-rules/security-rules-internal.ts new file mode 100644 index 0000000000..d21f5a0f0e --- /dev/null +++ b/src/security-rules/security-rules-internal.ts @@ -0,0 +1,34 @@ +/*! + * 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 { PrefixedFirebaseError } from '../utils/error'; + +export type SecurityRulesErrorCode = + 'already-exists' + | 'authentication-error' + | 'internal-error' + | 'invalid-argument' + | 'invalid-server-response' + | 'not-found' + | 'resource-exhausted' + | 'service-unavailable' + | 'unknown-error'; + +export class FirebaseSecurityRulesError extends PrefixedFirebaseError { + constructor(code: SecurityRulesErrorCode, message: string) { + super('security-rules', code, message); + } +} diff --git a/src/security-rules/security-rules-namespace.ts b/src/security-rules/security-rules-namespace.ts new file mode 100644 index 0000000000..52ffc592a7 --- /dev/null +++ b/src/security-rules/security-rules-namespace.ts @@ -0,0 +1,82 @@ +/*! + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { App } from '../app'; +import { + RulesFile as TRulesFile, + Ruleset as TRuleset, + RulesetMetadata as TRulesetMetadata, + RulesetMetadataList as TRulesetMetadataList, + SecurityRules as TSecurityRules, +} from './security-rules'; + +/** + * Gets the {@link firebase-admin.security-rules#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 firebase-admin.security-rules#SecurityRules} + * service, or as `admin.securityRules(app)` to access + * the {@link firebase-admin.security-rules#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. + * @returns The default `SecurityRules` service if no app is provided, or the + * `SecurityRules` service associated with the provided app. + */ +export declare function securityRules(app?: App): securityRules.SecurityRules; + +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace securityRules { + /** + * Type alias to {@link firebase-admin.security-rules#RulesFile}. + */ + export type RulesFile = TRulesFile; + + /** + * Type alias to {@link firebase-admin.security-rules#Ruleset}. + */ + export type Ruleset = TRuleset; + + /** + * Type alias to {@link firebase-admin.security-rules#RulesetMetadata}. + */ + export type RulesetMetadata = TRulesetMetadata; + + /** + * Type alias to {@link firebase-admin.security-rules#RulesetMetadataList}. + */ + export type RulesetMetadataList = TRulesetMetadataList; + + /** + * Type alias to {@link firebase-admin.security-rules#SecurityRules}. + */ + export type SecurityRules = TSecurityRules; +} diff --git a/src/security-rules/security-rules.ts b/src/security-rules/security-rules.ts new file mode 100644 index 0000000000..135e32459b --- /dev/null +++ b/src/security-rules/security-rules.ts @@ -0,0 +1,404 @@ +/*! + * 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 { App } from '../app'; +import * as validator from '../utils/validator'; +import { + SecurityRulesApiClient, RulesetResponse, RulesetContent, ListRulesetsResponse, +} from './security-rules-api-client-internal'; +import { FirebaseSecurityRulesError } from './security-rules-internal'; + +/** + * A source file containing some Firebase security rules. The content includes raw + * source code including text formatting, indentation and comments. Use the + * {@link 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.getRuleset} and {@link SecurityRules.deleteRuleset}. + */ + readonly name: string; + /** + * Creation time of the `Ruleset` as a UTC timestamp string. + */ + readonly createTime: string; +} + +/** + * A page of ruleset metadata. + */ +export class RulesetMetadataList { + + /** + * A batch of ruleset metadata. + */ + public readonly rulesets: RulesetMetadata[]; + + /** + * The next page token if available. This is needed to retrieve the next batch. + */ + public readonly nextPageToken?: string; + + /** + * @internal + */ + constructor(response: ListRulesetsResponse) { + if (!validator.isNonNullObject(response) || !validator.isArray(response.rulesets)) { + throw new FirebaseSecurityRulesError( + 'invalid-argument', + `Invalid ListRulesets response: ${JSON.stringify(response)}`); + } + + this.rulesets = response.rulesets.map((rs) => { + return { + name: stripProjectIdPrefix(rs.name), + createTime: new Date(rs.createTime).toUTCString(), + }; + }); + + if (response.nextPageToken) { + this.nextPageToken = response.nextPageToken; + } + } +} + +/** + * A set of Firebase security rules. + */ +export class Ruleset implements RulesetMetadata { + + /** + * {@inheritdoc RulesetMetadata.name} + */ + public readonly name: string; + + /** + * {@inheritdoc RulesetMetadata.createTime} + */ + public readonly createTime: string; + + public readonly source: RulesFile[]; + + /** + * @internal + */ + constructor(ruleset: RulesetResponse) { + if (!validator.isNonNullObject(ruleset) || + !validator.isNonEmptyString(ruleset.name) || + !validator.isNonEmptyString(ruleset.createTime) || + !validator.isNonNullObject(ruleset.source)) { + throw new FirebaseSecurityRulesError( + 'invalid-argument', + `Invalid Ruleset response: ${JSON.stringify(ruleset)}`); + } + + this.name = stripProjectIdPrefix(ruleset.name); + this.createTime = new Date(ruleset.createTime).toUTCString(); + this.source = ruleset.source.files || []; + } +} + +/** + * The Firebase `SecurityRules` service interface. + */ +export class SecurityRules { + + private static readonly CLOUD_FIRESTORE = 'cloud.firestore'; + private static readonly FIREBASE_STORAGE = 'firebase.storage'; + + private readonly client: SecurityRulesApiClient; + + /** + * @param app - The app for this SecurityRules service. + * @constructor + * @internal + */ + constructor(readonly app: App) { + this.client = new SecurityRulesApiClient(app); + } + + /** + * Gets the {@link 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. + * @returns A promise that fulfills with the specified `Ruleset`. + */ + public getRuleset(name: string): Promise { + return this.client.getRuleset(name) + .then((rulesetResponse) => { + return new Ruleset(rulesetResponse); + }); + } + + /** + * Gets the {@link Ruleset} currently applied to + * Cloud Firestore. Rejects with a `not-found` error if no ruleset is applied + * on Firestore. + * + * @returns A promise that fulfills with the Firestore ruleset. + */ + public getFirestoreRuleset(): Promise { + return this.getRulesetForRelease(SecurityRules.CLOUD_FIRESTORE); + } + + /** + * Creates a new {@link Ruleset} from the given + * source, and applies it to Cloud Firestore. + * + * @param source - Rules source to apply. + * @returns A promise that fulfills when the ruleset is created and released. + */ + public releaseFirestoreRulesetFromSource(source: string | Buffer): Promise { + return Promise.resolve() + .then(() => { + const rulesFile = this.createRulesFileFromSource('firestore.rules', source); + return this.createRuleset(rulesFile); + }) + .then((ruleset) => { + return this.releaseFirestoreRuleset(ruleset) + .then(() => { + return ruleset; + }); + }); + } + + /** + * Applies the specified {@link Ruleset} ruleset + * to Cloud Firestore. + * + * @param ruleset - Name of the ruleset to apply or a `RulesetMetadata` object + * containing the name. + * @returns A promise that fulfills when the ruleset is released. + */ + public releaseFirestoreRuleset(ruleset: string | RulesetMetadata): Promise { + return this.releaseRuleset(ruleset, SecurityRules.CLOUD_FIRESTORE); + } + + /** + * Gets the {@link 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`. + * @returns A promise that fulfills with the Cloud Storage ruleset. + */ + public getStorageRuleset(bucket?: string): Promise { + return Promise.resolve() + .then(() => { + return this.getBucketName(bucket); + }) + .then((bucketName) => { + return this.getRulesetForRelease(`${SecurityRules.FIREBASE_STORAGE}/${bucketName}`); + }); + } + + /** + * Creates a new {@link 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 firebase-admin.app#AppOptions}. + * @returns A promise that fulfills when the ruleset is created and released. + */ + public releaseStorageRulesetFromSource(source: string | Buffer, bucket?: string): Promise { + return Promise.resolve() + .then(() => { + // Bucket name is not required until the last step. But since there's a createRuleset step + // before then, make sure to run this check and fail early if the bucket name is invalid. + this.getBucketName(bucket); + const rulesFile = this.createRulesFileFromSource('storage.rules', source); + return this.createRuleset(rulesFile); + }) + .then((ruleset) => { + return this.releaseStorageRuleset(ruleset, bucket) + .then(() => { + return ruleset; + }); + }); + } + + /** + * Applies the specified {@link 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 firebase-admin.app#AppOptions}. + * @returns A promise that fulfills when the ruleset is released. + */ + public releaseStorageRuleset(ruleset: string | RulesetMetadata, bucket?: string): Promise { + return Promise.resolve() + .then(() => { + return this.getBucketName(bucket); + }) + .then((bucketName) => { + return this.releaseRuleset(ruleset, `${SecurityRules.FIREBASE_STORAGE}/${bucketName}`); + }); + } + + /** + * Creates a {@link RulesFile} 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. + * @returns A new rules file instance. + */ + public createRulesFileFromSource(name: string, source: string | Buffer): RulesFile { + if (!validator.isNonEmptyString(name)) { + throw new FirebaseSecurityRulesError( + 'invalid-argument', 'Name must be a non-empty string.'); + } + + let content: string; + if (validator.isNonEmptyString(source)) { + content = source; + } else if (validator.isBuffer(source)) { + content = source.toString('utf-8'); + } else { + throw new FirebaseSecurityRulesError( + 'invalid-argument', 'Source must be a non-empty string or a Buffer.'); + } + + return { + name, + content, + }; + } + + /** + * Creates a new {@link Ruleset} from the given {@link RulesFile}. + * + * @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 = { + source: { + files: [ file ], + }, + }; + + return this.client.createRuleset(ruleset) + .then((rulesetResponse) => { + return new Ruleset(rulesetResponse); + }); + } + + /** + * Deletes the {@link 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. + * @returns A promise that fulfills when the `Ruleset` is deleted. + */ + public deleteRuleset(name: string): Promise { + return this.client.deleteRuleset(name); + } + + /** + * 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. + * @returns A promise that fulfills with a page of rulesets. + */ + public listRulesetMetadata(pageSize = 100, nextPageToken?: string): Promise { + return this.client.listRulesets(pageSize, nextPageToken) + .then((response) => { + return new RulesetMetadataList(response); + }); + } + + private getRulesetForRelease(releaseName: string): Promise { + return this.client.getRelease(releaseName) + .then((release) => { + const rulesetName = release.rulesetName; + if (!validator.isNonEmptyString(rulesetName)) { + throw new FirebaseSecurityRulesError( + 'not-found', `Ruleset name not found for ${releaseName}.`); + } + + return this.getRuleset(stripProjectIdPrefix(rulesetName)); + }); + } + + private releaseRuleset(ruleset: string | RulesetMetadata, releaseName: string): Promise { + if (!validator.isNonEmptyString(ruleset) && + (!validator.isNonNullObject(ruleset) || !validator.isNonEmptyString(ruleset.name))) { + const err = new FirebaseSecurityRulesError( + 'invalid-argument', 'ruleset must be a non-empty name or a RulesetMetadata object.'); + return Promise.reject(err); + } + + const rulesetName = validator.isString(ruleset) ? ruleset : ruleset.name; + return this.client.updateOrCreateRelease(releaseName, rulesetName) + .then(() => { + return; + }); + } + + private getBucketName(bucket?: string): string { + const bucketName = (typeof bucket !== 'undefined') ? bucket : this.app.options.storageBucket; + if (!validator.isNonEmptyString(bucketName)) { + throw new FirebaseSecurityRulesError( + 'invalid-argument', + 'Bucket name not specified or invalid. Specify a default bucket name via the ' + + 'storageBucket option when initializing the app, or specify the bucket name ' + + 'explicitly when calling the rules API.', + ); + } + + return bucketName; + } +} + +function stripProjectIdPrefix(name: string): string { + return name.split('/').pop()!; +} diff --git a/src/storage/index.ts b/src/storage/index.ts new file mode 100644 index 0000000000..7b963593e8 --- /dev/null +++ b/src/storage/index.ts @@ -0,0 +1,90 @@ +/*! + * 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. + */ + +/** + * Cloud Storage for Firebase. + * + * @packageDocumentation + */ + +import { File } from '@google-cloud/storage'; +import { App, getApp } from '../app'; +import { FirebaseApp } from '../app/firebase-app'; +import { Storage } from './storage'; +import { FirebaseError } from '../utils/error'; +import { getFirebaseMetadata } from './utils'; + +export { Storage } from './storage'; + + +/** + * Gets the {@link Storage} service for the default app or a given app. + * + * `getStorage()` can be called with no arguments to access the default + * app's `Storage` service or as `getStorage(app)` to access the + * `Storage` service associated with a specific app. + * + * @example + * ```javascript + * // Get the Storage service for the default app + * const defaultStorage = getStorage(); + * ``` + * + * @example + * ```javascript + * // Get the Storage service for a given app + * const otherStorage = getStorage(otherApp); + * ``` + */ +export function getStorage(app?: App): Storage { + if (typeof app === 'undefined') { + app = getApp(); + } + + const firebaseApp: FirebaseApp = app as FirebaseApp; + return firebaseApp.getOrInitService('storage', (app) => new Storage(app)); +} + + + +/** + * Gets the download URL for the given {@link https://cloud.google.com/nodejs/docs/reference/storage/latest/storage/file | File}. + * + * @example + * ```javascript + * // Get the downloadUrl for a given file ref + * const storage = getStorage(); + * const myRef = ref(storage, 'images/mountains.jpg'); + * const downloadUrl = await getDownloadURL(myRef); + * ``` + */ +export async function getDownloadURL(file: File): Promise { + const endpoint = + (process.env.STORAGE_EMULATOR_HOST || + 'https://firebasestorage.googleapis.com') + '/v0'; + const { downloadTokens } = await getFirebaseMetadata(endpoint, file); + if (!downloadTokens) { + throw new FirebaseError({ + code: 'storage/no-download-token', + message: + 'No download token available. Please create one in the Firebase Console.', + }); + } + const [token] = downloadTokens.split(','); + return `${endpoint}/b/${file.bucket.name}/o/${encodeURIComponent( + file.name + )}?alt=media&token=${token}`; +} diff --git a/src/storage/storage-namespace.ts b/src/storage/storage-namespace.ts new file mode 100644 index 0000000000..f0dd80725d --- /dev/null +++ b/src/storage/storage-namespace.ts @@ -0,0 +1,48 @@ +/*! + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { App } from '../app'; +import { Storage as TStorage } from './storage'; + +/** + * Gets the {@link firebase-admin.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 `Storage` service or as `admin.storage(app)` to access the + * `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): storage.Storage; + +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace storage { + /** + * Type alias to {@link firebase-admin.storage#Storage}. + */ + export type Storage = TStorage; +} diff --git a/src/storage/storage.ts b/src/storage/storage.ts index db154ce106..6e155b379e 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"); @@ -14,43 +15,29 @@ * limitations under the License. */ -import {FirebaseApp} from '../firebase-app'; -import {FirebaseError} from '../utils/error'; -import {FirebaseServiceInterface, FirebaseServiceInternalsInterface} from '../firebase-service'; -import {ApplicationDefaultCredential, Certificate} from '../auth/credential'; -import {Bucket} from '@google-cloud/storage'; - +import { App } from '../app'; +import { FirebaseError } from '../utils/error'; +import { ServiceAccountCredential, isApplicationDefault } from '../app/credential-internal'; +import { Bucket, Storage as StorageClient } from '@google-cloud/storage'; +import * as utils from '../utils/index'; import * as validator from '../utils/validator'; /** - * Internals of a Storage instance. + * The default `Storage` service if no + * app is provided or the `Storage` service associated with the provided + * app. */ -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(); - } -} +export class Storage { -/** - * Storage service bound to the provided app. - */ -export class Storage implements FirebaseServiceInterface { - public INTERNAL: StorageInternals = new StorageInternals(); - - private appInternal: FirebaseApp; - private storageClient: any; + private readonly appInternal: App; + private readonly storageClient: StorageClient; /** - * @param {FirebaseApp} app The app for this Storage service. + * @param app - The app for this Storage service. * @constructor + * @internal */ - constructor(app: FirebaseApp) { + constructor(app: App) { if (!validator.isNonNullObject(app) || !('options' in app)) { throw new FirebaseError({ code: 'storage/invalid-argument', @@ -58,31 +45,46 @@ export class Storage implements FirebaseServiceInterface { }); } - let storage; + if (!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; try { - /* tslint:disable-next-line:no-var-requires */ - storage = require('@google-cloud/storage'); - } catch (e) { + storage = require('@google-cloud/storage').Storage; + } catch (err) { throw new FirebaseError({ code: 'storage/missing-dependencies', message: 'Failed to import the Cloud Storage client library for Node.js. ' - + 'Make sure to install the "@google-cloud/storage" npm package.', + + 'Make sure to install the "@google-cloud/storage" npm package. ' + + `Original error: ${err}`, }); } - const cert: Certificate = app.options.credential.getCertificate(); - if (cert != null) { - // cert is available when the SDK has been initialized with a service account JSON file, - // or by setting the GOOGLE_APPLICATION_CREDENTIALS envrionment variable. - this.storageClient = storage({ + const projectId: string | null = utils.getExplicitProjectId(app); + const credential = app.options.credential; + if (credential instanceof ServiceAccountCredential) { + this.storageClient = new storage({ + // When the SDK is initialized with ServiceAccountCredentials an explicit projectId is + // guaranteed to be available. + projectId: projectId!, credentials: { - private_key: cert.privateKey, - client_email: cert.clientEmail, + private_key: credential.privateKey, + client_email: credential.clientEmail, }, }); - } else if (app.options.credential instanceof ApplicationDefaultCredential) { + } else if (isApplicationDefault(app.options.credential)) { // Try to use the Google application default credentials. - this.storageClient = storage(); + this.storageClient = new storage(); } else { throw new FirebaseError({ code: 'storage/invalid-credential', @@ -95,12 +97,12 @@ 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. + * Gets a reference to a Cloud Storage bucket. * - * @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 {@link https://cloud.google.com/nodejs/docs/reference/storage/latest/Bucket | Bucket} + * instance as defined in the `@google-cloud/storage` package. */ public bucket(name?: string): Bucket { const bucketName = (typeof name !== 'undefined') @@ -115,13 +117,11 @@ export class Storage implements FirebaseServiceInterface { 'explicitly when calling the getBucket() method.', }); } - /** - * Returns the app associated with this Storage instance. - * - * @return {FirebaseApp} The app associated with this Storage instance. + * Optional app whose `Storage` service to + * return. If not provided, the default `Storage` service will be returned. */ - get app(): FirebaseApp { + get app(): App { return this.appInternal; } } diff --git a/src/storage/utils.ts b/src/storage/utils.ts new file mode 100644 index 0000000000..bb6711521b --- /dev/null +++ b/src/storage/utils.ts @@ -0,0 +1,43 @@ +import { File } from '@google-cloud/storage'; +export interface FirebaseMetadata { + name: string; + bucket: string; + generation: string; + metageneration: string; + contentType: string; + timeCreated: string; + updated: string; + storageClass: string; + size: string; + md5Hash: string; + contentEncoding: string; + contentDisposition: string; + crc32c: string; + etag: string; + downloadTokens?: string; +} + +export function getFirebaseMetadata( + endpoint: string, + file: File +): Promise { + const uri = `${endpoint}/b/${file.bucket.name}/o/${encodeURIComponent( + file.name + )}`; + + return new Promise((resolve, reject) => { + file.storage.makeAuthenticatedRequest( + { + method: 'GET', + uri, + }, + (err, body) => { + if (err) { + reject(err); + } else { + resolve(body); + } + } + ); + }); +} diff --git a/src/utils/api-request.ts b/src/utils/api-request.ts index 24f2e85691..2408e92344 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"); @@ -14,233 +15,829 @@ * limitations under the License. */ -import {deepCopy} from './deep-copy'; -import {FirebaseApp} from '../firebase-app'; -import {AppErrorCodes, FirebaseAppError} from './error'; +import { FirebaseApp } from '../app/firebase-app'; +import { AppErrorCodes, FirebaseAppError } from './error'; +import * as validator from './validator'; -import {OutgoingHttpHeaders} from 'http'; +import http = require('http'); import https = require('https'); +import url = require('url'); +import { EventEmitter } from 'events'; +import { Readable } from 'stream'; +import * as zlibmod from 'zlib'; /** Http method type definition. */ -export type HttpMethod = 'GET' | 'POST' | 'DELETE'; +export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD'; /** API callback function type definition. */ export type ApiCallbackFunction = (data: object) => void; /** - * Base class for handling HTTP requests. + * Configuration for constructing a new HTTP request. */ -export class HttpRequestHandler { +export interface HttpRequestConfig { + method: HttpMethod; + /** Target URL of the request. Should be a well-formed URL including protocol, hostname, port and path. */ + url: string; + headers?: {[key: string]: string}; + data?: string | object | Buffer | null; + /** Connect and read timeout (in milliseconds) for the outgoing request. */ + timeout?: number; + httpAgent?: http.Agent; +} + +/** + * Represents an HTTP response received from a remote server. + */ +export interface HttpResponse { + readonly status: number; + readonly headers: any; + /** Response data as a raw string. */ + readonly text?: string; + /** Response data as a parsed JSON object. */ + readonly data?: any; + /** For multipart responses, the payloads of individual parts. */ + readonly multipart?: Buffer[]; + /** + * Indicates if the response content is JSON-formatted or not. If true, data field can be used + * to retrieve the content as a parsed JSON object. + */ + isJson(): boolean; +} + +interface LowLevelResponse { + status: number; + headers: http.IncomingHttpHeaders; + request: http.ClientRequest | null; + data?: string; + multipart?: Buffer[]; + config: HttpRequestConfig; +} + +interface LowLevelError extends Error { + config: HttpRequestConfig; + code?: string; + request?: http.ClientRequest; + response?: LowLevelResponse; +} + +class DefaultHttpResponse implements HttpResponse { + + public readonly status: number; + public readonly headers: any; + public readonly text?: string; + + private readonly parsedData: any; + private readonly parseError: Error; + private readonly request: string; + /** - * Sends HTTP requests and returns a promise that resolves with the result. - * Will retry once if the first attempt encounters an AppErrorCodes.NETWORK_ERROR. + * Constructs a new HttpResponse from the given LowLevelResponse. + */ + constructor(resp: LowLevelResponse) { + this.status = resp.status; + this.headers = resp.headers; + this.text = resp.data; + try { + if (!resp.data) { + throw new FirebaseAppError(AppErrorCodes.INTERNAL_ERROR, 'HTTP response missing data.'); + } + this.parsedData = JSON.parse(resp.data); + } catch (err) { + this.parsedData = undefined; + this.parseError = err; + } + this.request = `${resp.config.method} ${resp.config.url}`; + } + + get data(): any { + if (this.isJson()) { + return this.parsedData; + } + throw new FirebaseAppError( + AppErrorCodes.UNABLE_TO_PARSE_RESPONSE, + `Error while parsing response data: "${ this.parseError.toString() }". Raw server ` + + `response: "${ this.text }". Status code: "${ this.status }". Outgoing ` + + `request: "${ this.request }."`, + ); + } + + public isJson(): boolean { + return typeof this.parsedData !== 'undefined'; + } +} + +/** + * Represents a multipart HTTP response. Parts that constitute the response body can be accessed + * via the multipart getter. Getters for text and data throw errors. + */ +class MultipartHttpResponse implements HttpResponse { + + public readonly status: number; + public readonly headers: any; + public readonly multipart?: Buffer[]; + + constructor(resp: LowLevelResponse) { + this.status = resp.status; + this.headers = resp.headers; + this.multipart = resp.multipart; + } + + get text(): string { + throw new FirebaseAppError( + AppErrorCodes.UNABLE_TO_PARSE_RESPONSE, + 'Unable to parse multipart payload as text', + ); + } + + get data(): any { + throw new FirebaseAppError( + AppErrorCodes.UNABLE_TO_PARSE_RESPONSE, + 'Unable to parse multipart payload as JSON', + ); + } + + public isJson(): boolean { + return false; + } +} + +export class HttpError extends Error { + constructor(public readonly response: HttpResponse) { + super(`Server responded with status ${response.status}.`); + // Set the prototype so that instanceof checks will work correctly. + // See: https://github.com/Microsoft/TypeScript/issues/13965 + Object.setPrototypeOf(this, HttpError.prototype); + } +} + +/** + * Specifies how failing HTTP requests should be retried. + */ +export interface RetryConfig { + /** Maximum number of times to retry a given request. */ + maxRetries: number; + + /** HTTP status codes that should be retried. */ + statusCodes?: number[]; + + /** Low-level I/O error codes that should be retried. */ + ioErrorCodes?: string[]; + + /** + * The multiplier for exponential back off. The retry delay is calculated in seconds using the formula + * `(2^n) * backOffFactor`, where n is the number of retries performed so far. When the backOffFactor is set + * to 0, retries are not delayed. When the backOffFactor is 1, retry duration is doubled each iteration. + */ + backOffFactor?: number; + + /** Maximum duration to wait before initiating a retry. */ + maxDelayInMillis: number; +} + +/** + * Default retry configuration for HTTP requests. Retries up to 4 times on connection reset and timeout errors + * as well as HTTP 503 errors. Exposed as a function to ensure that every HttpClient gets its own RetryConfig + * instance. + */ +export function defaultRetryConfig(): RetryConfig { + return { + maxRetries: 4, + statusCodes: [503], + ioErrorCodes: ['ECONNRESET', 'ETIMEDOUT'], + backOffFactor: 0.5, + maxDelayInMillis: 60 * 1000, + }; +} + +/** + * Ensures that the given RetryConfig object is valid. + * + * @param retry - The configuration to be validated. + */ +function validateRetryConfig(retry: RetryConfig): void { + if (!validator.isNumber(retry.maxRetries) || retry.maxRetries < 0) { + throw new FirebaseAppError( + AppErrorCodes.INVALID_ARGUMENT, 'maxRetries must be a non-negative integer'); + } + + if (typeof retry.backOffFactor !== 'undefined') { + if (!validator.isNumber(retry.backOffFactor) || retry.backOffFactor < 0) { + throw new FirebaseAppError( + AppErrorCodes.INVALID_ARGUMENT, 'backOffFactor must be a non-negative number'); + } + } + + if (!validator.isNumber(retry.maxDelayInMillis) || retry.maxDelayInMillis < 0) { + throw new FirebaseAppError( + AppErrorCodes.INVALID_ARGUMENT, 'maxDelayInMillis must be a non-negative integer'); + } + + if (typeof retry.statusCodes !== 'undefined' && !validator.isArray(retry.statusCodes)) { + throw new FirebaseAppError(AppErrorCodes.INVALID_ARGUMENT, 'statusCodes must be an array'); + } + + if (typeof retry.ioErrorCodes !== 'undefined' && !validator.isArray(retry.ioErrorCodes)) { + throw new FirebaseAppError(AppErrorCodes.INVALID_ARGUMENT, 'ioErrorCodes must be an array'); + } +} + +export class HttpClient { + + constructor(private readonly retry: RetryConfig | null = defaultRetryConfig()) { + if (this.retry) { + validateRetryConfig(this.retry); + } + } + + /** + * Sends an HTTP request to a remote server. If the server responds with a successful response (2xx), the returned + * promise resolves with an HttpResponse. If the server responds with an error (3xx, 4xx, 5xx), the promise rejects + * with an HttpError. In case of all other errors, the promise rejects with a FirebaseAppError. If a request fails + * due to a low-level network error, transparently retries the request once before rejecting the promise. + * + * If the request data is specified as an object, it will be serialized into a JSON string. The application/json + * content-type header will also be automatically set in this case. For all other payload types, the content-type + * header should be explicitly set by the caller. To send a JSON leaf value (e.g. "foo", 5), parse it into JSON, + * and pass as a string or a Buffer along with the appropriate content-type header. * - * @param {string} host The HTTP host. - * @param {number} port The port number. - * @param {string} path The endpoint path. - * @param {HttpMethod} httpMethod The http method. - * @param {object} [data] The request JSON. - * @param {object} [headers] The request headers. - * @param {number} [timeout] The request timeout in milliseconds. - * @return {Promise} A promise that resolves with the response. + * @param config - HTTP request to be sent. + * @returns A promise that resolves with the response details. */ - public sendRequest( - host: string, - port: number, - path: string, - httpMethod: HttpMethod, - data?: object, - headers?: object, - timeout?: number): Promise { - // Convenience for calling the real _sendRequest() method with the original params. - const sendOneRequest = () => { - return this._sendRequest(host, port, path, httpMethod, data, headers, timeout); - }; + public send(config: HttpRequestConfig): Promise { + return this.sendWithRetry(config); + } + + /** + * Sends an HTTP request. In the event of an error, retries the HTTP request according to the + * RetryConfig set on the HttpClient. + * + * @param config - HTTP request to be sent. + * @param retryAttempts - Number of retries performed up to now. + * @returns A promise that resolves with the response details. + */ + private sendWithRetry(config: HttpRequestConfig, retryAttempts = 0): Promise { + return AsyncHttpCall.invoke(config) + .then((resp) => { + return this.createHttpResponse(resp); + }) + .catch((err: LowLevelError) => { + const [delayMillis, canRetry] = this.getRetryDelayMillis(retryAttempts, err); + if (canRetry && this.retry && delayMillis <= this.retry.maxDelayInMillis) { + return this.waitForRetry(delayMillis).then(() => { + return this.sendWithRetry(config, retryAttempts + 1); + }); + } + + if (err.response) { + throw new HttpError(this.createHttpResponse(err.response)); + } - return sendOneRequest() - .catch ((response: { statusCode: number, error: string | object }) => { - // Retry if the request failed due to a network error. - if (response.error instanceof FirebaseAppError) { - if ((response.error as FirebaseAppError).hasCode(AppErrorCodes.NETWORK_ERROR)) { - return sendOneRequest(); - } + if (err.code === 'ETIMEDOUT') { + throw new FirebaseAppError( + AppErrorCodes.NETWORK_TIMEOUT, + `Error while making request: ${err.message}.`); } - return Promise.reject(response); + throw new FirebaseAppError( + AppErrorCodes.NETWORK_ERROR, + `Error while making request: ${err.message}. Error code: ${err.code}`); }); } + private createHttpResponse(resp: LowLevelResponse): HttpResponse { + if (resp.multipart) { + return new MultipartHttpResponse(resp); + } + return new DefaultHttpResponse(resp); + } + + private waitForRetry(delayMillis: number): Promise { + if (delayMillis > 0) { + return new Promise((resolve) => { + setTimeout(resolve, delayMillis); + }); + } + return Promise.resolve(); + } + /** - * Sends HTTP requests and returns a promise that resolves with the result. + * Checks if a failed request is eligible for a retry, and if so returns the duration to wait before initiating + * the retry. * - * @param {string} host The HTTP host. - * @param {number} port The port number. - * @param {string} path The endpoint path. - * @param {HttpMethod} httpMethod The http method. - * @param {object} [data] The request JSON. - * @param {object} [headers] The request headers. - * @param {number} [timeout] The request timeout in milliseconds. - * @return {Promise} A promise that resolves with the response. + * @param retryAttempts - Number of retries completed up to now. + * @param err - The last encountered error. + * @returns A 2-tuple where the 1st element is the duration to wait before another retry, and the + * 2nd element is a boolean indicating whether the request is eligible for a retry or not. */ - private _sendRequest( - host: string, - port: number, - path: string, - httpMethod: HttpMethod, - data?: object, - headers?: object, - timeout?: number): Promise { - let requestData; - if (data) { - try { - requestData = JSON.stringify(data); - } catch (e) { - return Promise.reject(e); + private getRetryDelayMillis(retryAttempts: number, err: LowLevelError): [number, boolean] { + if (!this.isRetryEligible(retryAttempts, err)) { + return [0, false]; + } + + const response = err.response; + if (response && response.headers['retry-after']) { + const delayMillis = this.parseRetryAfterIntoMillis(response.headers['retry-after']); + if (delayMillis > 0) { + return [delayMillis, true]; } } - const options: https.RequestOptions = { - method: httpMethod, - host, - port, - path, - headers: headers as OutgoingHttpHeaders, - }; - // Only https endpoints. - return new Promise((resolve, reject) => { - const req = https.request(options, (res) => { - const buffers: Buffer[] = []; - res.on('data', (buffer: Buffer) => buffers.push(buffer)); - res.on('end', () => { - const response = Buffer.concat(buffers).toString(); - - const statusCode = res.statusCode || 200; - - const responseHeaders = res.headers || {}; - const contentType = responseHeaders['content-type'] || 'application/json'; - - if (contentType.indexOf('text/html') !== -1 || contentType.indexOf('text/plain') !== -1) { - // Text response - if (statusCode >= 200 && statusCode < 300) { - resolve(response); - } else { - reject({ - statusCode, - error: response, - }); - } - } else { - // JSON response - try { - const json = JSON.parse(response); - - if (statusCode >= 200 && statusCode < 300) { - resolve(json); - } else { - reject({ - statusCode, - error: json, - }); - } - } catch (error) { - const parsingError = new FirebaseAppError( - AppErrorCodes.UNABLE_TO_PARSE_RESPONSE, - `Failed to parse response data: "${ error.toString() }". Raw server` + - `response: "${ response }". Status code: "${ res.statusCode }". Outgoing ` + - `request: "${ options.method } ${options.host}${ options.path }"`, - ); - reject({ - statusCode, - error: parsingError, - }); - } - } - }); + + return [this.backOffDelayMillis(retryAttempts), true]; + } + + private isRetryEligible(retryAttempts: number, err: LowLevelError): boolean { + if (!this.retry) { + return false; + } + + if (retryAttempts >= this.retry.maxRetries) { + return false; + } + + if (err.response) { + const statusCodes = this.retry.statusCodes || []; + return statusCodes.indexOf(err.response.status) !== -1; + } + + if (err.code) { + const retryCodes = this.retry.ioErrorCodes || []; + return retryCodes.indexOf(err.code) !== -1; + } + + return false; + } + + /** + * Parses the Retry-After HTTP header as a milliseconds value. Return value is negative if the Retry-After header + * contains an expired timestamp or otherwise malformed. + */ + private parseRetryAfterIntoMillis(retryAfter: string): number { + const delaySeconds: number = parseInt(retryAfter, 10); + if (!isNaN(delaySeconds)) { + return delaySeconds * 1000; + } + + const date = new Date(retryAfter); + if (!isNaN(date.getTime())) { + return date.getTime() - Date.now(); + } + return -1; + } + + private backOffDelayMillis(retryAttempts: number): number { + if (retryAttempts === 0) { + return 0; + } + + if (!this.retry) { + throw new FirebaseAppError(AppErrorCodes.INTERNAL_ERROR, 'Expected this.retry to exist.'); + } + + const backOffFactor = this.retry.backOffFactor || 0; + const delayInSeconds = (2 ** retryAttempts) * backOffFactor; + return Math.min(delayInSeconds * 1000, this.retry.maxDelayInMillis); + } +} + +/** + * Parses a full HTTP response message containing both a header and a body. + * + * @param response - The HTTP response to be parsed. + * @param config - The request configuration that resulted in the HTTP response. + * @returns An object containing the parsed HTTP status, headers and the body. + */ +export function parseHttpResponse( + response: string | Buffer, config: HttpRequestConfig): HttpResponse { + + const responseText: string = validator.isBuffer(response) ? + response.toString('utf-8') : response as string; + const endOfHeaderPos: number = responseText.indexOf('\r\n\r\n'); + const headerLines: string[] = responseText.substring(0, endOfHeaderPos).split('\r\n'); + + const statusLine: string = headerLines[0]; + const status: string = statusLine.trim().split(/\s/)[1]; + + const headers: {[key: string]: string} = {}; + headerLines.slice(1).forEach((line) => { + const colonPos = line.indexOf(':'); + const name = line.substring(0, colonPos).trim().toLowerCase(); + const value = line.substring(colonPos + 1).trim(); + headers[name] = value; + }); + + let data = responseText.substring(endOfHeaderPos + 4); + if (data.endsWith('\n')) { + data = data.slice(0, -1); + } + if (data.endsWith('\r')) { + data = data.slice(0, -1); + } + + const lowLevelResponse: LowLevelResponse = { + status: parseInt(status, 10), + headers, + data, + config, + request: null, + }; + if (!validator.isNumber(lowLevelResponse.status)) { + throw new FirebaseAppError(AppErrorCodes.INTERNAL_ERROR, 'Malformed HTTP status line.'); + } + return new DefaultHttpResponse(lowLevelResponse); +} + +/** + * A helper class for sending HTTP requests over the wire. This is a wrapper around the standard + * http and https packages of Node.js, providing content processing, timeouts and error handling. + * It also wraps the callback API of the Node.js standard library in a more flexible Promise API. + */ +class AsyncHttpCall { + + private readonly config: HttpRequestConfigImpl; + private readonly options: https.RequestOptions; + private readonly entity: Buffer | undefined; + private readonly promise: Promise; + + private resolve: (_: any) => void; + private reject: (_: any) => void; + + /** + * Sends an HTTP request based on the provided configuration. + */ + public static invoke(config: HttpRequestConfig): Promise { + return new AsyncHttpCall(config).promise; + } + + private constructor(config: HttpRequestConfig) { + try { + this.config = new HttpRequestConfigImpl(config); + this.options = this.config.buildRequestOptions(); + this.entity = this.config.buildEntity(this.options.headers!); + this.promise = new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + this.execute(); }); + } catch (err) { + this.promise = Promise.reject(this.enhanceError(err, null)); + } + } - if (timeout) { - // Listen to timeouts and throw a network error. - req.on('socket', (socket) => { - socket.setTimeout(timeout); - socket.on('timeout', () => { - req.abort(); - - const networkTimeoutError = new FirebaseAppError( - AppErrorCodes.NETWORK_TIMEOUT, - `${ host } network timeout. Please try again.`, - ); - reject({ - statusCode: 408, - error: networkTimeoutError, - }); - }); - }); + private execute(): void { + const transport: any = this.options.protocol === 'https:' ? https : http; + const req: http.ClientRequest = transport.request(this.options, (res: http.IncomingMessage) => { + this.handleResponse(res, req); + }); + + // Handle errors + req.on('error', (err) => { + if (req.aborted) { + return; } + this.enhanceAndReject(err, null, req); + }); - req.on('error', (error) => { - const networkRequestError = new FirebaseAppError( - AppErrorCodes.NETWORK_ERROR, - `A network request error has occurred: ${ error && error.message }`, - ); - reject({ - statusCode: 502, - error: networkRequestError, - }); + 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, timeoutCallback); + } + + // Send the request + req.end(this.entity); + } + + private handleResponse(res: http.IncomingMessage, req: http.ClientRequest): void { + if (req.aborted) { + return; + } + + if (!res.statusCode) { + throw new FirebaseAppError( + AppErrorCodes.INTERNAL_ERROR, + 'Expected a statusCode on the response from a ClientRequest'); + } + + const response: LowLevelResponse = { + status: res.statusCode, + headers: res.headers, + request: req, + data: undefined, + config: this.config, + }; + const boundary = this.getMultipartBoundary(res.headers); + const respStream: Readable = this.uncompressResponse(res); + + if (boundary) { + this.handleMultipartResponse(response, respStream, boundary); + } else { + this.handleRegularResponse(response, respStream); + } + } + + /** + * Extracts multipart boundary from the HTTP header. The content-type header of a multipart + * response has the form 'multipart/subtype; boundary=string'. + * + * If the content-type header does not exist, or does not start with + * 'multipart/', then null will be returned. + */ + private getMultipartBoundary(headers: http.IncomingHttpHeaders): string | null { + const contentType = headers['content-type']; + if (!contentType || !contentType.startsWith('multipart/')) { + return null; + } + + const segments: string[] = contentType.split(';'); + const emptyObject: {[key: string]: string} = {}; + const headerParams = segments.slice(1) + .map((segment) => segment.trim().split('=')) + .reduce((curr, params) => { + // Parse key=value pairs in the content-type header into properties of an object. + if (params.length === 2) { + const keyValuePair: {[key: string]: string} = {}; + keyValuePair[params[0]] = params[1]; + return Object.assign(curr, keyValuePair); + } + return curr; + }, emptyObject); + + return headerParams.boundary; + } + + private uncompressResponse(res: http.IncomingMessage): Readable { + // Uncompress the response body transparently if required. + let respStream: Readable = res; + const encodings = ['gzip', 'compress', 'deflate']; + if (res.headers['content-encoding'] && encodings.indexOf(res.headers['content-encoding']) !== -1) { + // Add the unzipper to the body stream processing pipeline. + const zlib: typeof zlibmod = require('zlib'); // eslint-disable-line @typescript-eslint/no-var-requires + respStream = respStream.pipe(zlib.createUnzip()); + // Remove the content-encoding in order to not confuse downstream operations. + delete res.headers['content-encoding']; + } + return respStream; + } + + private handleMultipartResponse( + response: LowLevelResponse, respStream: Readable, boundary: string): void { + + const busboy = require('@fastify/busboy'); // eslint-disable-line @typescript-eslint/no-var-requires + const multipartParser = new busboy.Dicer({ boundary }); + const responseBuffer: Buffer[] = []; + multipartParser.on('part', (part: any) => { + const tempBuffers: Buffer[] = []; + + part.on('data', (partData: Buffer) => { + tempBuffers.push(partData); }); - if (requestData) { - req.write(requestData); + part.on('end', () => { + responseBuffer.push(Buffer.concat(tempBuffers)); + }); + }); + + multipartParser.on('finish', () => { + response.data = undefined; + response.multipart = responseBuffer; + this.finalizeResponse(response); + }); + + respStream.pipe(multipartParser); + } + + private handleRegularResponse(response: LowLevelResponse, respStream: Readable): void { + const responseBuffer: Buffer[] = []; + respStream.on('data', (chunk: Buffer) => { + responseBuffer.push(chunk); + }); + + respStream.on('error', (err) => { + const req: http.ClientRequest | null = response.request; + if (req && req.aborted) { + return; } + this.enhanceAndReject(err, null, req); + }); - req.end(); + respStream.on('end', () => { + response.data = Buffer.concat(responseBuffer).toString(); + this.finalizeResponse(response); }); } + + /** + * Finalizes the current HTTP call in-flight by either resolving or rejecting the associated + * promise. In the event of an error, adds additional useful information to the returned error. + */ + private finalizeResponse(response: LowLevelResponse): void { + if (response.status >= 200 && response.status < 300) { + this.resolve(response); + } else { + this.rejectWithError( + 'Request failed with status code ' + response.status, + null, + response.request, + response, + ); + } + } + + /** + * Creates a new error from the given message, and enhances it with other information available. + * Then the promise associated with this HTTP call is rejected with the resulting error. + */ + private rejectWithError( + message: string, + code?: string | null, + request?: http.ClientRequest | null, + response?: LowLevelResponse): void { + + const error = new Error(message); + this.enhanceAndReject(error, code, request, response); + } + + private enhanceAndReject( + error: any, + code?: string | null, + request?: http.ClientRequest | null, + response?: LowLevelResponse): void { + + this.reject(this.enhanceError(error, code, request, response)); + } + + /** + * Enhances the given error by adding more information to it. Specifically, the HttpRequestConfig, + * the underlying request and response will be attached to the error. + */ + private enhanceError( + error: any, + code?: string | null, + request?: http.ClientRequest | null, + response?: LowLevelResponse): LowLevelError { + + error.config = this.config; + if (code) { + error.code = code; + } + error.request = request; + error.response = response; + return error; + } } /** - * Class that extends HttpRequestHandler and signs HTTP requests with a service - * credential access token. - * - * @param {Credential} credential The service account credential used to - * sign HTTP requests. - * @constructor + * An adapter class for extracting options and entity data from an HttpRequestConfig. */ -export class SignedApiRequestHandler extends HttpRequestHandler { - constructor(private app_: FirebaseApp) { +class HttpRequestConfigImpl implements HttpRequestConfig { + + constructor(private readonly config: HttpRequestConfig) { + + } + + get method(): HttpMethod { + return this.config.method; + } + + get url(): string { + return this.config.url; + } + + get headers(): {[key: string]: string} | undefined { + return this.config.headers; + } + + get data(): string | object | Buffer | undefined | null { + return this.config.data; + } + + get timeout(): number | undefined { + return this.config.timeout; + } + + get httpAgent(): http.Agent | undefined { + return this.config.httpAgent; + } + + public buildRequestOptions(): https.RequestOptions { + const parsed = this.buildUrl(); + const protocol = parsed.protocol; + let port: string | null = parsed.port; + if (!port) { + const isHttps = protocol === 'https:'; + port = isHttps ? '443' : '80'; + } + + return { + protocol, + hostname: parsed.hostname, + port, + path: parsed.path, + method: this.method, + agent: this.httpAgent, + headers: Object.assign({}, this.headers), + }; + } + + public buildEntity(headers: http.OutgoingHttpHeaders): Buffer | undefined { + let data: Buffer | undefined; + if (!this.hasEntity() || !this.isEntityEnclosingRequest()) { + return data; + } + + if (validator.isBuffer(this.data)) { + data = this.data as Buffer; + } else if (validator.isObject(this.data)) { + data = Buffer.from(JSON.stringify(this.data), 'utf-8'); + if (typeof headers['content-type'] === 'undefined') { + headers['content-type'] = 'application/json;charset=utf-8'; + } + } else if (validator.isString(this.data)) { + data = Buffer.from(this.data as string, 'utf-8'); + } else { + throw new Error('Request data must be a string, a Buffer or a json serializable object'); + } + + // Add Content-Length header if data exists. + headers['Content-Length'] = data.length.toString(); + return data; + } + + private buildUrl(): url.UrlWithStringQuery { + const fullUrl: string = this.urlWithProtocol(); + if (!this.hasEntity() || this.isEntityEnclosingRequest()) { + return url.parse(fullUrl); + } + + if (!validator.isObject(this.data)) { + throw new Error(`${this.method} requests cannot have a body`); + } + + // Parse URL and append data to query string. + const parsedUrl = new url.URL(fullUrl); + const dataObj = this.data as {[key: string]: string}; + for (const key in dataObj) { + if (Object.prototype.hasOwnProperty.call(dataObj, key)) { + parsedUrl.searchParams.append(key, dataObj[key]); + } + } + + return url.parse(parsedUrl.toString()); + } + + private urlWithProtocol(): string { + const fullUrl: string = this.url; + if (fullUrl.startsWith('http://') || fullUrl.startsWith('https://')) { + return fullUrl; + } + return `https://${fullUrl}`; + } + + private hasEntity(): boolean { + return !!this.data; + } + + private isEntityEnclosingRequest(): boolean { + // GET and HEAD requests do not support entity (body) in request. + return this.method !== 'GET' && this.method !== 'HEAD'; + } +} + +export class AuthorizedHttpClient extends HttpClient { + constructor(private readonly app: FirebaseApp) { super(); } - /** - * Sends HTTP requests and returns a promise that resolves with the result. - * - * @param {string} host The HTTP host. - * @param {number} port The port number. - * @param {string} path The endpoint path. - * @param {HttpMethod} httpMethod The http method. - * @param {object} data The request JSON. - * @param {object} headers The request headers. - * @param {number} timeout The request timeout in milliseconds. - * @return {Promise} A promise that resolves with the response. - */ - public sendRequest( - host: string, - port: number, - path: string, - httpMethod: HttpMethod, - data?: object, - headers?: object, - timeout?: number): Promise { - return this.app_.INTERNAL.getToken().then((accessTokenObj) => { - const headersCopy: object = (headers && deepCopy(headers)) || {}; - const authorizationHeaderKey = 'Authorization'; - headersCopy[authorizationHeaderKey] = 'Bearer ' + accessTokenObj.accessToken; - return super.sendRequest(host, port, path, httpMethod, data, headersCopy, timeout); + public send(request: HttpRequestConfig): Promise { + return this.getToken().then((token) => { + const requestCopy = Object.assign({}, request); + requestCopy.headers = Object.assign({}, request.headers); + const authHeader = 'Authorization'; + requestCopy.headers[authHeader] = `Bearer ${token}`; + + if (!requestCopy.httpAgent && this.app.options.httpAgent) { + requestCopy.httpAgent = this.app.options.httpAgent; + } + return super.send(requestCopy); }); } + + protected getToken(): Promise { + return this.app.INTERNAL.getToken() + .then((accessTokenObj) => { + return accessTokenObj.accessToken; + }); + } } /** * Class that defines all the settings for the backend API endpoint. * - * @param {string} endpoint The Firebase Auth backend endpoint. - * @param {HttpMethod} httpMethod The http method for that endpoint. + * @param endpoint - The Firebase Auth backend endpoint. + * @param httpMethod - The http method for that endpoint. * @constructor */ export class ApiSettings { @@ -249,46 +846,164 @@ export class ApiSettings { constructor(private endpoint: string, private httpMethod: HttpMethod = 'POST') { this.setRequestValidator(null) - .setResponseValidator(null); + .setResponseValidator(null); } - /** @return {string} The backend API endpoint. */ + /** @returns The backend API endpoint. */ public getEndpoint(): string { return this.endpoint; } - /** @return {HttpMethod} The request HTTP method. */ + /** @returns The request HTTP method. */ public getHttpMethod(): HttpMethod { return this.httpMethod; } /** - * @param {ApiCallbackFunction} requestValidator The request validator. - * @return {ApiSettings} The current API settings instance. + * @param requestValidator - The request validator. + * @returns The current API settings instance. */ - public setRequestValidator(requestValidator: ApiCallbackFunction): ApiSettings { - const nullFunction = (request: object) => undefined; + public setRequestValidator(requestValidator: ApiCallbackFunction | null): ApiSettings { + const nullFunction: ApiCallbackFunction = () => undefined; this.requestValidator = requestValidator || nullFunction; return this; } - /** @return {ApiCallbackFunction} The request validator. */ + /** @returns The request validator. */ public getRequestValidator(): ApiCallbackFunction { return this.requestValidator; } /** - * @param {ApiCallbackFunction} responseValidator The response validator. - * @return {ApiSettings} The current API settings instance. + * @param responseValidator - The response validator. + * @returns The current API settings instance. */ - public setResponseValidator(responseValidator: ApiCallbackFunction): ApiSettings { - const nullFunction = (request: object) => undefined; + public setResponseValidator(responseValidator: ApiCallbackFunction | null): ApiSettings { + const nullFunction: ApiCallbackFunction = () => undefined; this.responseValidator = responseValidator || nullFunction; return this; } - /** @return {ApiCallbackFunction} The response validator. */ + /** @returns The response validator. */ public getResponseValidator(): ApiCallbackFunction { return this.responseValidator; } } + +/** + * Class used for polling an endpoint with exponential backoff. + * + * Example usage: + * ``` + * const poller = new ExponentialBackoffPoller(); + * poller + * .poll(() => { + * return myRequestToPoll() + * .then((responseData: any) => { + * if (!isValid(responseData)) { + * // Continue polling. + * return null; + * } + * + * // Polling complete. Resolve promise with final response data. + * return responseData; + * }); + * }) + * .then((responseData: any) => { + * console.log(`Final response: ${responseData}`); + * }); + * ``` + */ +export class ExponentialBackoffPoller extends EventEmitter { + private numTries = 0; + private completed = false; + + private masterTimer: NodeJS.Timeout; + private repollTimer: NodeJS.Timeout; + + private pollCallback?: () => Promise; + private resolve: (result: T) => void; + private reject: (err: object) => void; + + constructor( + private readonly initialPollingDelayMillis: number = 1000, + private readonly maxPollingDelayMillis: number = 10000, + private readonly masterTimeoutMillis: number = 60000) { + super(); + } + + /** + * Poll the provided callback with exponential backoff. + * + * @param 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. + * @returns A Promise which resolves to the truthy value returned by the provided + * callback when polling is complete. + */ + public poll(callback: () => Promise): Promise { + if (this.pollCallback) { + throw new Error('poll() can only be called once per instance of ExponentialBackoffPoller'); + } + + this.pollCallback = callback; + this.on('poll', this.repoll); + + this.masterTimer = setTimeout(() => { + if (this.completed) { + return; + } + + this.markCompleted(); + this.reject(new Error('ExponentialBackoffPoller deadline exceeded - Master timeout reached')); + }, this.masterTimeoutMillis); + + return new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + this.repoll(); + }); + } + + private repoll(): void { + this.pollCallback!() + .then((result) => { + if (this.completed) { + return; + } + + if (!result) { + this.repollTimer = + setTimeout(() => this.emit('poll'), this.getPollingDelayMillis()); + this.numTries++; + return; + } + + this.markCompleted(); + this.resolve(result); + }) + .catch((err) => { + if (this.completed) { + return; + } + + this.markCompleted(); + this.reject(err); + }); + } + + private getPollingDelayMillis(): number { + const increasedPollingDelay = Math.pow(2, this.numTries) * this.initialPollingDelayMillis; + return Math.min(increasedPollingDelay, this.maxPollingDelayMillis); + } + + private markCompleted(): void { + this.completed = true; + if (this.masterTimer) { + clearTimeout(this.masterTimer); + } + if (this.repollTimer) { + clearTimeout(this.repollTimer); + } + } +} diff --git a/src/utils/crypto-signer.ts b/src/utils/crypto-signer.ts new file mode 100644 index 0000000000..ec33a3a714 --- /dev/null +++ b/src/utils/crypto-signer.ts @@ -0,0 +1,251 @@ +/*! + * @license + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { App } from '../app'; +import { FirebaseApp } from '../app/firebase-app'; +import { ServiceAccountCredential } from '../app/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 - The data to be signed. + * @returns 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. + * + * @returns 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 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 app - A FirebaseApp instance. + * @returns A CryptoSigner instance. + */ +export function cryptoSignerFromApp(app: App): CryptoSigner { + const credential = app.options.credential; + if (credential instanceof ServiceAccountCredential) { + return new ServiceAccountSigner(credential); + } + + return new IAMSigner(new AuthorizedHttpClient(app as FirebaseApp), 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 - 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; + } + + /** @returns The error code. */ + public get code(): string { + return this.errorInfo.code; + } + + /** @returns The error message. */ + public get message(): string { + return this.errorInfo.message; + } + + /** @returns 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/deep-copy.ts b/src/utils/deep-copy.ts index 8f2520e36f..1dde902c03 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"); @@ -17,8 +18,8 @@ /** * Returns a deep copy of an object or array. * - * @param {object|array} value The object or array to deep copy. - * @return {object|array} A deep copy of the provided object or array. + * @param value - The object or array to deep copy. + * @returns A deep copy of the provided object or array. */ export function deepCopy(value: T): T { return deepExtend(undefined, value); @@ -36,9 +37,9 @@ export function deepCopy(value: T): T { * Note that the target can be a function, in which case the properties in the source object are * copied onto it as static properties of the function. * - * @param {any} target The value which is being extended. - * @param {any} source The value whose properties are extending the target. - * @return {any} The target value. + * @param target - The value which is being extended. + * @param source - The value whose properties are extending the target. + * @returns The target value. */ export function deepExtend(target: any, source: any): any { if (!(source instanceof Object)) { @@ -46,12 +47,12 @@ export function deepExtend(target: any, source: any): any { } switch (source.constructor) { - case Date: + case Date: { // Treat Dates like scalars; if the target date object had any child // properties - they will be lost! const dateValue = (source as any) as Date; return new Date(dateValue.getTime()); - + } case Object: if (target === undefined) { target = {}; @@ -69,7 +70,7 @@ export function deepExtend(target: any, source: any): any { } for (const prop in source) { - if (!source.hasOwnProperty(prop)) { + if (!Object.prototype.hasOwnProperty.call(source, prop)) { continue; } target[prop] = deepExtend(target[prop], source[prop]); diff --git a/src/utils/error.ts b/src/utils/error.ts index 938f3c19d0..772d808a9a 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"); @@ -14,7 +15,8 @@ * limitations under the License. */ -import {deepCopy} from '../utils/deep-copy'; +import { FirebaseError as FirebaseErrorInterface } from '../app'; +import { deepCopy } from '../utils/deep-copy'; /** * Defines error info type. This includes a code and message string. @@ -24,11 +26,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). */ @@ -38,11 +35,13 @@ interface ServerToClientCode { /** * Firebase error code structure. This extends Error. - * - * @param {ErrorInfo} errorInfo The error information (code and message). - * @constructor */ -export class FirebaseError extends Error { +export class FirebaseError extends Error implements FirebaseErrorInterface { + /** + * @param errorInfo - The error information (code and message). + * @constructor + * @internal + */ constructor(private errorInfo: ErrorInfo) { super(errorInfo.message); @@ -53,17 +52,17 @@ export class FirebaseError extends Error { (this as any).__proto__ = FirebaseError.prototype; } - /** @return {string} The error code. */ + /** @returns The error code. */ public get code(): string { return this.errorInfo.code; } - /** @return {string} The error message. */ + /** @returns The error message. */ public get message(): string { return this.errorInfo.message; } - /** @return {object} The object representation of the error. */ + /** @returns The object representation of the error. */ public toJSON(): object { return { code: this.code, @@ -74,13 +73,15 @@ export class FirebaseError extends Error { /** * A FirebaseError with a prefix in front of the error code. - * - * @param {string} codePrefix The prefix to apply to the error code. - * @param {string} code The error code. - * @param {string} message The error message. - * @constructor */ -class PrefixedFirebaseError extends FirebaseError { +export class PrefixedFirebaseError extends FirebaseError { + /** + * @param codePrefix - The prefix to apply to the error code. + * @param code - The error code. + * @param message - The error message. + * @constructor + * @internal + */ constructor(private codePrefix: string, code: string, message: string) { super({ code: `${codePrefix}/${code}`, @@ -98,8 +99,8 @@ class PrefixedFirebaseError extends FirebaseError { * Allows the error type to be checked without needing to know implementation details * of the code prefixing. * - * @param {string} code The non-prefixed error code to test against. - * @return {boolean} True if the code matches, false otherwise. + * @param code - The non-prefixed error code to test against. + * @returns True if the code matches, false otherwise. */ public hasCode(code: string): boolean { return `${this.codePrefix}/${code}` === this.code; @@ -108,12 +109,14 @@ class PrefixedFirebaseError extends FirebaseError { /** * Firebase App error code structure. This extends PrefixedFirebaseError. - * - * @param {string} code The error code. - * @param {string} message The error message. - * @constructor */ export class FirebaseAppError extends PrefixedFirebaseError { + /** + * @param code - The error code. + * @param message - The error message. + * @constructor + * @internal + */ constructor(code: string, message: string) { super('app', code, message); @@ -127,35 +130,40 @@ export class FirebaseAppError extends PrefixedFirebaseError { /** * Firebase Auth error code structure. This extends PrefixedFirebaseError. - * - * @param {ErrorInfo} info The error code info. - * @param {string} [message] The error message. This will override the default - * message if provided. - * @constructor */ export class FirebaseAuthError extends PrefixedFirebaseError { /** * Creates the developer-facing error corresponding to the backend error code. * - * @param {string} serverErrorCode The server error code. - * @param {string} [message] The error message. The default message is used + * @param serverErrorCode - The server error code. + * @param [message] The error message. The default message is used * if not provided. - * @param {object} [rawServerResponse] The error's raw server response. - * @return {FirebaseAuthError} The corresponding developer-facing error. + * @param [rawServerResponse] The error's raw server response. + * @returns The corresponding developer-facing error. + * @internal */ public static fromServerError( serverErrorCode: string, message?: string, rawServerResponse?: object, ): FirebaseAuthError { + // serverErrorCode could contain additional details: + // ERROR_CODE : Detailed message which can also contain colons + const colonSeparator = (serverErrorCode || '').indexOf(':'); + let customMessage = null; + if (colonSeparator !== -1) { + customMessage = serverErrorCode.substring(colonSeparator + 1).trim(); + serverErrorCode = serverErrorCode.substring(0, colonSeparator).trim(); + } // If not found, default to internal error. const clientCodeKey = AUTH_SERVER_TO_CLIENT_CODE[serverErrorCode] || 'INTERNAL_ERROR'; - const error: ErrorInfo = deepCopy(AuthClientErrorCode[clientCodeKey]); - error.message = message || error.message; + const error: ErrorInfo = deepCopy((AuthClientErrorCode as any)[clientCodeKey]); + // Server detailed message should have highest priority. + error.message = customMessage || message || error.message; if (clientCodeKey === 'INTERNAL_ERROR' && typeof rawServerResponse !== 'undefined') { try { - error.message += ` Raw server response: "${ JSON.stringify(rawServerResponse) }"`; + error.message += ` Raw server response: "${JSON.stringify(rawServerResponse)}"`; } catch (e) { // Ignore JSON parsing error. } @@ -164,6 +172,12 @@ export class FirebaseAuthError extends PrefixedFirebaseError { return new FirebaseAuthError(error); } + /** + * @param info - The error code info. + * @param message - The error message. This will override the default message if provided. + * @constructor + * @internal + */ constructor(info: ErrorInfo, message?: string) { // Override default message if custom message provided. super('auth', info.code, message || info.message); @@ -178,80 +192,107 @@ export class FirebaseAuthError extends PrefixedFirebaseError { /** * Firebase Database error code structure. This extends FirebaseError. - * - * @param {ErrorInfo} info The error code info. - * @param {string} [message] The error message. This will override the default - * message if provided. - * @constructor */ export class FirebaseDatabaseError extends FirebaseError { + /** + * @param info - The error code info. + * @param message - The error message. This will override the default + * message if provided. + * @constructor + * @internal + */ 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 }); } } /** * Firebase Firestore error code structure. This extends FirebaseError. - * - * @param {ErrorInfo} info The error code info. - * @param {string} [message] The error message. This will override the default - * message if provided. - * @constructor */ export class FirebaseFirestoreError extends FirebaseError { + /** + * @param info - The error code info. + * @param message - The error message. This will override the default + * message if provided. + * @constructor + * @internal + */ 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 }); } } /** * Firebase instance ID error code structure. This extends FirebaseError. - * - * @param {ErrorInfo} info The error code info. - * @param {string} [message] The error message. This will override the default - * message if provided. - * @constructor */ export class FirebaseInstanceIdError extends FirebaseError { + /** + * + * @param info - The error code info. + * @param message - The error message. This will override the default + * message if provided. + * @constructor + * @internal + */ + 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`. + */ +export class FirebaseInstallationsError extends FirebaseError { + /** + * + * @param info - The error code info. + * @param message - The error message. This will override the default + * message if provided. + * @constructor + * @internal + */ constructor(info: ErrorInfo, message?: string) { // Override default message if custom message provided. - super({code: 'instance-id/' + info.code, message: message || info.message}); + super({ code: 'installations/' + info.code, message: message || info.message }); + (this as any).__proto__ = FirebaseInstallationsError.prototype; } } /** * Firebase Messaging error code structure. This extends PrefixedFirebaseError. - * - * @param {ErrorInfo} info The error code info. - * @param {string} [message] The error message. This will override the default message if provided. - * @constructor */ export class FirebaseMessagingError extends PrefixedFirebaseError { /** * Creates the developer-facing error corresponding to the backend error code. * - * @param {string} serverErrorCode The server error code. - * @param {string} [message] The error message. The default message is used + * @param serverErrorCode - The server error code. + * @param [message] The error message. The default message is used * if not provided. - * @param {object} [rawServerResponse] The error's raw server response. - * @return {FirebaseMessagingError} The corresponding developer-facing error. + * @param [rawServerResponse] The error's raw server response. + * @returns The corresponding developer-facing error. + * @internal */ public static fromServerError( - serverErrorCode: string, - message?: string, + serverErrorCode: string | null, + message?: string | null, rawServerResponse?: object, ): FirebaseMessagingError { // If not found, default to unknown error. - const clientCodeKey = MESSAGING_SERVER_TO_CLIENT_CODE[serverErrorCode] || 'UNKNOWN_ERROR'; - const error: ErrorInfo = deepCopy(MessagingClientErrorCode[clientCodeKey]); + let clientCodeKey = 'UNKNOWN_ERROR'; + if (serverErrorCode && serverErrorCode in MESSAGING_SERVER_TO_CLIENT_CODE) { + clientCodeKey = MESSAGING_SERVER_TO_CLIENT_CODE[serverErrorCode]; + } + const error: ErrorInfo = deepCopy((MessagingClientErrorCode as any)[clientCodeKey]); error.message = message || error.message; if (clientCodeKey === 'UNKNOWN_ERROR' && typeof rawServerResponse !== 'undefined') { try { - error.message += ` Raw server response: "${ JSON.stringify(rawServerResponse) }"`; + error.message += ` Raw server response: "${JSON.stringify(rawServerResponse)}"`; } catch (e) { // Ignore JSON parsing error. } @@ -260,6 +301,9 @@ export class FirebaseMessagingError extends PrefixedFirebaseError { return new FirebaseMessagingError(error); } + /** + * @internal + */ public static fromTopicManagementServerError( serverErrorCode: string, message?: string, @@ -267,12 +311,12 @@ export class FirebaseMessagingError extends PrefixedFirebaseError { ): FirebaseMessagingError { // If not found, default to unknown error. const clientCodeKey = TOPIC_MGT_SERVER_TO_CLIENT_CODE[serverErrorCode] || 'UNKNOWN_ERROR'; - const error: ErrorInfo = deepCopy(MessagingClientErrorCode[clientCodeKey]); + const error: ErrorInfo = deepCopy((MessagingClientErrorCode as any)[clientCodeKey]); error.message = message || error.message; if (clientCodeKey === 'UNKNOWN_ERROR' && typeof rawServerResponse !== 'undefined') { try { - error.message += ` Raw server response: "${ JSON.stringify(rawServerResponse) }"`; + error.message += ` Raw server response: "${JSON.stringify(rawServerResponse)}"`; } catch (e) { // Ignore JSON parsing error. } @@ -281,6 +325,13 @@ export class FirebaseMessagingError extends PrefixedFirebaseError { return new FirebaseMessagingError(error); } + /** + * + * @param info - The error code info. + * @param message - The error message. This will override the default message if provided. + * @constructor + * @internal + */ constructor(info: ErrorInfo, message?: string) { // Override default message if custom message provided. super('messaging', info.code, message || info.message); @@ -293,6 +344,26 @@ export class FirebaseMessagingError extends PrefixedFirebaseError { } } +/** + * Firebase project management error code structure. This extends PrefixedFirebaseError. + */ +export class FirebaseProjectManagementError extends PrefixedFirebaseError { + /** + * @param code - The error code. + * @param message - The error message. + * @constructor + * @internal + */ + constructor(code: ProjectManagementErrorCode, message: string) { + super('project-management', 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__ = FirebaseProjectManagementError.prototype; + } +} /** * App client error codes and their default messages. @@ -300,6 +371,7 @@ export class FirebaseMessagingError extends PrefixedFirebaseError { export class AppErrorCodes { public static APP_DELETED = 'app-deleted'; public static DUPLICATE_APP = 'duplicate-app'; + public static INVALID_ARGUMENT = 'invalid-argument'; public static INTERNAL_ERROR = 'internal-error'; public static INVALID_APP_NAME = 'invalid-app-name'; public static INVALID_APP_OPTIONS = 'invalid-app-options'; @@ -314,22 +386,54 @@ export class AppErrorCodes { * Auth client error codes and their default messages. */ export class AuthClientErrorCode { + public static AUTH_BLOCKING_TOKEN_EXPIRED = { + code: 'auth-blocking-token-expired', + message: 'The provided Firebase Auth Blocking token is expired.', + }; + public static BILLING_NOT_ENABLED = { + code: 'billing-not-enabled', + message: 'Feature requires billing to be enabled.', + }; public static CLAIMS_TOO_LARGE = { code: 'claims-too-large', message: 'Developer claims maximum payload size exceeded.', }; + public static CONFIGURATION_EXISTS = { + code: 'configuration-exists', + message: 'A configuration already exists with the provided identifier.', + }; + public static CONFIGURATION_NOT_FOUND = { + code: 'configuration-not-found', + message: 'There is no configuration corresponding to the provided identifier.', + }; + public static ID_TOKEN_EXPIRED = { + code: 'id-token-expired', + message: 'The provided Firebase ID token is expired.', + }; public static INVALID_ARGUMENT = { code: 'argument-error', message: 'Invalid argument provided.', }; + public static INVALID_CONFIG = { + code: 'invalid-config', + message: 'The provided configuration is invalid.', + }; public static EMAIL_ALREADY_EXISTS = { 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.', }; + public static INVALID_ID_TOKEN = { + code: 'invalid-id-token', + message: 'The provided ID token is not a valid Firebase ID token.', + }; public static ID_TOKEN_REVOKED = { code: 'id-token-revoked', message: 'The Firebase ID token has been revoked.', @@ -342,6 +446,14 @@ export class AuthClientErrorCode { code: 'invalid-claims', message: 'The provided custom claim attributes are invalid.', }; + public static INVALID_CONTINUE_URI = { + code: 'invalid-continue-uri', + message: 'The continue URL must be a valid URL string.', + }; + public static INVALID_CREATION_TIME = { + code: 'invalid-creation-time', + message: 'The creation time must be a valid UTC date string.', + }; public static INVALID_CREDENTIAL = { code: 'invalid-credential', message: 'Invalid credential object provided.', @@ -354,6 +466,11 @@ export class AuthClientErrorCode { code: 'invalid-display-name', message: 'The displayName field must be a valid string.', }; + public static INVALID_DYNAMIC_LINK_DOMAIN = { + code: 'invalid-dynamic-link-domain', + message: 'The provided dynamic link domain is not configured or authorized ' + + 'for the current project.', + }; public static INVALID_EMAIL_VERIFIED = { code: 'invalid-email-verified', message: 'The emailVerified field must be a boolean.', @@ -362,6 +479,63 @@ export class AuthClientErrorCode { code: 'invalid-email', message: 'The email address is improperly formatted.', }; + public static INVALID_NEW_EMAIL = { + code: 'invalid-new-email', + message: 'The new email address is improperly formatted.', + }; + public static INVALID_ENROLLED_FACTORS = { + code: 'invalid-enrolled-factors', + message: 'The enrolled factors must be a valid array of MultiFactorInfo objects.', + }; + public static INVALID_ENROLLMENT_TIME = { + code: 'invalid-enrollment-time', + message: 'The second factor enrollment time must be a valid UTC date string.', + }; + public static INVALID_HASH_ALGORITHM = { + code: 'invalid-hash-algorithm', + message: 'The hash algorithm must match one of the strings in the list of ' + + 'supported algorithms.', + }; + public static INVALID_HASH_BLOCK_SIZE = { + code: 'invalid-hash-block-size', + message: 'The hash block size must be a valid number.', + }; + public static INVALID_HASH_DERIVED_KEY_LENGTH = { + code: 'invalid-hash-derived-key-length', + message: 'The hash derived key length must be a valid number.', + }; + public static INVALID_HASH_KEY = { + code: 'invalid-hash-key', + message: 'The hash key must a valid byte buffer.', + }; + public static INVALID_HASH_MEMORY_COST = { + code: 'invalid-hash-memory-cost', + message: 'The hash memory cost must be a valid number.', + }; + public static INVALID_HASH_PARALLELIZATION = { + code: 'invalid-hash-parallelization', + message: 'The hash parallelization must be a valid number.', + }; + public static INVALID_HASH_ROUNDS = { + code: 'invalid-hash-rounds', + message: 'The hash rounds must be a valid number.', + }; + public static INVALID_HASH_SALT_SEPARATOR = { + code: 'invalid-hash-salt-separator', + message: 'The hashing algorithm salt separator field must be a valid byte buffer.', + }; + public static INVALID_LAST_SIGN_IN_TIME = { + code: 'invalid-last-sign-in-time', + message: 'The last sign-in time must be a valid UTC date string.', + }; + public static INVALID_NAME = { + code: 'invalid-name', + message: 'The resource name provided is invalid.', + }; + public static INVALID_OAUTH_CLIENT_ID = { + code: 'invalid-oauth-client-id', + message: 'The provided OAuth client ID is invalid.', + }; public static INVALID_PAGE_TOKEN = { code: 'invalid-page-token', message: 'The page token must be a valid non-empty string.', @@ -370,6 +544,14 @@ export class AuthClientErrorCode { code: 'invalid-password', message: 'The password must be a string with at least 6 characters.', }; + public static INVALID_PASSWORD_HASH = { + code: 'invalid-password-hash', + message: 'The password hash must be a valid byte buffer.', + }; + public static INVALID_PASSWORD_SALT = { + code: 'invalid-password-salt', + message: 'The password salt must be a valid byte buffer.', + }; public static INVALID_PHONE_NUMBER = { code: 'invalid-phone-number', message: 'The phone number must be a non-empty E.164 standard compliant identifier ' + @@ -379,14 +561,118 @@ export class AuthClientErrorCode { code: 'invalid-photo-url', message: 'The photoURL field must be a valid URL.', }; + public static INVALID_PROJECT_ID = { + code: 'invalid-project-id', + message: 'Invalid parent project. Either parent project doesn\'t exist or didn\'t enable multi-tenancy.', + }; + public static INVALID_PROVIDER_DATA = { + code: 'invalid-provider-data', + message: 'The providerData must be a valid array of UserInfo objects.', + }; + public static INVALID_PROVIDER_ID = { + code: 'invalid-provider-id', + message: 'The providerId must be a valid supported provider identifier string.', + }; + public static INVALID_PROVIDER_UID = { + 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 ' + + 'between 5 minutes and 2 weeks.', + }; + public static INVALID_TENANT_ID = { + code: 'invalid-tenant-id', + message: 'The tenant ID must be a valid non-empty string.', + }; + public static INVALID_TENANT_TYPE = { + 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.', }; + public static INVALID_USER_IMPORT = { + code: 'invalid-user-import', + message: 'The user record to import is invalid.', + }; public static INVALID_TOKENS_VALID_AFTER_TIME = { code: 'invalid-tokens-valid-after-time', message: 'The tokensValidAfterTime must be a valid UTC number in seconds.', }; + public static MISMATCHING_TENANT_ID = { + code: 'mismatching-tenant-id', + message: 'User tenant ID does not match with the current TenantAwareAuth tenant ID.', + }; + public static MISSING_ANDROID_PACKAGE_NAME = { + code: 'missing-android-pkg-name', + message: 'An Android Package Name must be provided if the Android App is ' + + 'required to be installed.', + }; + public static MISSING_CONFIG = { + code: 'missing-config', + message: 'The provided configuration is missing required attributes.', + }; + public static MISSING_CONTINUE_URI = { + code: 'missing-continue-uri', + message: 'A valid continue URL must be provided in the request.', + }; + public static MISSING_DISPLAY_NAME = { + code: 'missing-display-name', + message: 'The resource being created or edited is missing a valid display name.', + }; + public static MISSING_EMAIL = { + code: 'missing-email', + message: 'The email is required for the specified action. For example, a multi-factor user ' + + 'requires a verified email.', + }; + public static MISSING_IOS_BUNDLE_ID = { + code: 'missing-ios-bundle-id', + message: 'The request is missing an iOS Bundle ID.', + }; + public static MISSING_ISSUER = { + code: 'missing-issuer', + message: 'The OAuth/OIDC configuration issuer must not be empty.', + }; + public static MISSING_HASH_ALGORITHM = { + code: 'missing-hash-algorithm', + message: 'Importing users with password hashes requires that the hashing ' + + 'algorithm and its parameters be provided.', + }; + public static MISSING_OAUTH_CLIENT_ID = { + 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.', + }; + public static MISSING_SAML_RELYING_PARTY_CONFIG = { + 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.', + }; public static MISSING_UID = { code: 'missing-uid', message: 'A uid identifier is required for the current operation.', @@ -394,8 +680,8 @@ export class AuthClientErrorCode { public static OPERATION_NOT_ALLOWED = { code: 'operation-not-allowed', message: 'The given sign-in provider is disabled for this Firebase project. ' + - 'Enable it in the Firebase console, under the sign-in method tab of the ' + - 'Auth section.', + 'Enable it in the Firebase console, under the sign-in method tab of the ' + + 'Auth section.', }; public static PHONE_NUMBER_ALREADY_EXISTS = { code: 'phone-number-already-exists', @@ -412,14 +698,84 @@ export class AuthClientErrorCode { 'https://firebase.google.com/docs/admin/setup for details on how to authenticate this SDK ' + 'with appropriate permissions.', }; + public static QUOTA_EXCEEDED = { + code: 'quota-exceeded', + message: 'The project quota for the specified operation has been exceeded.', + }; + public static SECOND_FACTOR_LIMIT_EXCEEDED = { + code: 'second-factor-limit-exceeded', + message: 'The maximum number of allowed second factors on a user has been exceeded.', + }; + public static SECOND_FACTOR_UID_ALREADY_EXISTS = { + code: 'second-factor-uid-already-exists', + message: 'The specified second factor "uid" already exists.', + }; + public static SESSION_COOKIE_EXPIRED = { + code: 'session-cookie-expired', + message: 'The Firebase session cookie is expired.', + }; + public static SESSION_COOKIE_REVOKED = { + code: 'session-cookie-revoked', + message: 'The Firebase session cookie has been revoked.', + }; + public static TENANT_NOT_FOUND = { + code: 'tenant-not-found', + message: 'There is no tenant corresponding to the provided identifier.', + }; public static UID_ALREADY_EXISTS = { code: 'uid-already-exists', message: 'The user with the provided uid already exists.', }; + public static UNAUTHORIZED_DOMAIN = { + code: 'unauthorized-continue-uri', + message: 'The domain of the continue URL is not whitelisted. Whitelist the domain in the ' + + 'Firebase console.', + }; + public static UNSUPPORTED_FIRST_FACTOR = { + code: 'unsupported-first-factor', + message: 'A multi-factor user requires a supported first factor.', + }; + public static UNSUPPORTED_SECOND_FACTOR = { + code: 'unsupported-second-factor', + message: 'The request specified an unsupported type of second factor.', + }; + public static UNSUPPORTED_TENANT_OPERATION = { + code: 'unsupported-tenant-operation', + message: 'This operation is not supported in a multi-tenant context.', + }; + public static UNVERIFIED_EMAIL = { + code: 'unverified-email', + message: 'A verified email is required for the specified action. For example, a multi-factor user ' + + 'requires a verified email.', + }; public static USER_NOT_FOUND = { code: 'user-not-found', message: 'There is no user record corresponding to the provided identifier.', }; + public static NOT_FOUND = { + code: 'not-found', + message: 'The requested resource was not found.', + }; + public static USER_DISABLED = { + code: 'user-disabled', + message: 'The user record is disabled.', + } + public static USER_NOT_DISABLED = { + code: 'user-not-disabled', + message: 'The user must be disabled in order to bulk delete it (or you must pass force=true).', + }; + public static INVALID_RECAPTCHA_ACTION = { + code: 'invalid-recaptcha-action', + message: 'reCAPTCHA action must be "BLOCK".' + } + public static INVALID_RECAPTCHA_ENFORCEMENT_STATE = { + code: 'invalid-recaptcha-enforcement-state', + message: 'reCAPTCHA enforcement state must be either "OFF", "AUDIT" or "ENFORCE".' + } + public static RECAPTCHA_NOT_ENABLED = { + code: 'racaptcha-not-enabled', + message: 'reCAPTCHA enterprise is not enabled.' + } } /** @@ -488,8 +844,8 @@ export class MessagingClientErrorCode { code: 'message-rate-exceeded', message: 'Sending limit exceeded for the message target.', }; - public static INVALID_APNS_CREDENTIALS = { - code: 'invalid-apns-credentials', + public static THIRD_PARTY_AUTH_ERROR = { + code: 'third-party-auth-error', message: 'A message targeted to an iOS device could not be sent because the required APNs ' + 'SSL certificate was not uploaded or has expired. Check the validity of your development ' + 'and production certificates.', @@ -520,7 +876,7 @@ export class MessagingClientErrorCode { }; } -export class InstanceIdClientErrorCode { +export class InstallationsClientErrorCode { public static INVALID_ARGUMENT = { code: 'invalid-argument', message: 'Invalid argument provided.', @@ -529,56 +885,162 @@ 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.', + }; +} + +export type ProjectManagementErrorCode = + 'already-exists' + | 'authentication-error' + | 'internal-error' + | 'invalid-argument' + | 'invalid-project-id' + | 'invalid-server-response' + | 'not-found' + | 'service-unavailable' + | 'unknown-error'; + /** @const {ServerToClientCode} Auth server to client enum error codes. */ const AUTH_SERVER_TO_CLIENT_CODE: ServerToClientCode = { + // Feature being configured or used requires a billing account. + BILLING_NOT_ENABLED: 'BILLING_NOT_ENABLED', // Claims payload is too large. CLAIMS_TOO_LARGE: 'CLAIMS_TOO_LARGE', - // Project not found. - CONFIGURATION_NOT_FOUND: 'PROJECT_NOT_FOUND', + // Configuration being added already exists. + CONFIGURATION_EXISTS: 'CONFIGURATION_EXISTS', + // Configuration not found. + CONFIGURATION_NOT_FOUND: 'CONFIGURATION_NOT_FOUND', // Provided credential has insufficient permissions. INSUFFICIENT_PERMISSION: 'INSUFFICIENT_PERMISSION', + // Provided configuration has invalid fields. + INVALID_CONFIG: 'INVALID_CONFIG', + // Provided configuration identifier is invalid. + INVALID_CONFIG_ID: 'INVALID_PROVIDER_ID', + // ActionCodeSettings missing continue URL. + INVALID_CONTINUE_URI: 'INVALID_CONTINUE_URI', + // Dynamic link domain in provided ActionCodeSettings is not authorized. + INVALID_DYNAMIC_LINK_DOMAIN: 'INVALID_DYNAMIC_LINK_DOMAIN', // uploadAccount provides an email that already exists. - DUPLICATE_EMAIL: 'EMAIL_EXISTS', + DUPLICATE_EMAIL: 'EMAIL_ALREADY_EXISTS', // uploadAccount provides a localId that already exists. DUPLICATE_LOCAL_ID: 'UID_ALREADY_EXISTS', + // Request specified a multi-factor enrollment ID that already exists. + 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. INVALID_CLAIMS: 'INVALID_CLAIMS', + // Invalid session cookie duration. + INVALID_DURATION: 'INVALID_SESSION_COOKIE_DURATION', // Invalid email provided. INVALID_EMAIL: 'INVALID_EMAIL', + // Invalid new email provided. + INVALID_NEW_EMAIL: 'INVALID_NEW_EMAIL', + // Invalid tenant display name. This can be thrown on CreateTenant and UpdateTenant. + INVALID_DISPLAY_NAME: 'INVALID_DISPLAY_NAME', + // Invalid ID token provided. + INVALID_ID_TOKEN: 'INVALID_ID_TOKEN', + // Invalid tenant/parent resource name. + INVALID_NAME: 'INVALID_NAME', + // OIDC configuration has an invalid OAuth client ID. + INVALID_OAUTH_CLIENT_ID: 'INVALID_OAUTH_CLIENT_ID', // Invalid page token. INVALID_PAGE_SELECTION: 'INVALID_PAGE_TOKEN', // Invalid phone number. INVALID_PHONE_NUMBER: 'INVALID_PHONE_NUMBER', + // Invalid agent project. Either agent project doesn't exist or didn't enable multi-tenancy. + INVALID_PROJECT_ID: 'INVALID_PROJECT_ID', + // Invalid provider ID. + 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. + MISSING_ANDROID_PACKAGE_NAME: 'MISSING_ANDROID_PACKAGE_NAME', + // Missing configuration. + MISSING_CONFIG: 'MISSING_CONFIG', + // Missing configuration identifier. + MISSING_CONFIG_ID: 'MISSING_PROVIDER_ID', + // Missing tenant display name: This can be thrown on CreateTenant and UpdateTenant. + MISSING_DISPLAY_NAME: 'MISSING_DISPLAY_NAME', + // Email is required for the specified action. For example a multi-factor user requires + // a verified email. + MISSING_EMAIL: 'MISSING_EMAIL', + // Missing iOS bundle ID. + MISSING_IOS_BUNDLE_ID: 'MISSING_IOS_BUNDLE_ID', + // Missing OIDC issuer. + MISSING_ISSUER: 'MISSING_ISSUER', // No localId provided (deleteAccount missing localId). MISSING_LOCAL_ID: 'MISSING_UID', + // OIDC configuration is missing an OAuth client ID. + MISSING_OAUTH_CLIENT_ID: 'MISSING_OAUTH_CLIENT_ID', + // Missing provider ID. + MISSING_PROVIDER_ID: 'MISSING_PROVIDER_ID', + // Missing SAML RP config. + MISSING_SAML_RELYING_PARTY_CONFIG: 'MISSING_SAML_RELYING_PARTY_CONFIG', // Empty user list in uploadAccount. MISSING_USER_ACCOUNT: 'MISSING_UID', // Password auth disabled in console. OPERATION_NOT_ALLOWED: 'OPERATION_NOT_ALLOWED', + // Provided credential has insufficient permissions. + PERMISSION_DENIED: 'INSUFFICIENT_PERMISSION', // Phone number already exists. PHONE_NUMBER_EXISTS: 'PHONE_NUMBER_ALREADY_EXISTS', // Project not found. PROJECT_NOT_FOUND: 'PROJECT_NOT_FOUND', + // In multi-tenancy context: project creation quota exceeded. + QUOTA_EXCEEDED: 'QUOTA_EXCEEDED', + // Currently only 5 second factors can be set on the same user. + SECOND_FACTOR_LIMIT_EXCEEDED: 'SECOND_FACTOR_LIMIT_EXCEEDED', + // Tenant not found. + TENANT_NOT_FOUND: 'TENANT_NOT_FOUND', + // Tenant ID mismatch. + TENANT_ID_MISMATCH: 'MISMATCHING_TENANT_ID', + // Token expired error. + TOKEN_EXPIRED: 'ID_TOKEN_EXPIRED', + // Continue URL provided in ActionCodeSettings has a domain that is not whitelisted. + UNAUTHORIZED_DOMAIN: 'UNAUTHORIZED_DOMAIN', + // A multi-factor user requires a supported first factor. + UNSUPPORTED_FIRST_FACTOR: 'UNSUPPORTED_FIRST_FACTOR', + // The request specified an unsupported type of second factor. + UNSUPPORTED_SECOND_FACTOR: 'UNSUPPORTED_SECOND_FACTOR', + // Operation is not supported in a multi-tenant context. + UNSUPPORTED_TENANT_OPERATION: 'UNSUPPORTED_TENANT_OPERATION', + // A verified email is required for the specified action. For example a multi-factor user + // requires a verified email. + UNVERIFIED_EMAIL: 'UNVERIFIED_EMAIL', // User on which action is to be performed is not found. USER_NOT_FOUND: 'USER_NOT_FOUND', + // User record is disabled. + USER_DISABLED: 'USER_DISABLED', // Password provided is too weak. WEAK_PASSWORD: 'INVALID_PASSWORD', + // Unrecognized reCAPTCHA action. + INVALID_RECAPTCHA_ACTION: 'INVALID_RECAPTCHA_ACTION', + // Unrecognized reCAPTCHA enforcement state. + INVALID_RECAPTCHA_ENFORCEMENT_STATE: 'INVALID_RECAPTCHA_ENFORCEMENT_STATE', + // reCAPTCHA is not enabled for account defender. + RECAPTCHA_NOT_ENABLED: 'RECAPTCHA_NOT_ENABLED' }; /** @const {ServerToClientCode} Messaging server to client enum error codes. */ @@ -611,20 +1073,21 @@ const MESSAGING_SERVER_TO_CLIENT_CODE: ServerToClientCode = { // Topics message rate exceeded. TopicsMessageRateExceeded: 'TOPICS_MESSAGE_RATE_EXCEEDED', // Invalid APNs credentials. - InvalidApnsCredential: 'INVALID_APNS_CREDENTIALS', + InvalidApnsCredential: 'THIRD_PARTY_AUTH_ERROR', /* FCM v1 canonical error codes */ NOT_FOUND: 'REGISTRATION_TOKEN_NOT_REGISTERED', PERMISSION_DENIED: 'MISMATCHED_CREDENTIAL', RESOURCE_EXHAUSTED: 'MESSAGE_RATE_EXCEEDED', - UNAUTHENTICATED: 'INVALID_APNS_CREDENTIALS', + UNAUTHENTICATED: 'THIRD_PARTY_AUTH_ERROR', /* FCM v1 new error codes */ - APNS_AUTH_ERROR: 'INVALID_APNS_CREDENTIALS', + APNS_AUTH_ERROR: 'THIRD_PARTY_AUTH_ERROR', INTERNAL: 'INTERNAL_ERROR', INVALID_ARGUMENT: 'INVALID_ARGUMENT', QUOTA_EXCEEDED: 'MESSAGE_RATE_EXCEEDED', SENDER_ID_MISMATCH: 'MISMATCHED_CREDENTIAL', + THIRD_PARTY_AUTH_ERROR: 'THIRD_PARTY_AUTH_ERROR', UNAVAILABLE: 'SERVER_UNAVAILABLE', UNREGISTERED: 'REGISTRATION_TOKEN_NOT_REGISTERED', UNSPECIFIED_ERROR: 'UNKNOWN_ERROR', diff --git a/src/utils/index.ts b/src/utils/index.ts index c7cf632ea1..824d41c0f9 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"); @@ -14,20 +15,32 @@ * limitations under the License. */ -import {FirebaseApp, FirebaseAppOptions} from '../firebase-app'; -import {Certificate} from '../auth/credential'; - +import { App } from '../app/index'; +import { + ServiceAccountCredential, ComputeEngineCredential +} from '../app/credential-internal'; import * as validator from './validator'; +let sdkVersion: string; + +// TODO: Move to firebase-admin/app as an internal member. +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. * * For example, this can be used to map underscore_cased properties to camelCase. * - * @param {object} obj The object whose properties to rename. - * @param {object} keyMap The mapping from old to new property names. + * @param obj - The object whose properties to rename. + * @param keyMap - The mapping from old to new property names. */ -export function renameProperties(obj: object, keyMap: { [key: string]: string }): void { +export function renameProperties(obj: {[key: string]: any}, keyMap: { [key: string]: string }): void { Object.keys(keyMap).forEach((oldKey) => { if (oldKey in obj) { const newKey = keyMap[oldKey]; @@ -41,9 +54,9 @@ export function renameProperties(obj: object, keyMap: { [key: string]: string }) /** * Defines a new read-only property directly on an object and returns the object. * - * @param {object} obj The object on which to define the property. - * @param {string} prop The name of the property to be defined or modified. - * @param {any} value The value associated with the property. + * @param obj - The object on which to define the property. + * @param prop - The name of the property to be defined or modified. + * @param value - The value associated with the property. */ export function addReadonlyGetter(obj: object, prop: string, value: any): void { Object.defineProperty(obj, prop, { @@ -56,27 +69,235 @@ export function addReadonlyGetter(obj: object, prop: string, value: any): void { } /** - * Determines the Google Cloud project ID associated with a Firebase app by examining - * the Firebase app options, credentials and the local environment in that order. + * Returns the Google Cloud project ID associated with a Firebase app, if it's explicitly + * specified in either the Firebase app options, credentials or the local environment. + * Otherwise returns null. * - * @param {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. + * @returns A project ID string or null. */ -export function getProjectId(app: FirebaseApp): string { - const options: FirebaseAppOptions = app.options; +export function getExplicitProjectId(app: App): string | null { + const options = app.options; if (validator.isNonEmptyString(options.projectId)) { return options.projectId; } - const cert: Certificate = options.credential.getCertificate(); - if (cert != null && validator.isNonEmptyString(cert.projectId)) { - return cert.projectId; + const credential = app.options.credential; + if (credential instanceof ServiceAccountCredential) { + return credential.projectId; } - const projectId = process.env.GCLOUD_PROJECT; + const projectId = process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT; if (validator.isNonEmptyString(projectId)) { return projectId; } return null; } + +/** + * Determines the Google Cloud project ID associated with a Firebase app. This method + * first checks if a project ID is explicitly specified in either the Firebase app options, + * credentials or the local environment in that order. If no explicit project ID is + * configured, but the SDK has been initialized with ComputeEngineCredentials, this + * method attempts to discover the project ID from the local metadata service. + * + * @param app - A Firebase app to get the project ID from. + * + * @returns A project ID string or null. + */ +export function findProjectId(app: App): Promise { + const projectId = getExplicitProjectId(app); + if (projectId) { + return Promise.resolve(projectId); + } + + const credential = app.options.credential; + if (credential instanceof ComputeEngineCredential) { + return credential.getProjectId(); + } + + return Promise.resolve(null); +} + +/** + * Returns the service account email associated with a Firebase app, if it's explicitly + * specified in either the Firebase app options, credentials or the local environment. + * Otherwise returns null. + * + * @param app - A Firebase app to get the service account email from. + * + * @returns A service account email string or null. + */ +export function getExplicitServiceAccountEmail(app: App): string | null { + const options = app.options; + if (validator.isNonEmptyString(options.serviceAccountId)) { + return options.serviceAccountId; + } + + const credential = app.options.credential; + if (credential instanceof ServiceAccountCredential) { + return credential.clientEmail; + } + return null; +} + +/** + * Determines the service account email associated with a Firebase app. This method first + * checks if a service account email is explicitly specified in either the Firebase app options, + * credentials or the local environment in that order. If no explicit service account email is + * configured, but the SDK has been initialized with ComputeEngineCredentials, this + * method attempts to discover the service account email from the local metadata service. + * + * @param app - A Firebase app to get the service account email from. + * + * @returns A service account email ID string or null. + */ +export function findServiceAccountEmail(app: App): Promise { + const accountId = getExplicitServiceAccountEmail(app); + if (accountId) { + return Promise.resolve(accountId); + } + + const credential = app.options.credential; + if (credential instanceof ComputeEngineCredential) { + return credential.getServiceAccountEmail(); + } + + return Promise.resolve(null); +} + +/** + * Encodes data using web-safe-base64. + * + * @param data - The raw data byte input. + * @returns The base64-encoded result. + */ +export function toWebSafeBase64(data: Buffer): string { + return data.toString('base64').replace(/\//g, '_').replace(/\+/g, '-'); +} + +/** + * Formats a string of form 'project/{projectId}/{api}' and replaces + * with corresponding arguments {projectId: '1234', api: 'resource'} + * and returns output: 'project/1234/resource'. + * + * @param str - The original string where the param need to be + * replaced. + * @param params - The optional parameters to replace in the + * string. + * @returns The resulting formatted string. + */ +export function formatString(str: string, params?: object): string { + let formatted = str; + Object.keys(params || {}).forEach((key) => { + formatted = formatted.replace( + new RegExp('{' + key + '}', 'g'), + (params as {[key: string]: string})[key]); + }); + return formatted; +} + +/** + * Generates the update mask for the provided object. + * Note this will ignore the last key with value undefined. + * + * @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. + * @returns The computed update mask list. + */ +export function generateUpdateMask( + obj: any, terminalPaths: string[] = [], root = '' +): string[] { + const updateMask: string[] = []; + if (!validator.isNonNullObject(obj)) { + return updateMask; + } + for (const key in obj) { + 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); + } + } + } + } + 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; +} + +/** + * Internal type to represent a resource name + */ +export type ParsedResource = { + projectId?: string; + locationId?: string; + resourceId: string; +} + +/** + * Parses the top level resources of a given resource name. + * Supports both full and partial resources names, example: + * `locations/{location}/functions/{functionName}`, + * `projects/{project}/locations/{location}/functions/{functionName}`, or {functionName} + * Does not support deeply nested resource names. + * + * @param resourceName - The resource name string. + * @param resourceIdKey - The key of the resource name to be parsed. + * @returns A parsed resource name object. + */ +export function parseResourceName(resourceName: string, resourceIdKey: string): ParsedResource { + if (!resourceName.includes('/')) { + return { resourceId: resourceName }; + } + const CHANNEL_NAME_REGEX = + new RegExp(`^(projects/([^/]+)/)?locations/([^/]+)/${resourceIdKey}/([^/]+)$`); + const match = CHANNEL_NAME_REGEX.exec(resourceName); + if (match === null) { + throw new Error('Invalid resource name format.'); + } + const projectId = match[2]; + const locationId = match[3]; + const resourceId = match[4]; + + return { projectId, locationId, resourceId }; +} diff --git a/src/utils/jwt.ts b/src/utils/jwt.ts new file mode 100644 index 0000000000..acd1038d2f --- /dev/null +++ b/src/utils/jwt.ts @@ -0,0 +1,370 @@ +/*! + * 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 * as jwks from 'jwks-rsa'; +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'; +const NO_KID_IN_HEADER_ERROR_MESSAGE = 'no-kid-in-header-error'; + +const HOUR_IN_SECONDS = 3600; + +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 }>; +} + +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 cached for 6 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() + (HOUR_IN_SECONDS * 6 * 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. + */ +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. + * + * @returns 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 verifying 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 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] }) + .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.'); + } + }); + } +} + +/** + * Class for verifying 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, undefined as any, { algorithms:['none'] }); + } +} + +/** + * 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) => { + 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)) { + 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 verified. + * @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.'; + 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)); + } + } + 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', + NO_KID_IN_HEADER = 'no-kid-error', + KEY_FETCH_ERROR = 'key-fetch-error', +} diff --git a/src/utils/validator.ts b/src/utils/validator.ts index c508df23fe..d63d14772c 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"); @@ -16,23 +17,33 @@ import url = require('url'); +/** + * Validates that a value is a byte buffer. + * + * @param value - The value to validate. + * @returns Whether the value is byte buffer or not. + */ +export function isBuffer(value: any): value is Buffer { + return value instanceof Buffer; +} + /** * Validates that a value is an array. * - * @param {any} value The value to validate. - * @return {boolean} Whether the value is an array or not. + * @param value - The value to validate. + * @returns Whether the value is an array or not. */ -export function isArray(value: any): boolean { - return value instanceof Array; +export function isArray(value: any): value is T[] { + return Array.isArray(value); } /** * Validates that a value is a non-empty array. * - * @param {any} value The value to validate. - * @return {boolean} Whether the value is a non-empty array or not. + * @param value - The value to validate. + * @returns Whether the value is a non-empty array or not. */ -export function isNonEmptyArray(value: any): boolean { +export function isNonEmptyArray(value: any): value is T[] { return isArray(value) && value.length !== 0; } @@ -40,8 +51,8 @@ export function isNonEmptyArray(value: any): boolean { /** * Validates that a value is a boolean. * - * @param {any} value The value to validate. - * @return {boolean} Whether the value is a boolean or not. + * @param value - The value to validate. + * @returns Whether the value is a boolean or not. */ export function isBoolean(value: any): boolean { return typeof value === 'boolean'; @@ -51,8 +62,8 @@ export function isBoolean(value: any): boolean { /** * Validates that a value is a number. * - * @param {any} value The value to validate. - * @return {boolean} Whether the value is a number or not. + * @param value - The value to validate. + * @returns Whether the value is a number or not. */ export function isNumber(value: any): boolean { return typeof value === 'number' && !isNaN(value); @@ -62,21 +73,35 @@ export function isNumber(value: any): boolean { /** * Validates that a value is a string. * - * @param {any} value The value to validate. - * @return {boolean} Whether the value is a string or not. + * @param value - The value to validate. + * @returns Whether the value is a string or not. */ -export function isString(value: any): boolean { +export function isString(value: any): value is string { return typeof value === 'string'; } +/** + * Validates that a value is a base64 string. + * + * @param value - The value to validate. + * @returns Whether the value is a base64 string or not. + */ +export function isBase64String(value: any): boolean { + if (!isString(value)) { + return false; + } + return /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value); +} + + /** * Validates that a value is a non-empty string. * - * @param {any} value The value to validate. - * @return {boolean} Whether the value is a non-empty string or not. + * @param value - The value to validate. + * @returns Whether the value is a non-empty string or not. */ -export function isNonEmptyString(value: any): boolean { +export function isNonEmptyString(value: any): value is string { return isString(value) && value !== ''; } @@ -84,21 +109,21 @@ export function isNonEmptyString(value: any): boolean { /** * Validates that a value is a nullable object. * - * @param {any} value The value to validate. - * @return {boolean} Whether the value is an object or not. + * @param value - The value to validate. + * @returns Whether the value is an object or not. */ export function isObject(value: any): boolean { - return typeof value === 'object' && !(value instanceof Array); + return typeof value === 'object' && !isArray(value); } /** * Validates that a value is a non-null object. * - * @param {any} value The value to validate. - * @return {boolean} Whether the value is a non-null object or not. + * @param value - The value to validate. + * @returns Whether the value is a non-null object or not. */ -export function isNonNullObject(value: any): boolean { +export function isNonNullObject(value: T | null | undefined): value is T { return isObject(value) && value !== null; } @@ -106,8 +131,8 @@ export function isNonNullObject(value: any): boolean { /** * Validates that a string is a valid Firebase Auth uid. * - * @param {any} uid The string to validate. - * @return {boolean} Whether the string is a valid Firebase Auth uid. + * @param uid - The string to validate. + * @returns Whether the string is a valid Firebase Auth uid. */ export function isUid(uid: any): boolean { return typeof uid === 'string' && uid.length > 0 && uid.length <= 128; @@ -117,8 +142,8 @@ export function isUid(uid: any): boolean { /** * Validates that a string is a valid Firebase Auth password. * - * @param {any} password The password string to validate. - * @return {boolean} Whether the string is a valid Firebase Auth password. + * @param password - The password string to validate. + * @returns Whether the string is a valid Firebase Auth password. */ export function isPassword(password: any): boolean { // A password must be a string of at least 6 characters. @@ -129,8 +154,8 @@ export function isPassword(password: any): boolean { /** * Validates that a string is a valid email. * - * @param {any} email The string to validate. - * @return {boolean} Whether the string is valid email or not. + * @param email - The string to validate. + * @returns Whether the string is valid email or not. */ export function isEmail(email: any): boolean { if (typeof email !== 'string') { @@ -145,8 +170,8 @@ export function isEmail(email: any): boolean { /** * Validates that a string is a valid phone number. * - * @param {any} phoneNumber The string to validate. - * @return {boolean} Whether the string is a valid phone number or not. + * @param phoneNumber - The string to validate. + * @returns Whether the string is a valid phone number or not. */ export function isPhoneNumber(phoneNumber: any): boolean { if (typeof phoneNumber !== 'string') { @@ -161,20 +186,50 @@ export function isPhoneNumber(phoneNumber: any): boolean { return re1.test(phoneNumber) && re2.test(phoneNumber); } +/** + * Validates that a string is a valid ISO date string. + * + * @param dateString - The string to validate. + * @returns Whether the string is a valid ISO date string. + */ +export function isISODateString(dateString: any): boolean { + try { + return isNonEmptyString(dateString) && + (new Date(dateString).toISOString() === dateString); + } catch (e) { + return false; + } +} + + +/** + * Validates that a string is a valid UTC date string. + * + * @param dateString - The string to validate. + * @returns Whether the string is a valid UTC date string. + */ +export function isUTCDateString(dateString: any): boolean { + try { + return isNonEmptyString(dateString) && + (new Date(dateString).toUTCString() === dateString); + } catch (e) { + return false; + } +} /** * Validates that a string is a valid web URL. * - * @param {any} urlStr The string to validate. - * @return {boolean} Whether the string is valid web URL or not. + * @param urlStr - The string to validate. + * @returns Whether the string is valid web URL or not. */ export function isURL(urlStr: any): boolean { if (typeof urlStr !== 'string') { return false; } // Lookup illegal characters. - const re = /[^a-z0-9\:\/\?\#\[\]\@\!\$\&\'\(\)\*\+\,\;\=\.\-\_\~\%]/i; + const re = /[^a-z0-9:/?#[\]@!$&'()*+,;=.\-_~%]/i; if (re.test(urlStr)) { return false; } @@ -189,12 +244,12 @@ export function isURL(urlStr: any): boolean { } // Validate hostname: Can contain letters, numbers, underscore and dashes separated by a dot. // Each zone must not start with a hyphen or underscore. - if (!/^[a-zA-Z0-9]+[\w\-]*([\.]?[a-zA-Z0-9]+[\w\-]*)*$/.test(hostname)) { + if (!hostname || !/^[a-zA-Z0-9]+[\w-]*([.]?[a-zA-Z0-9]+[\w-]*)*$/.test(hostname)) { return false; } - // Allow for pathnames: (/chars+)* + // Allow for pathnames: (/chars+)*/? // Where chars can be a combination of: a-z A-Z 0-9 - _ . ~ ! $ & ' ( ) * + , ; = : @ % - const pathnameRe = /^(\/[\w\-\.\~\!\$\'\(\)\*\+\,\;\=\:\@\%]+)*$/; + const pathnameRe = /^(\/[\w\-.~!$'()*+,;=:@%]+)*\/?$/; // Validate pathname. if (pathname && pathname !== '/' && @@ -212,8 +267,8 @@ export function isURL(urlStr: any): boolean { /** * Validates that the provided topic is a valid FCM topic name. * - * @param {any} topic The topic to validate. - * @return {boolean} Whether the provided topic is a valid FCM topic name. + * @param topic - The topic to validate. + * @returns Whether the provided topic is a valid FCM topic name. */ export function isTopic(topic: any): boolean { if (typeof topic !== 'string') { @@ -223,3 +278,19 @@ export function isTopic(topic: any): boolean { const VALID_TOPIC_REGEX = /^(\/topics\/)?(private\/)?[a-zA-Z0-9-_.~%]+$/; return VALID_TOPIC_REGEX.test(topic); } + +/** + * Validates that the provided string can be used as a task ID + * for Cloud Tasks. + * + * @param taskId - the task ID to validate. + * @returns Whether the provided task ID is valid. + */ +export function isTaskId(taskId: any): boolean { + if (typeof taskId !== 'string') { + return false; + } + + const VALID_TASK_ID_REGEX = /^[A-Za-z0-9_-]+$/; + return VALID_TASK_ID_REGEX.test(taskId); +} diff --git a/test/integration/app-check.spec.ts b/test/integration/app-check.spec.ts new file mode 100644 index 0000000000..83a71a6f53 --- /dev/null +++ b/test/integration/app-check.spec.ts @@ -0,0 +1,117 @@ +/*! + * 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'); + 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); + }); + }); + + 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.include.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/integration/app.spec.ts b/test/integration/app.spec.ts index 532adb5798..3470ae088a 100644 --- a/test/integration/app.spec.ts +++ b/test/integration/app.spec.ts @@ -15,7 +15,9 @@ */ import * as admin from '../../lib/index'; -import {expect} from 'chai'; +import { App, deleteApp, getApp, initializeApp } from '../../lib/app/index'; +import { getAuth } from '../../lib/auth/index'; +import { expect } from 'chai'; import { defaultApp, nullApp, nonNullApp, databaseUrl, projectId, storageBucket, } from './setup'; @@ -27,16 +29,56 @@ describe('admin', () => { expect(storageBucket).to.be.not.empty; }); - it('does not load Firestore by default', () => { - const gcloud = require.cache[require.resolve('@google-cloud/firestore')]; - expect(gcloud).to.be.undefined; - }); + describe('Dependency lazy loading', () => { + const tempCache: {[key: string]: any} = {}; + const dependencies = ['@firebase/database-compat/standalone', '@google-cloud/firestore']; + let lazyLoadingApp: App; + + before(() => { + // Unload dependencies if already loaded. Some of the other test files have imports + // to firebase-admin/database and firebase-admin/firestore, which cause the corresponding + // dependencies to get loaded before the tests are executed. + dependencies.forEach((name) => { + const resolvedName = require.resolve(name); + tempCache[name] = require.cache[resolvedName]; + delete require.cache[resolvedName]; + }); + + // Initialize the SDK + lazyLoadingApp = initializeApp(defaultApp.options, 'lazyLoadingApp'); + }); + + it('does not load RTDB by default', () => { + const firebaseRtdb = require.cache[require.resolve('@firebase/database-compat/standalone')]; + expect(firebaseRtdb).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-compat/standalone')]; + expect(firebaseRtdb).to.not.be.undefined; + }); - it('loads Firestore when calling admin.firestore', () => { - const firestoreNamespace = admin.firestore; - expect(firestoreNamespace).to.not.be.null; - const gcloud = require.cache[require.resolve('@google-cloud/firestore')]; - expect(gcloud).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; + }); + + it('loads Firestore when calling admin.firestore', () => { + const firestoreNamespace = admin.firestore; + expect(firestoreNamespace).to.not.be.null; + const gcloud = require.cache[require.resolve('@google-cloud/firestore')]; + expect(gcloud).to.not.be.undefined; + }); + + after(() => { + dependencies.forEach((name) => { + const resolvedName = require.resolve(name); + require.cache[resolvedName] = tempCache[name]; + }); + return deleteApp(lazyLoadingApp); + }) }); }); @@ -84,3 +126,42 @@ describe('admin.app', () => { expect(admin.storage(app).app).to.deep.equal(app); }); }); + +describe('getApp', () => { + it('getApp() returns the default App', () => { + const app = getApp(); + expect(app).to.deep.equal(defaultApp); + expect(app.name).to.equal('[DEFAULT]'); + expect(app.options.databaseURL).to.equal(databaseUrl); + expect(app.options.databaseAuthVariableOverride).to.be.undefined; + expect(app.options.storageBucket).to.equal(storageBucket); + }); + + it('getApp("null") returns the App named "null"', () => { + const app = getApp('null'); + expect(app).to.deep.equal(nullApp); + expect(app.name).to.equal('null'); + expect(app.options.databaseURL).to.equal(databaseUrl); + expect(app.options.databaseAuthVariableOverride).to.be.null; + expect(app.options.storageBucket).to.equal(storageBucket); + }); + + it('getApp("nonNull") returns the App named "nonNull"', () => { + const app = getApp('nonNull'); + expect(app).to.deep.equal(nonNullApp); + expect(app.name).to.equal('nonNull'); + expect(app.options.databaseURL).to.equal(databaseUrl); + expect((app.options.databaseAuthVariableOverride as any).uid).to.be.ok; + expect(app.options.storageBucket).to.equal(storageBucket); + }); + + it('namespace services are attached to the default App', () => { + const app = getApp(); + expect(getAuth(app).app).to.deep.equal(app); + }); + + it('namespace services are attached to the named App', () => { + const app = getApp('null'); + expect(getAuth(app).app).to.deep.equal(app); + }); +}); diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index 64e0acb66f..7b113b3156 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -14,32 +14,60 @@ * limitations under the License. */ -import * as admin from '../../lib/index'; +import * as url from 'url'; +import * as crypto from 'crypto'; +import * as bcrypt from 'bcrypt'; import * as chai from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; -import firebase = require('firebase'); -import {clone} from 'lodash'; -import {generateRandomString, projectId, apiKey} from './setup'; +import firebase from '@firebase/app-compat'; +import '@firebase/auth-compat'; +import { clone } from 'lodash'; +import { User, FirebaseAuth } from '@firebase/auth-types'; +import { + generateRandomString, projectId, apiKey, noServiceAccountApp, cmdArgs, +} from './setup'; +import * as mocks from '../resources/mocks'; +import { deepExtend, deepCopy } from '../../src/utils/deep-copy'; +import { + AuthProviderConfig, CreateTenantRequest, DeleteUsersResult, PhoneMultiFactorInfo, + TenantAwareAuth, UpdatePhoneMultiFactorInfoRequest, UpdateTenantRequest, UserImportOptions, + UserImportRecord, UserRecord, getAuth, UpdateProjectConfigRequest, UserMetadata, MultiFactorConfig, + PasswordPolicyConfig, SmsRegionConfig, +} from '../../lib/auth/index'; +import * as sinon from 'sinon'; +import * as sinonChai from 'sinon-chai'; + +const chalk = require('chalk'); // eslint-disable-line @typescript-eslint/no-var-requires chai.should(); +chai.use(sinonChai); 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); +const sessionCookieUids = [ + generateRandomString(20), + generateRandomString(20), + generateRandomString(20), + generateRandomString(20), +]; const testPhoneNumber = '+11234567890'; const testPhoneNumber2 = '+16505550101'; const nonexistentPhoneNumber = '+18888888888'; -const updatedEmail = generateRandomString(20) + '@example.com'; +const updatedEmail = generateRandomString(20).toLowerCase() + '@example.com'; const updatedPhone = '+16505550102'; -const customClaims = { +const customClaims: { [key: string]: any } = { admin: true, groupId: '1234', }; const uids = [newUserUid + '-1', newUserUid + '-2', newUserUid + '-3']; const mockUserData = { - email: newUserUid + '@example.com', + email: newUserUid.toLowerCase() + '@example.com', emailVerified: false, phoneNumber: testPhoneNumber, password: 'password', @@ -47,33 +75,79 @@ const mockUserData = { photoURL: 'http://www.example.com/' + newUserUid + '/photo.png', disabled: false, }; +const actionCodeSettings = { + url: 'http://localhost/?a=1&b=2#c=3', + handleCodeInApp: false, +}; +let deleteQueue = Promise.resolve(); + +interface UserImportTest { + name: string; + importOptions: UserImportOptions; + rawPassword: string; + rawSalt?: string; + computePasswordHash(userImportTest: UserImportTest): Buffer; +} + +/** @return Random generated SAML provider ID. */ +function randomSamlProviderId(): string { + return 'saml.' + generateRandomString(10, false).toLowerCase(); +} + +/** @return Random generated OIDC provider ID. */ +function randomOidcProviderId(): string { + return 'oidc.' + generateRandomString(10, false).toLowerCase(); +} + +function clientAuth(): FirebaseAuth { + expect(firebase.auth).to.be.ok; + return firebase.auth!(); +} describe('admin.auth', () => { let uidFromCreateUserWithoutUid: string; + const processWarningSpy = sinon.spy(); before(() => { firebase.initializeApp({ apiKey, authDomain: projectId + '.firebaseapp.com', }); - cleanup(); + if (authEmulatorHost) { + (clientAuth() as any).useEmulator('http://' + authEmulatorHost); + } + process.on('warning', processWarningSpy); + return cleanup(); + }); + + afterEach(() => { + expect( + processWarningSpy.neverCalledWith( + sinon.match( + (warning: Error) => warning.name === 'MaxListenersExceededWarning' + ) + ), + 'process.on("warning") was called with an unexpected MaxListenersExceededWarning.' + ).to.be.true; + processWarningSpy.resetHistory(); }); after(() => { - cleanup(); + process.removeListener('warning', processWarningSpy); + return cleanup(); }); it('createUser() creates a new user when called without a UID', () => { const newUserData = clone(mockUserData); - newUserData.email = generateRandomString(20) + '@example.com'; + newUserData.email = generateRandomString(20).toLowerCase() + '@example.com'; newUserData.phoneNumber = testPhoneNumber2; - return admin.auth().createUser(newUserData) + return getAuth().createUser(newUserData) .then((userRecord) => { uidFromCreateUserWithoutUid = userRecord.uid; expect(typeof userRecord.uid).to.equal('string'); // Confirm expected email. - expect(userRecord.email).to.equal(newUserData.email.toLowerCase()); + expect(userRecord.email).to.equal(newUserData.email); // Confirm expected phone number. expect(userRecord.phoneNumber).to.equal(newUserData.phoneNumber); }); @@ -82,58 +156,461 @@ describe('admin.auth', () => { it('createUser() creates a new user with the specified UID', () => { const newUserData: any = clone(mockUserData); newUserData.uid = newUserUid; - return admin.auth().createUser(newUserData) + return getAuth().createUser(newUserData) .then((userRecord) => { expect(userRecord.uid).to.equal(newUserUid); // Confirm expected email. - expect(userRecord.email).to.equal(newUserData.email.toLowerCase()); + expect(userRecord.email).to.equal(newUserData.email); // Confirm expected phone number. expect(userRecord.phoneNumber).to.equal(newUserData.phoneNumber); }); }); + 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', + displayName: 'Work phone number', + factorId: 'phone', + }, + { + phoneNumber: '+16505550002', + displayName: 'Personal phone number', + factorId: 'phone', + }, + ]; + const newUserData: any = { + uid: newMultiFactorUserUid, + email: generateRandomString(20).toLowerCase() + '@example.com', + emailVerified: true, + password: 'password', + multiFactor: { + enrolledFactors, + }, + }; + return getAuth().createUser(newUserData) + .then((userRecord) => { + expect(userRecord.uid).to.equal(newMultiFactorUserUid); + // Confirm expected email. + expect(userRecord.email).to.equal(newUserData.email); + // Confirm second factors added to user. + expect(userRecord.multiFactor!.enrolledFactors.length).to.equal(2); + // Confirm first enrolled second factor. + const firstMultiFactor = userRecord.multiFactor!.enrolledFactors[0]; + expect(firstMultiFactor.uid).not.to.be.undefined; + expect(firstMultiFactor.enrollmentTime).not.to.be.undefined; + expect((firstMultiFactor as 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 PhoneMultiFactorInfo).phoneNumber).to.equal( + enrolledFactors[1].phoneNumber); + expect(secondMultiFactor.displayName).to.equal(enrolledFactors[1].displayName); + expect(secondMultiFactor.factorId).to.equal(enrolledFactors[1].factorId); + }); + }); + it('createUser() fails when the UID is already in use', () => { const newUserData: any = clone(mockUserData); newUserData.uid = newUserUid; - return admin.auth().createUser(newUserData) + return getAuth().createUser(newUserData) .should.eventually.be.rejected.and.have.property('code', 'auth/uid-already-exists'); }); it('getUser() returns a user record with the matching UID', () => { - return admin.auth().getUser(newUserUid) + return getAuth().getUser(newUserUid) .then((userRecord) => { expect(userRecord.uid).to.equal(newUserUid); }); }); it('getUserByEmail() returns a user record with the matching email', () => { - return admin.auth().getUserByEmail(mockUserData.email) + return getAuth().getUserByEmail(mockUserData.email) .then((userRecord) => { expect(userRecord.uid).to.equal(newUserUid); }); }); it('getUserByPhoneNumber() returns a user record with the matching phone number', () => { - return admin.auth().getUserByPhoneNumber(mockUserData.phoneNumber) + return getAuth().getUserByPhoneNumber(mockUserData.phoneNumber) + .then((userRecord) => { + expect(userRecord.uid).to.equal(newUserUid); + }); + }); + + 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: 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 getAuth().importUsers([importUser]); + + try { + await getAuth().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 getAuth().getUserByProviderUid('email', mockUserData.email) + .then((userRecord) => { + expect(userRecord.uid).to.equal(newUserUid); + }); + }); + + it('getUserByProviderUid() redirects to getUserByPhoneNumber if given a phone number', () => { + return getAuth().getUserByProviderUid('phone', mockUserData.phoneNumber) + .then((userRecord) => { + expect(userRecord.uid).to.equal(newUserUid); + }); + }); + + 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: 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 getAuth().importUsers([importUser]); + + try { + await getAuth().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 getAuth().getUserByProviderUid('email', mockUserData.email) + .then((userRecord) => { + expect(userRecord.uid).to.equal(newUserUid); + }); + }); + + it('getUserByProviderUid() redirects to getUserByPhoneNumber if given a phone number', () => { + return getAuth().getUserByProviderUid('phone', mockUserData.phoneNumber) + .then((userRecord) => { + expect(userRecord.uid).to.equal(newUserUid); + }); + }); + + 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: 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 getAuth().importUsers([importUser]); + + try { + await getAuth().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 getAuth().getUserByProviderUid('email', mockUserData.email) + .then((userRecord) => { + expect(userRecord.uid).to.equal(newUserUid); + }); + }); + + it('getUserByProviderUid() redirects to getUserByPhoneNumber if given a phone number', () => { + return getAuth().getUserByProviderUid('phone', mockUserData.phoneNumber) .then((userRecord) => { expect(userRecord.uid).to.equal(newUserUid); }); }); + 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: 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 getAuth().importUsers([importUser]); + + try { + await getAuth().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 getAuth().getUserByProviderUid('email', mockUserData.email) + .then((userRecord) => { + expect(userRecord.uid).to.equal(newUserUid); + }); + }); + + it('getUserByProviderUid() redirects to getUserByPhoneNumber if given a phone number', () => { + return getAuth().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 + * 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 }> { + return values.map((ur) => ({ uid: ur.uid, email: ur.email, phoneNumber: ur.phoneNumber })); + } + + const testUser1 = { uid: 'uid1', email: 'user1@example.com', phoneNumber: '+15555550001' }; + const testUser2 = { uid: 'uid2', email: 'user2@example.com', phoneNumber: '+15555550002' }; + const testUser3 = { uid: 'uid3', email: 'user3@example.com', phoneNumber: '+15555550003' }; + const usersToCreate = [testUser1, testUser2, testUser3]; + + // Also create a user with a provider config. (You can't create a user with + // a provider config. But you *can* import one.) + const importUser1: UserImportRecord = { + uid: 'uid4', + email: 'user4@example.com', + phoneNumber: '+15555550004', + 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 Four', + email: 'user4@example.com', + phoneNumber: '+15555550004', + photoURL: 'http://example.com/user4', + providerId: 'google.com', + uid: 'google_uid4', + }], + }; + + const testUser4 = mapUserRecordsToUidEmailPhones([importUser1])[0]; + + before(async () => { + // Delete all the users that we're about to create (in case they were + // left over from a prior run). + const uidsToDelete = usersToCreate.map((user) => user.uid); + uidsToDelete.push(importUser1.uid); + await deleteUsersWithDelay(uidsToDelete); + + // Create/import users required by these tests + await Promise.all(usersToCreate.map((user) => getAuth().createUser(user))); + await getAuth().importUsers([importUser1]); + }); + + after(async () => { + const uidsToDelete = usersToCreate.map((user) => user.uid); + uidsToDelete.push(importUser1.uid); + await deleteUsersWithDelay(uidsToDelete); + }); + + it('returns users by various identifier types in a single call', async () => { + const users = await getAuth().getUsers([ + { uid: 'uid1' }, + { email: 'user2@example.com' }, + { phoneNumber: '+15555550003' }, + { providerId: 'google.com', providerUid: 'google_uid4' }, + ]) + .then((getUsersResult) => getUsersResult.users) + .then(mapUserRecordsToUidEmailPhones); + + expect(users).to.have.deep.members([testUser1, testUser2, testUser3, testUser4]); + }); + + it('returns found users and ignores non-existing users', async () => { + const users = await getAuth().getUsers([ + { uid: 'uid1' }, + { uid: 'uid_that_doesnt_exist' }, + { uid: 'uid3' }, + ]); + 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 users = await getAuth().getUsers(notFoundIds); + + expect(users.users).to.be.empty; + expect(users.notFound).to.deep.equal(notFoundIds); + }); + + it('de-dups duplicate users', async () => { + const users = await getAuth().getUsers([ + { uid: 'uid1' }, + { uid: 'uid1' }, + ]) + .then((getUsersResult) => getUsersResult.users) + .then(mapUserRecordsToUidEmailPhones); + + expect(users).to.deep.equal([testUser1]); + }); + + it('returns users with a lastRefreshTime', async () => { + const isUTCString = (s: string): boolean => { + return new Date(s).toUTCString() === s; + }; + + const newUserRecord = await getAuth().createUser({ + uid: 'lastRefreshTimeUser', + email: 'lastRefreshTimeUser@example.com', + password: 'p4ssword', + }); + + try { + // New users should not have a lastRefreshTime set. + expect(newUserRecord.metadata.lastRefreshTime).to.be.null; + + // Login to set the lastRefreshTime. + await firebase.auth!().signInWithEmailAndPassword('lastRefreshTimeUser@example.com', 'p4ssword') + .then(async () => { + // Attempt to retrieve the user 3 times (with a small delay between + // each attempt). Occassionally, this call retrieves the user data + // without the lastLoginTime/lastRefreshTime set; possibly because + // it's hitting a different server than the login request uses. + let userRecord: UserRecord | null = null; + + for (let i = 0; i < 3; i++) { + userRecord = await getAuth().getUser('lastRefreshTimeUser'); + if (userRecord!['metadata']['lastRefreshTime']) { + break; + } + + await new Promise((resolve) => { + setTimeout(resolve, 1000 * Math.pow(2, i)); + }); + } + + const metadata = userRecord!['metadata']; + expect(metadata['lastRefreshTime']).to.exist; + 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); + expect(lastRefreshTime).lte(creationTime + 3600 * 1000); + }); + } finally { + getAuth().deleteUser('lastRefreshTimeUser'); + } + }); + }); + it('listUsers() returns up to the specified number of users', () => { - const promises: Array> = []; + const promises: Array> = []; uids.forEach((uid) => { const tempUserData = { uid, password: 'password', }; - promises.push(admin.auth().createUser(tempUserData)); + promises.push(getAuth().createUser(tempUserData)); }); return Promise.all(promises) .then(() => { // Return 2 users with the provided page token. // This test will fail if other users are created in between. - return admin.auth().listUsers(2, uids[0]); + return getAuth().listUsers(2, uids[0]); }) .then((listUsersResult) => { // Confirm expected number of users. @@ -142,44 +619,64 @@ describe('admin.auth', () => { 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]); - expect(listUsersResult.users[0].passwordHash.length).greaterThan(0); - expect(listUsersResult.users[0].passwordSalt.length).greaterThan(0); + + expect( + listUsersResult.users[0].passwordHash, + 'Missing passwordHash field. A common cause would be forgetting to ' + + 'add the "Firebase Authentication Admin" permission. See ' + + 'instructions in CONTRIBUTING.md', + ).to.be.ok; + expect(listUsersResult.users[0].passwordHash!.length).greaterThan(0); + + expect( + listUsersResult.users[0].passwordSalt, + 'Missing passwordSalt field. A common cause would be forgetting to ' + + 'add the "Firebase Authentication Admin" permission. See ' + + 'instructions in CONTRIBUTING.md', + ).to.be.ok; + expect(listUsersResult.users[0].passwordSalt!.length).greaterThan(0); expect(listUsersResult.users[1].uid).to.equal(uids[2]); - expect(listUsersResult.users[1].passwordHash.length).greaterThan(0); - expect(listUsersResult.users[1].passwordSalt.length).greaterThan(0); + expect(listUsersResult.users[1].passwordHash!.length).greaterThan(0); + expect(listUsersResult.users[1].passwordSalt!.length).greaterThan(0); }); }); - it('revokeRefreshTokens() invalidates existing sessions and ID tokens', () => { - let currentIdToken: string = null; - let currentUser: any = null; + it('revokeRefreshTokens() invalidates existing sessions and ID tokens', async () => { + let currentIdToken: string; + let currentUser: User; // Sign in with an email and password account. - return firebase.auth().signInWithEmailAndPassword(mockUserData.email, mockUserData.password) - .then((user) => { - currentUser = user; + return clientAuth().signInWithEmailAndPassword(mockUserData.email, mockUserData.password) + .then(({ user }) => { + expect(user).to.exist; + currentUser = user!; // Get user's ID token. - return user.getIdToken(); + return user!.getIdToken(); }) .then((idToken) => { currentIdToken = idToken; // Verify that user's ID token while checking for revocation. - return admin.auth().verifyIdToken(currentIdToken, true); + return getAuth().verifyIdToken(currentIdToken, true); }) .then((decodedIdToken) => { // Verification should succeed. Revoke that user's session. return new Promise((resolve) => setTimeout(() => resolve( - admin.auth().revokeRefreshTokens(decodedIdToken.sub), + getAuth().revokeRefreshTokens(decodedIdToken.sub), ), 1000)); }) .then(() => { - // verifyIdToken without checking revocation should still succeed. - return admin.auth().verifyIdToken(currentIdToken) - .should.eventually.be.fulfilled; + const verifyingIdToken = getAuth().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. - return admin.auth().verifyIdToken(currentIdToken, true) + return getAuth().verifyIdToken(currentIdToken, true) .should.eventually.be.rejected.and.have.property('code', 'auth/id-token-revoked'); }) .then(() => { @@ -189,106 +686,354 @@ describe('admin.auth', () => { }) .then(() => { // New sign-in should succeed. - return firebase.auth().signInWithEmailAndPassword( - mockUserData.email, mockUserData.password); + return clientAuth().signInWithEmailAndPassword( + mockUserData.email, mockUserData.password); }) - .then((user) => { + .then(({ user }) => { // Get new session's ID token. - return user.getIdToken(); + expect(user).to.exist; + return user!.getIdToken(); }) .then((idToken) => { // ID token for new session should be valid even with revocation check. - return admin.auth().verifyIdToken(idToken, true) + return getAuth().verifyIdToken(idToken, true) .should.eventually.be.fulfilled; }); - }).timeout(10000); + }); it('setCustomUserClaims() sets claims that are accessible via user\'s ID token', () => { // Set custom claims on the user. - return admin.auth().setCustomUserClaims(newUserUid, customClaims) + return getAuth().setCustomUserClaims(newUserUid, customClaims) .then(() => { - return admin.auth().getUser(newUserUid); + return getAuth().getUser(newUserUid); }) .then((userRecord) => { // Confirm custom claims set on the UserRecord. expect(userRecord.customClaims).to.deep.equal(customClaims); - return firebase.auth().signInWithEmailAndPassword( - userRecord.email, mockUserData.password); + expect(userRecord.email).to.exist; + return clientAuth().signInWithEmailAndPassword( + userRecord.email!, mockUserData.password); }) - .then((user) => { - // Get the user's ID token. - return user.getIdToken(); + .then(({ user }) => { + // Get the user's ID token. + expect(user).to.exist; + return user!.getIdToken(); }) .then((idToken) => { - // Verify ID token contents. - return admin.auth().verifyIdToken(idToken); + // Verify ID token contents. + return getAuth().verifyIdToken(idToken); }) - .then((decodedIdToken) => { + .then((decodedIdToken: { [key: string]: any }) => { // Confirm expected claims set on the user's ID token. for (const key in customClaims) { - if (customClaims.hasOwnProperty(key)) { + if (Object.prototype.hasOwnProperty.call(customClaims, key)) { expect(decodedIdToken[key]).to.equal(customClaims[key]); } } + // Test clearing of custom claims. + return getAuth().setCustomUserClaims(newUserUid, null); + }) + .then(() => { + return getAuth().getUser(newUserUid); + }) + .then((userRecord) => { + // Custom claims should be cleared. + expect(userRecord.customClaims).to.deep.equal({}); + // Force token refresh. All claims should be cleared. + expect(clientAuth().currentUser).to.exist; + return clientAuth().currentUser!.getIdToken(true); + }) + .then((idToken) => { + // Verify ID token contents. + return getAuth().verifyIdToken(idToken); + }) + .then((decodedIdToken: { [key: string]: any }) => { + // Confirm all custom claims are cleared. + for (const key in customClaims) { + if (Object.prototype.hasOwnProperty.call(customClaims, key)) { + expect(decodedIdToken[key]).to.be.undefined; + } + } }); }); - 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.toLowerCase()); - // 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 getAuth().createUser({ + uid: name + '_' + tenRandomChars, + displayName: name, + email: name + '_' + tenRandomChars + '@example.com', + }); + } + + let updateUser: 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 getAuth().updateUser(updateUser.uid, { + 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('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 getAuth().updateUser(updateUser.uid, { + 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 getAuth().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 getAuth().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 function () { + if (authEmulatorHost) { + return this.skip(); // Not yet supported in Auth Emulator. + } + const googleFederatedUid = 'google_uid_' + generateRandomString(10); + let userRecord = await getAuth().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 getAuth().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 function () { + if (authEmulatorHost) { + return this.skip(); // Not yet supported in Auth Emulator. + } + await deletePhoneNumberUser('+15555550001'); + + const googleFederatedUid = 'google_uid_' + generateRandomString(10); + const facebookFederatedUid = 'facebook_uid_' + generateRandomString(10); + + let userRecord = await getAuth().updateUser(updateUser.uid, { + phoneNumber: '+15555550001', + providerToLink: { + providerId: 'google.com', + uid: googleFederatedUid, + }, + }); + userRecord = await getAuth().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 getAuth().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 getAuth().updateUser(userRecord.uid, { + providersToUnlink: [], + }); + + expect(updatedUserRecord).to.deep.equal(userRecord); + } finally { + safeDelete(userRecord.uid); + } + }); + + it('A user with user record disabled is unable to sign in', async () => { + const password = 'password'; + const email = 'updatedEmail@example.com'; + return getAuth().updateUser(updateUser.uid, { disabled: true, password, email }) + .then(() => { + return clientAuth().signInWithEmailAndPassword(email, password); + }) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.have.property('code', 'auth/user-disabled'); + }); + }); }); it('getUser() fails when called with a non-existing UID', () => { - return admin.auth().getUser(nonexistentUid) + return getAuth().getUser(nonexistentUid) .should.eventually.be.rejected.and.have.property('code', 'auth/user-not-found'); }); it('getUserByEmail() fails when called with a non-existing email', () => { - return admin.auth().getUserByEmail(nonexistentUid + '@example.com') + return getAuth().getUserByEmail(nonexistentUid + '@example.com') .should.eventually.be.rejected.and.have.property('code', 'auth/user-not-found'); }); it('getUserByPhoneNumber() fails when called with a non-existing phone number', () => { - return admin.auth().getUserByPhoneNumber(nonexistentPhoneNumber) + return getAuth().getUserByPhoneNumber(nonexistentPhoneNumber) + .should.eventually.be.rejected.and.have.property('code', 'auth/user-not-found'); + }); + + it('getUserByProviderUid() fails when called with a non-existing provider id', () => { + return getAuth().getUserByProviderUid('google.com', nonexistentUid) + .should.eventually.be.rejected.and.have.property('code', 'auth/user-not-found'); + }); + + it('getUserByProviderUid() fails when called with a non-existing provider id', () => { + return getAuth().getUserByProviderUid('google.com', nonexistentUid) + .should.eventually.be.rejected.and.have.property('code', 'auth/user-not-found'); + }); + + it('getUserByProviderUid() fails when called with a non-existing provider id', () => { + return getAuth().getUserByProviderUid('google.com', nonexistentUid) + .should.eventually.be.rejected.and.have.property('code', 'auth/user-not-found'); + }); + + it('getUserByProviderUid() fails when called with a non-existing provider id', () => { + return getAuth().getUserByProviderUid('google.com', nonexistentUid) + .should.eventually.be.rejected.and.have.property('code', 'auth/user-not-found'); + }); + + it('getUserByProviderUid() fails when called with a non-existing provider id', () => { + return getAuth().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, { + return getAuth().updateUser(nonexistentUid, { emailVerified: true, }).should.eventually.be.rejected.and.have.property('code', 'auth/user-not-found'); }); it('deleteUser() fails when called with a non-existing UID', () => { - return admin.auth().deleteUser(nonexistentUid) + return getAuth().deleteUser(nonexistentUid) .should.eventually.be.rejected.and.have.property('code', 'auth/user-not-found'); }); it('createCustomToken() mints a JWT that can be used to sign in', () => { - return admin.auth().createCustomToken(newUserUid, { + return getAuth().createCustomToken(newUserUid, { + isAdmin: true, + }) + .then((customToken) => { + return clientAuth().signInWithCustomToken(customToken); + }) + .then(({ user }) => { + expect(user).to.exist; + return user!.getIdToken(); + }) + .then((idToken) => { + return getAuth().verifyIdToken(idToken); + }) + .then((token) => { + expect(token.uid).to.equal(newUserUid); + expect(token.isAdmin).to.be.true; + }); + }); + + it('createCustomToken() can mint JWTs without a service account', () => { + return getAuth(noServiceAccountApp).createCustomToken(newUserUid, { isAdmin: true, }) .then((customToken) => { - return firebase.auth().signInWithCustomToken(customToken); + return clientAuth().signInWithCustomToken(customToken); }) - .then((user) => { - return user.getIdToken(); + .then(({ user }) => { + expect(user).to.exist; + return user!.getIdToken(); }) .then((idToken) => { - return admin.auth().verifyIdToken(idToken); + return getAuth(noServiceAccountApp).verifyIdToken(idToken); }) .then((token) => { expect(token.uid).to.equal(newUserUid); @@ -297,63 +1042,2226 @@ describe('admin.auth', () => { }); it('verifyIdToken() fails when called with an invalid token', () => { - return admin.auth().verifyIdToken('invalid-token') + return getAuth().verifyIdToken('invalid-token') .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); }); - it('deleteUser() deletes the user with the given UID', () => { - return Promise.all([ - admin.auth().deleteUser(newUserUid), - admin.auth().deleteUser(uidFromCreateUserWithoutUid), - ]).should.eventually.be.fulfilled; + if (authEmulatorHost) { + describe('Auth emulator support', () => { + const uid = 'authEmulatorUser'; + before(() => { + return getAuth().createUser({ + uid, + email: 'lastRefreshTimeUser@example.com', + password: 'p4ssword', + }); + }); + after(() => { + return getAuth().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, + }, undefined, 'secret'); + return getAuth().verifyIdToken(unsignedToken); + }); + + it('verifyIdToken() fails when called with a token with wrong project', () => { + const unsignedToken = mocks.generateIdToken( + { algorithm: 'none', audience: 'nosuch' }, + undefined, 'secret'); + return getAuth().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', + }, undefined, 'secret'); + return getAuth().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'; + const newEmail = uid + 'new@example.com'; + const newPassword = 'newPassword'; + const userData = { + uid, + email, + emailVerified: false, + password: 'password', + }; + + // Create the test user before running this suite of tests. + before(() => { + return getAuth().createUser(userData); + }); + + // Sign out after each test. + afterEach(() => { + return clientAuth().signOut(); + }); + + // Delete test user at the end of test suite. + after(() => { + return safeDelete(uid); + }); + + it('generatePasswordResetLink() should return a password reset link', () => { + // Ensure old password set on created user. + return getAuth().updateUser(uid, { password: 'password' }) + .then(() => { + return getAuth().generatePasswordResetLink(email, actionCodeSettings); + }) + .then((link) => { + const code = getActionCode(link); + expect(getContinueUrl(link)).equal(actionCodeSettings.url); + return clientAuth().confirmPasswordReset(code, newPassword); + }) + .then(() => { + return clientAuth().signInWithEmailAndPassword(email, newPassword); + }) + .then((result) => { + expect(result.user).to.exist; + expect(result.user!.email).to.equal(email); + // Password reset also verifies the user's email. + expect(result.user!.emailVerified).to.be.true; + }); + }); + + it('generateEmailVerificationLink() should return a verification link', () => { + // Ensure the user's email is unverified. + return getAuth().updateUser(uid, { password: 'password', emailVerified: false }) + .then((userRecord) => { + expect(userRecord.emailVerified).to.be.false; + return getAuth().generateEmailVerificationLink(email, actionCodeSettings); + }) + .then((link) => { + const code = getActionCode(link); + expect(getContinueUrl(link)).equal(actionCodeSettings.url); + return clientAuth().applyActionCode(code); + }) + .then(() => { + return clientAuth().signInWithEmailAndPassword(email, userData.password); + }) + .then((result) => { + expect(result.user).to.exist; + expect(result.user!.email).to.equal(email); + expect(result.user!.emailVerified).to.be.true; + }); + }); + + it('generateSignInWithEmailLink() should return a sign-in link', () => { + return getAuth().generateSignInWithEmailLink(email, actionCodeSettings) + .then((link) => { + expect(getContinueUrl(link)).equal(actionCodeSettings.url); + return clientAuth().signInWithEmailLink(email, link); + }) + .then((result) => { + expect(result.user).to.exist; + expect(result.user!.email).to.equal(email); + expect(result.user!.emailVerified).to.be.true; + }); + }); + + it('generateVerifyAndChangeEmailLink() should return a verification link', function () { + if (authEmulatorHost) { + return this.skip(); // Not yet supported in Auth Emulator. + } + // Ensure the user's email is verified. + return getAuth().updateUser(uid, { password: 'password', emailVerified: true }) + .then((userRecord) => { + expect(userRecord.emailVerified).to.be.true; + return getAuth().generateVerifyAndChangeEmailLink(email, newEmail, actionCodeSettings); + }) + .then((link) => { + const code = getActionCode(link); + expect(getContinueUrl(link)).equal(actionCodeSettings.url); + return clientAuth().applyActionCode(code); + }) + .then(() => { + return clientAuth().signInWithEmailAndPassword(newEmail, 'password'); + }) + .then((result) => { + expect(result.user).to.exist; + expect(result.user!.email).to.equal(newEmail); + expect(result.user!.emailVerified).to.be.true; + }); + }); }); -}); -/** - * 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 - * or is found not to exist. - */ -function deletePhoneNumberUser(phoneNumber) { - return admin.auth().getUserByPhoneNumber(phoneNumber) - .then((userRecord) => { - return admin.auth().deleteUser(userRecord.uid); - }) - .catch((error) => { - // Suppress user not found error. - if (error.code !== 'auth/user-not-found') { - throw error; + describe('Project config management operations', () => { + before(function () { + if (authEmulatorHost) { + this.skip(); // getConfig is not supported in Auth Emulator } }); -} -/** - * Runs cleanup routine that could affect outcome of tests and removes any - * intermediate users created. - * - * @return {Promise} A promise that resolves when test preparations are ready. - */ -function cleanup() { - // Delete any existing users that could affect the test outcome. - const promises: Array> = [ - deletePhoneNumberUser(testPhoneNumber), - deletePhoneNumberUser(testPhoneNumber2), - deletePhoneNumberUser(nonexistentPhoneNumber), - deletePhoneNumberUser(updatedPhone), - ]; - // Delete list of users for testing listUsers. - uids.forEach((uid) => { - promises.push( - admin.auth().deleteUser(uid) - .catch((error) => { - // Suppress user not found error. - if (error.code !== 'auth/user-not-found') { - throw error; + after(() => { + getAuth().projectConfigManager().updateProjectConfig({ + passwordPolicyConfig: { + enforcementState: 'OFF', + forceUpgradeOnSignin: false, + constraints: { + requireLowercase: false, + requireNonAlphanumeric: false, + requireNumeric: false, + requireUppercase: false, + maxLength: 4096, + minLength: 6, } - }), - ); - }); - return Promise.all(promises); + } + }) + }); + + const mfaSmsEnabledTotpEnabledConfig: MultiFactorConfig = { + state: 'ENABLED', + factorIds: ['phone'], + providerConfigs: [ + { + state: 'ENABLED', + totpProviderConfig: { + adjacentIntervals: 5, + }, + }, + ], + }; + const mfaSmsEnabledTotpDisabledConfig: MultiFactorConfig = { + state: 'ENABLED', + factorIds: ['phone'], + providerConfigs: [ + { + state: 'DISABLED', + totpProviderConfig: {}, + } + ], + }; + const passwordConfig: PasswordPolicyConfig = { + enforcementState: 'ENFORCE', + forceUpgradeOnSignin: true, + constraints: { + requireUppercase: true, + requireLowercase: true, + requireNonAlphanumeric: true, + requireNumeric: true, + minLength: 8, + maxLength: 30, + }, + }; + const smsRegionAllowByDefaultConfig: SmsRegionConfig = { + allowByDefault: { + disallowedRegions: ['AC', 'AD'], + } + }; + const smsRegionAllowlistOnlyConfig: SmsRegionConfig = { + allowlistOnly: { + allowedRegions: ['AC', 'AD'], + } + }; + const projectConfigOption1: UpdateProjectConfigRequest = { + smsRegionConfig: smsRegionAllowByDefaultConfig, + multiFactorConfig: mfaSmsEnabledTotpEnabledConfig, + passwordPolicyConfig: passwordConfig, + recaptchaConfig: { + emailPasswordEnforcementState: 'AUDIT', + managedRules: [ + { + endScore: 0.1, + action: 'BLOCK', + }, + ], + useAccountDefender: true, + }, + emailPrivacyConfig: { + enableImprovedEmailPrivacy: true, + } + }; + const projectConfigOption2: UpdateProjectConfigRequest = { + smsRegionConfig: smsRegionAllowlistOnlyConfig, + recaptchaConfig: { + emailPasswordEnforcementState: 'OFF', + useAccountDefender: false, + }, + emailPrivacyConfig: { + enableImprovedEmailPrivacy: false, + } + }; + const projectConfigOptionSmsEnabledTotpDisabled: UpdateProjectConfigRequest = { + smsRegionConfig: smsRegionAllowlistOnlyConfig, + multiFactorConfig: mfaSmsEnabledTotpDisabledConfig, + }; + const expectedProjectConfig1: any = { + smsRegionConfig: smsRegionAllowByDefaultConfig, + multiFactorConfig: mfaSmsEnabledTotpEnabledConfig, + passwordPolicyConfig: passwordConfig, + recaptchaConfig: { + emailPasswordEnforcementState: 'AUDIT', + managedRules: [ + { + endScore: 0.1, + action: 'BLOCK', + }, + ], + useAccountDefender: true, + }, + emailPrivacyConfig: { + enableImprovedEmailPrivacy: true, + }, + }; + const expectedProjectConfig2: any = { + smsRegionConfig: smsRegionAllowlistOnlyConfig, + multiFactorConfig: mfaSmsEnabledTotpEnabledConfig, + passwordPolicyConfig: passwordConfig, + recaptchaConfig: { + emailPasswordEnforcementState: 'OFF', + managedRules: [ + { + endScore: 0.1, + action: 'BLOCK', + }, + ], + }, + emailPrivacyConfig: {}, + }; + const expectedProjectConfigSmsEnabledTotpDisabled: any = { + smsRegionConfig: smsRegionAllowlistOnlyConfig, + multiFactorConfig: mfaSmsEnabledTotpDisabledConfig, + passwordPolicyConfig: passwordConfig, + recaptchaConfig: { + emailPasswordEnforcementState: 'OFF', + managedRules: [ + { + endScore: 0.1, + action: 'BLOCK', + }, + ], + }, + emailPrivacyConfig: {}, + }; + + it('updateProjectConfig() should resolve with the updated project config', () => { + return getAuth().projectConfigManager().updateProjectConfig(projectConfigOption1) + .then((actualProjectConfig) => { + // ReCAPTCHA keys are generated differently each time. + delete actualProjectConfig.recaptchaConfig?.recaptchaKeys; + expect(actualProjectConfig.toJSON()).to.deep.equal(expectedProjectConfig1); + return getAuth().projectConfigManager().updateProjectConfig(projectConfigOption2); + }) + .then((actualProjectConfig) => { + expect(actualProjectConfig.toJSON()).to.deep.equal(expectedProjectConfig2); + return getAuth().projectConfigManager().updateProjectConfig(projectConfigOptionSmsEnabledTotpDisabled); + }) + .then((actualProjectConfig) => { + expect(actualProjectConfig.toJSON()).to.deep.equal(expectedProjectConfigSmsEnabledTotpDisabled); + }); + }); + + it('getProjectConfig() should resolve with expected project config', () => { + return getAuth().projectConfigManager().getProjectConfig() + .then((actualConfig) => { + const actualConfigObj = actualConfig.toJSON(); + expect(actualConfigObj).to.deep.equal(expectedProjectConfigSmsEnabledTotpDisabled); + }); + }); + }); + + describe('Tenant management operations', () => { + let createdTenantId: string; + const createdTenants: string[] = []; + const mfaSmsEnabledTotpEnabledConfig: MultiFactorConfig = { + state: 'ENABLED', + factorIds: ['phone'], + providerConfigs: [ + { + state: 'ENABLED', + totpProviderConfig: { + adjacentIntervals: 5, + }, + }, + ], + }; + const mfaSmsEnabledTotpDisabledConfig: MultiFactorConfig = { + state: 'ENABLED', + factorIds: ['phone'], + providerConfigs: [ + { + state: 'DISABLED', + totpProviderConfig: {}, + } + ], + } + const mfaSmsDisabledTotpEnabledConfig: MultiFactorConfig = { + state: 'DISABLED', + factorIds: [], + providerConfigs: [ + { + state: 'ENABLED', + totpProviderConfig: {}, + } + ], + } + const smsRegionAllowByDefaultConfig: SmsRegionConfig = { + allowByDefault: { + disallowedRegions: ['AC', 'AD'], + } + } + const tenantOptions: CreateTenantRequest = { + displayName: 'testTenant1', + emailSignInConfig: { + enabled: true, + passwordRequired: true, + }, + multiFactorConfig: mfaSmsEnabledTotpEnabledConfig, + // Add random phone number / code pairs. + testPhoneNumbers: { + '+16505551234': '019287', + '+16505550676': '985235', + }, + emailPrivacyConfig: { + enableImprovedEmailPrivacy: true, + }, + }; + const expectedCreatedTenant: any = { + displayName: 'testTenant1', + emailSignInConfig: { + enabled: true, + passwordRequired: true, + }, + anonymousSignInEnabled: false, + multiFactorConfig: mfaSmsEnabledTotpEnabledConfig, + // These test phone numbers will not be checked when running integration + // tests against the emulator suite and are ignored in auth emulator + // altogether. For more information, please refer to this section of the + // auth emulator DD: go/firebase-auth-emulator-dd#heading=h.odk06so2ydjd + testPhoneNumbers: { + '+16505551234': '019287', + '+16505550676': '985235', + }, + emailPrivacyConfig: { + enableImprovedEmailPrivacy: true, + }, + }; + const expectedUpdatedTenant: any = { + displayName: 'testTenantUpdated', + emailSignInConfig: { + enabled: false, + passwordRequired: true, + }, + anonymousSignInEnabled: false, + multiFactorConfig: mfaSmsDisabledTotpEnabledConfig, + // Test phone numbers will not be checked when running integration tests + // against emulator suite. For more information, please refer to: + // go/firebase-auth-emulator-dd#heading=h.odk06so2ydjd + testPhoneNumbers: { + '+16505551234': '123456', + }, + recaptchaConfig: { + emailPasswordEnforcementState: 'AUDIT', + managedRules: [ + { + endScore: 0.3, + action: 'BLOCK', + }, + ], + useAccountDefender: true, + }, + emailPrivacyConfig: {}, + }; + const expectedUpdatedTenant2: any = { + displayName: 'testTenantUpdated', + emailSignInConfig: { + enabled: true, + passwordRequired: false, + }, + anonymousSignInEnabled: false, + multiFactorConfig: mfaSmsEnabledTotpEnabledConfig, + smsRegionConfig: smsRegionAllowByDefaultConfig, + recaptchaConfig: { + emailPasswordEnforcementState: 'OFF', + managedRules: [ + { + endScore: 0.3, + action: 'BLOCK', + }, + ], + useAccountDefender: false, + }, + emailPrivacyConfig: {}, + }; + const expectedUpdatedTenantSmsEnabledTotpDisabled: any = { + displayName: 'testTenantUpdated', + emailSignInConfig: { + enabled: true, + passwordRequired: false, + }, + anonymousSignInEnabled: false, + multiFactorConfig: mfaSmsEnabledTotpDisabledConfig, + smsRegionConfig: smsRegionAllowByDefaultConfig, + recaptchaConfig: { + emailPasswordEnforcementState: 'OFF', + managedRules: [ + { + endScore: 0.3, + action: 'BLOCK', + }, + ], + useAccountDefender: false, + }, + emailPrivacyConfig: {}, + }; + + // https://mochajs.org/ + // Passing arrow functions (aka "lambdas") to Mocha is discouraged. + // Lambdas lexically bind this and cannot access the Mocha context. + before(function () { + /* tslint:disable:no-console */ + if (!cmdArgs.testMultiTenancy) { + // To enable, run: npm run test:integration -- --testMultiTenancy + // By default we skip multi-tenancy as it is a Google Cloud Identity Platform + // feature only and requires to be enabled via the Cloud Console. + console.log(chalk.yellow(' Skipping multi-tenancy tests.')); + this.skip(); + } + /* tslint:enable:no-console */ + }); + + // Delete test tenants at the end of test suite. + after(() => { + const promises: Array> = []; + createdTenants.forEach((tenantId) => { + promises.push( + getAuth().tenantManager().deleteTenant(tenantId) + .catch(() => {/** Ignore. */ })); + }); + return Promise.all(promises); + }); + + it('createTenant() should resolve with a new tenant', () => { + return getAuth().tenantManager().createTenant(tenantOptions) + .then((actualTenant) => { + createdTenantId = actualTenant.tenantId; + createdTenants.push(createdTenantId); + expectedCreatedTenant.tenantId = createdTenantId; + const actualTenantObj = actualTenant.toJSON(); + if (authEmulatorHost) { + // Not supported in Auth Emulator + delete (actualTenantObj as { testPhoneNumbers?: Record }).testPhoneNumbers; + delete expectedCreatedTenant.testPhoneNumbers; + } + expect(actualTenantObj).to.deep.equal(expectedCreatedTenant); + }); + }); + + it('createTenant() can enable anonymous users', async () => { + const tenant = await getAuth().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', () => { + let tenantAwareAuth: TenantAwareAuth; + let createdUserUid: string; + let lastValidSinceTime: number; + const newUserData = clone(mockUserData); + newUserData.email = generateRandomString(20).toLowerCase() + '@example.com'; + newUserData.phoneNumber = testPhoneNumber; + const importOptions: any = { + hash: { + algorithm: 'HMAC_SHA256', + key: Buffer.from('secret'), + }, + }; + const rawPassword = 'password'; + const rawSalt = 'NaCl'; + + before(function () { + if (!createdTenantId) { + this.skip(); + } else { + tenantAwareAuth = getAuth().tenantManager().authForTenant(createdTenantId); + } + }); + + // Delete test user at the end of test suite. + after(() => { + // If user successfully created, make sure it is deleted at the end of the test suite. + if (createdUserUid) { + return tenantAwareAuth.deleteUser(createdUserUid) + .catch(() => { + // Ignore error. + }); + } + }); + + it('createUser() should create a user in the expected tenant', () => { + return tenantAwareAuth.createUser(newUserData) + .then((userRecord) => { + createdUserUid = userRecord.uid; + expect(userRecord.tenantId).to.equal(createdTenantId); + expect(userRecord.email).to.equal(newUserData.email); + expect(userRecord.phoneNumber).to.equal(newUserData.phoneNumber); + }); + }); + + it('setCustomUserClaims() should set custom attributes on the tenant specific user', () => { + return tenantAwareAuth.setCustomUserClaims(createdUserUid, customClaims) + .then(() => { + return tenantAwareAuth.getUser(createdUserUid); + }) + .then((userRecord) => { + expect(userRecord.uid).to.equal(createdUserUid); + expect(userRecord.tenantId).to.equal(createdTenantId); + // Confirm custom claims set on the UserRecord. + expect(userRecord.customClaims).to.deep.equal(customClaims); + }); + }); + + it('updateUser() should update the tenant specific user', () => { + return tenantAwareAuth.updateUser(createdUserUid, { + email: updatedEmail, + phoneNumber: updatedPhone, + }) + .then((userRecord) => { + expect(userRecord.uid).to.equal(createdUserUid); + expect(userRecord.tenantId).to.equal(createdTenantId); + expect(userRecord.email).to.equal(updatedEmail); + expect(userRecord.phoneNumber).to.equal(updatedPhone); + }); + }); + + it('generateEmailVerificationLink() should generate the link for tenant specific user', () => { + // Generate email verification link to confirm it is generated in the expected + // tenant context. + return tenantAwareAuth.generateEmailVerificationLink(updatedEmail, actionCodeSettings) + .then((link) => { + // Confirm tenant ID set in link. + expect(getTenantId(link)).equal(createdTenantId); + }); + }); + + it('generatePasswordResetLink() should generate the link for tenant specific user', () => { + // Generate password reset link to confirm it is generated in the expected + // tenant context. + return tenantAwareAuth.generatePasswordResetLink(updatedEmail, actionCodeSettings) + .then((link) => { + // Confirm tenant ID set in link. + expect(getTenantId(link)).equal(createdTenantId); + }); + }); + + it('generateSignInWithEmailLink() should generate the link for tenant specific user', () => { + // Generate link for sign-in to confirm it is generated in the expected + // tenant context. + return tenantAwareAuth.generateSignInWithEmailLink(updatedEmail, actionCodeSettings) + .then((link) => { + // Confirm tenant ID set in link. + expect(getTenantId(link)).equal(createdTenantId); + }); + }); + + it('revokeRefreshTokens() should revoke the tokens for the tenant specific user', () => { + // Revoke refresh tokens. + // On revocation, tokensValidAfterTime will be updated to current time. All tokens issued + // before that time will be rejected. As the underlying backend field is rounded to the nearest + // second, we are subtracting one second. + lastValidSinceTime = new Date().getTime() - 1000; + return tenantAwareAuth.revokeRefreshTokens(createdUserUid) + .then(() => { + return tenantAwareAuth.getUser(createdUserUid); + }) + .then((userRecord) => { + expect(userRecord.tokensValidAfterTime).to.exist; + expect(new Date(userRecord.tokensValidAfterTime!).getTime()) + .to.be.greaterThan(lastValidSinceTime); + }); + }); + + it('listUsers() should list tenant specific users', () => { + return tenantAwareAuth.listUsers(100) + .then((listUsersResult) => { + // Confirm expected user returned in the list and all users returned + // belong to the expected tenant. + const allUsersBelongToTenant = + listUsersResult.users.every((user) => user.tenantId === createdTenantId); + expect(allUsersBelongToTenant).to.be.true; + const knownUserInTenant = + listUsersResult.users.some((user) => user.uid === createdUserUid); + expect(knownUserInTenant).to.be.true; + }); + }); + + it('deleteUser() should delete the tenant specific user', () => { + return tenantAwareAuth.deleteUser(createdUserUid) + .then(() => { + return tenantAwareAuth.getUser(createdUserUid) + .should.eventually.be.rejected.and.have.property('code', 'auth/user-not-found'); + }); + }); + + it('importUsers() should upload a user to the specified tenant', () => { + const currentHashKey = importOptions.hash.key.toString('utf8'); + const passwordHash = + crypto.createHmac('sha256', currentHashKey).update(rawPassword + rawSalt).digest(); + const importUserRecord: any = { + uid: createdUserUid, + email: createdUserUid + '@example.com', + passwordHash, + passwordSalt: Buffer.from(rawSalt), + }; + return tenantAwareAuth.importUsers([importUserRecord], importOptions) + .then(() => { + return tenantAwareAuth.getUser(createdUserUid); + }) + .then((userRecord) => { + // Confirm user uploaded successfully. + expect(userRecord.tenantId).to.equal(createdTenantId); + expect(userRecord.uid).to.equal(createdUserUid); + }); + }); + + it('createCustomToken() mints a JWT that can be used to sign in tenant users', async () => { + try { + clientAuth().tenantId = createdTenantId; + + const customToken = await tenantAwareAuth.createCustomToken('uid1'); + const { user } = await clientAuth().signInWithCustomToken(customToken); + expect(user).to.not.be.null; + const idToken = await user!.getIdToken(); + const token = await tenantAwareAuth.verifyIdToken(idToken); + + expect(token.uid).to.equal('uid1'); + expect(token.firebase.tenant).to.equal(createdTenantId); + } finally { + clientAuth().tenantId = null; + } + }); + }); + + // Sanity check OIDC/SAML config management API. + describe('SAML management APIs', () => { + let tenantAwareAuth: TenantAwareAuth; + const authProviderConfig = { + providerId: randomSamlProviderId(), + displayName: 'SAML_DISPLAY_NAME1', + enabled: true, + idpEntityId: 'IDP_ENTITY_ID1', + ssoURL: 'https://example.com/login1', + x509Certificates: [mocks.x509CertPairs[0].public], + rpEntityId: 'RP_ENTITY_ID1', + callbackURL: 'https://projectId.firebaseapp.com/__/auth/handler', + enableRequestSigning: true, + }; + const modifiedConfigOptions = { + displayName: 'SAML_DISPLAY_NAME3', + enabled: false, + idpEntityId: 'IDP_ENTITY_ID3', + ssoURL: 'https://example.com/login3', + x509Certificates: [mocks.x509CertPairs[1].public], + rpEntityId: 'RP_ENTITY_ID3', + callbackURL: 'https://projectId3.firebaseapp.com/__/auth/handler', + enableRequestSigning: false, + }; + + before(function () { + if (!createdTenantId) { + this.skip(); + } else { + tenantAwareAuth = getAuth().tenantManager().authForTenant(createdTenantId); + } + }); + + // Delete SAML configuration at the end of test suite. + after(() => { + if (tenantAwareAuth) { + return tenantAwareAuth.deleteProviderConfig(authProviderConfig.providerId) + .catch(() => { + // Ignore error. + }); + } + }); + + it('should support CRUD operations', function () { + // TODO(lisajian): Unskip once auth emulator supports OIDC/SAML + if (authEmulatorHost) { + return this.skip(); // Not yet supported in Auth Emulator. + } + return tenantAwareAuth.createProviderConfig(authProviderConfig) + .then((config) => { + assertDeepEqualUnordered(authProviderConfig, config); + return tenantAwareAuth.getProviderConfig(authProviderConfig.providerId); + }) + .then((config) => { + assertDeepEqualUnordered(authProviderConfig, config); + return tenantAwareAuth.updateProviderConfig( + authProviderConfig.providerId, modifiedConfigOptions); + }) + .then((config) => { + const modifiedConfig = deepExtend( + { providerId: authProviderConfig.providerId }, modifiedConfigOptions); + assertDeepEqualUnordered(modifiedConfig, config); + return tenantAwareAuth.deleteProviderConfig(authProviderConfig.providerId); + }) + .then(() => { + return tenantAwareAuth.getProviderConfig(authProviderConfig.providerId) + .should.eventually.be.rejected.and.have.property('code', 'auth/configuration-not-found'); + }); + }); + }); + + describe('OIDC management APIs', () => { + let tenantAwareAuth: TenantAwareAuth; + const authProviderConfig = { + providerId: randomOidcProviderId(), + displayName: 'OIDC_DISPLAY_NAME1', + enabled: true, + issuer: 'https://oidc.com/issuer1', + clientId: 'CLIENT_ID1', + responseType: { + idToken: true, + }, + }; + const deltaChanges = { + displayName: 'OIDC_DISPLAY_NAME3', + enabled: false, + issuer: 'https://oidc.com/issuer3', + clientId: 'CLIENT_ID3', + clientSecret: 'CLIENT_SECRET', + responseType: { + idToken: false, + 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) { + this.skip(); + } else { + tenantAwareAuth = getAuth().tenantManager().authForTenant(createdTenantId); + } + }); + + // Delete OIDC configuration at the end of test suite. + after(() => { + if (tenantAwareAuth) { + return tenantAwareAuth.deleteProviderConfig(authProviderConfig.providerId) + .catch(() => { + // Ignore error. + }); + } + }); + + it('should support CRUD operations', function () { + // TODO(lisajian): Unskip once auth emulator supports OIDC/SAML + if (authEmulatorHost) { + return this.skip(); // Not yet supported in Auth Emulator. + } + return tenantAwareAuth.createProviderConfig(authProviderConfig) + .then((config) => { + assertDeepEqualUnordered(authProviderConfig, config); + return tenantAwareAuth.getProviderConfig(authProviderConfig.providerId); + }) + .then((config) => { + assertDeepEqualUnordered(authProviderConfig, config); + return tenantAwareAuth.updateProviderConfig( + authProviderConfig.providerId, deltaChanges); + }) + .then((config) => { + assertDeepEqualUnordered(modifiedConfigOptions, config); + return tenantAwareAuth.deleteProviderConfig(authProviderConfig.providerId); + }) + .then(() => { + return tenantAwareAuth.getProviderConfig(authProviderConfig.providerId) + .should.eventually.be.rejected.and.have.property('code', 'auth/configuration-not-found'); + }); + }); + }); + + it('getTenant() should resolve with expected tenant', () => { + return getAuth().tenantManager().getTenant(createdTenantId) + .then((actualTenant) => { + const actualTenantObj = actualTenant.toJSON(); + if (authEmulatorHost) { + // Not supported in Auth Emulator + delete (actualTenantObj as { testPhoneNumbers?: Record }).testPhoneNumbers; + delete expectedCreatedTenant.testPhoneNumbers; + } + expect(actualTenantObj).to.deep.equal(expectedCreatedTenant); + }); + }); + + it('updateTenant() should resolve with the updated tenant', () => { + expectedUpdatedTenant.tenantId = createdTenantId; + expectedUpdatedTenant2.tenantId = createdTenantId; + const updatedOptions: UpdateTenantRequest = { + displayName: expectedUpdatedTenant.displayName, + emailSignInConfig: { + enabled: false, + }, + multiFactorConfig: deepCopy(expectedUpdatedTenant.multiFactorConfig), + testPhoneNumbers: deepCopy(expectedUpdatedTenant.testPhoneNumbers), + recaptchaConfig: deepCopy(expectedUpdatedTenant.recaptchaConfig), + emailPrivacyConfig: { enableImprovedEmailPrivacy: false }, + }; + const updatedOptions2: UpdateTenantRequest = { + emailSignInConfig: { + enabled: true, + passwordRequired: false, + }, + multiFactorConfig: deepCopy(expectedUpdatedTenant2.multiFactorConfig), + // Test clearing of phone numbers. + testPhoneNumbers: null, + smsRegionConfig: deepCopy(expectedUpdatedTenant2.smsRegionConfig), + recaptchaConfig: deepCopy(expectedUpdatedTenant2.recaptchaConfig), + emailPrivacyConfig: { + enableImprovedEmailPrivacy: false, + }, + }; + if (authEmulatorHost) { + return getAuth().tenantManager().updateTenant(createdTenantId, updatedOptions) + .then((actualTenant) => { + const actualTenantObj = actualTenant.toJSON(); + // Not supported in Auth Emulator + delete (actualTenantObj as { testPhoneNumbers?: Record }).testPhoneNumbers; + delete expectedUpdatedTenant.testPhoneNumbers; + expect(actualTenantObj).to.deep.equal(expectedUpdatedTenant); + return getAuth().tenantManager().updateTenant(createdTenantId, updatedOptions2); + }) + .then((actualTenant) => { + const actualTenantObj = actualTenant.toJSON(); + // Not supported in Auth Emulator + delete (actualTenantObj as { testPhoneNumbers?: Record }).testPhoneNumbers; + delete expectedUpdatedTenant2.testPhoneNumbers; + expect(actualTenantObj).to.deep.equal(expectedUpdatedTenant2); + }); + } + return getAuth().tenantManager().updateTenant(createdTenantId, updatedOptions) + .then((actualTenant) => { + expect(actualTenant.toJSON()).to.deep.equal(expectedUpdatedTenant); + return getAuth().tenantManager().updateTenant(createdTenantId, updatedOptions2); + }) + .then((actualTenant) => { + // response from backend ignores account defender status is recaptcha status is OFF. + const expectedUpdatedTenantCopy = deepCopy(expectedUpdatedTenant2); + delete expectedUpdatedTenantCopy.recaptchaConfig.useAccountDefender; + expect(actualTenant.toJSON()).to.deep.equal(expectedUpdatedTenantCopy); + }); + }); + + it('updateTenant() should not update tenant when SMS region config is undefined', () => { + expectedUpdatedTenant.tenantId = createdTenantId; + const updatedOptions2: UpdateTenantRequest = { + displayName: expectedUpdatedTenant2.displayName, + smsRegionConfig: undefined, + }; + if (authEmulatorHost) { + return getAuth().tenantManager().updateTenant(createdTenantId, updatedOptions2) + .then((actualTenant) => { + const actualTenantObj = actualTenant.toJSON(); + // Not supported in Auth Emulator + delete (actualTenantObj as { testPhoneNumbers?: Record }).testPhoneNumbers; + delete expectedUpdatedTenant2.testPhoneNumbers; + expect(actualTenantObj).to.deep.equal(expectedUpdatedTenant2); + }); + } + return getAuth().tenantManager().updateTenant(createdTenantId, updatedOptions2) + .then((actualTenant) => { + // response from backend ignores account defender status is recaptcha status is OFF. + const expectedUpdatedTenantCopy = deepCopy(expectedUpdatedTenant2); + delete expectedUpdatedTenantCopy.recaptchaConfig.useAccountDefender; + expect(actualTenant.toJSON()).to.deep.equal(expectedUpdatedTenantCopy); + }); + }); + + it('updateTenant() should not update MFA-related config of tenant when MultiFactorConfig is undefined', () => { + expectedUpdatedTenant.tenantId = createdTenantId; + const updateRequestNoMfaConfig: UpdateTenantRequest = { + displayName: expectedUpdatedTenant2.displayName, + multiFactorConfig: undefined, + }; + if (authEmulatorHost) { + return getAuth().tenantManager().updateTenant(createdTenantId, updateRequestNoMfaConfig) + .then((actualTenant) => { + const actualTenantObj = actualTenant.toJSON(); + // Configuring test phone numbers are not supported in Auth Emulator + delete (actualTenantObj as { testPhoneNumbers?: Record }).testPhoneNumbers; + delete expectedUpdatedTenant2.testPhoneNumbers; + expect(actualTenantObj).to.deep.equal(expectedUpdatedTenant2); + }); + } + return getAuth().tenantManager().updateTenant(createdTenantId, updateRequestNoMfaConfig) + }); + + it('updateTenant() should not update tenant reCAPTCHA config is undefined', () => { + expectedUpdatedTenant.tenantId = createdTenantId; + const updatedOptions2: UpdateTenantRequest = { + displayName: expectedUpdatedTenant2.displayName, + recaptchaConfig: undefined, + }; + if (authEmulatorHost) { + return getAuth().tenantManager().updateTenant(createdTenantId, updatedOptions2) + .then((actualTenant) => { + const actualTenantObj = actualTenant.toJSON(); + // Not supported in Auth Emulator + delete (actualTenantObj as { testPhoneNumbers?: Record }).testPhoneNumbers; + delete expectedUpdatedTenant2.testPhoneNumbers; + expect(actualTenantObj).to.deep.equal(expectedUpdatedTenant2); + }); + } + return getAuth().tenantManager().updateTenant(createdTenantId, updatedOptions2) + .then((actualTenant) => { + // response from backend ignores account defender status is recaptcha status is OFF. + const expectedUpdatedTenantCopy = deepCopy(expectedUpdatedTenant2); + delete expectedUpdatedTenantCopy.recaptchaConfig.useAccountDefender; + expect(actualTenant.toJSON()).to.deep.equal(expectedUpdatedTenantCopy); + }); + }); + it('updateTenant() should not disable SMS MFA when TOTP is disabled', () => { + expectedUpdatedTenantSmsEnabledTotpDisabled.tenantId = createdTenantId; + const updateRequestSMSEnabledTOTPDisabled: UpdateTenantRequest = { + displayName: expectedUpdatedTenant2.displayName, + multiFactorConfig: { + state: 'ENABLED', + factorIds: ['phone'], + providerConfigs: [ + { + state: 'DISABLED', + totpProviderConfig: {} + }, + ], + }, + }; + if (authEmulatorHost) { + return getAuth().tenantManager().updateTenant(createdTenantId, updateRequestSMSEnabledTOTPDisabled) + .then((actualTenant) => { + const actualTenantObj = actualTenant.toJSON(); + // Configuring test phone numbers are not supported in Auth Emulator + delete (actualTenantObj as { testPhoneNumbers?: Record }).testPhoneNumbers; + delete expectedUpdatedTenantSmsEnabledTotpDisabled.testPhoneNumbers; + expect(actualTenantObj).to.deep.equal(expectedUpdatedTenantSmsEnabledTotpDisabled); + }); + } + return getAuth().tenantManager().updateTenant(createdTenantId, updateRequestSMSEnabledTOTPDisabled) + .then((actualTenant) => { + // response from backend ignores account defender status is recaptcha status is OFF. + const expectedUpdatedTenantCopy = deepCopy(expectedUpdatedTenantSmsEnabledTotpDisabled); + delete expectedUpdatedTenantCopy.recaptchaConfig.useAccountDefender; + expect(actualTenant.toJSON()).to.deep.equal(expectedUpdatedTenantCopy); + }); + }); + + it('updateTenant() should be able to enable/disable anon provider', async () => { + const tenantManager = getAuth().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('updateTenant() should enforce password policies on tenant', () => { + const passwordConfig: PasswordPolicyConfig = { + enforcementState: 'ENFORCE', + forceUpgradeOnSignin: true, + constraints: { + requireLowercase: true, + requireNonAlphanumeric: true, + requireNumeric: true, + requireUppercase: true, + minLength: 6, + maxLength: 30, + }, + }; + return getAuth().tenantManager().updateTenant(createdTenantId, { passwordPolicyConfig: passwordConfig }) + .then((actualTenant) => { + expect(deepCopy(actualTenant.passwordPolicyConfig)).to.deep.equal(passwordConfig as any); + }); + }); + + it('updateTenant() should disable password policies on tenant', () => { + const passwordConfig: PasswordPolicyConfig = { + enforcementState: 'OFF', + }; + const expectedPasswordConfig: any = { + enforcementState: 'OFF', + forceUpgradeOnSignin: false, + constraints: { + requireLowercase: false, + requireNonAlphanumeric: false, + requireNumeric: false, + requireUppercase: false, + minLength: 6, + maxLength: 4096, + }, + }; + return getAuth().tenantManager().updateTenant(createdTenantId, { passwordPolicyConfig: passwordConfig }) + .then((actualTenant) => { + expect(deepCopy(actualTenant.passwordPolicyConfig)).to.deep.equal(expectedPasswordConfig); + }); + }); + + it('listTenants() should resolve with expected number of tenants', () => { + const allTenantIds: string[] = []; + const tenantOptions2 = deepCopy(tenantOptions); + tenantOptions2.displayName = 'testTenant2'; + const listAllTenantIds = (tenantIds: string[], nextPageToken?: string): Promise => { + return getAuth().tenantManager().listTenants(100, nextPageToken) + .then((result) => { + result.tenants.forEach((tenant) => { + tenantIds.push(tenant.tenantId); + }); + if (result.pageToken) { + return listAllTenantIds(tenantIds, result.pageToken); + } + }); + }; + return getAuth().tenantManager().createTenant(tenantOptions2) + .then((actualTenant) => { + createdTenants.push(actualTenant.tenantId); + // Test listTenants returns the expected tenants. + return listAllTenantIds(allTenantIds); + }) + .then(() => { + // All created tenants should be in the list of tenants. + createdTenants.forEach((tenantId) => { + expect(allTenantIds).to.contain(tenantId); + }); + }); + }); + + it('deleteTenant() should successfully delete the provided tenant', () => { + const allTenantIds: string[] = []; + const listAllTenantIds = (tenantIds: string[], nextPageToken?: string): Promise => { + return getAuth().tenantManager().listTenants(100, nextPageToken) + .then((result) => { + result.tenants.forEach((tenant) => { + tenantIds.push(tenant.tenantId); + }); + if (result.pageToken) { + return listAllTenantIds(tenantIds, result.pageToken); + } + }); + }; + + return getAuth().tenantManager().deleteTenant(createdTenantId) + .then(() => { + // Use listTenants() instead of getTenant() to check that the tenant + // is no longer present, because Auth Emulator implicitly creates the + // tenant in getTenant() when it is not found + return listAllTenantIds(allTenantIds); + }) + .then(() => { + expect(allTenantIds).to.not.contain(createdTenantId); + }); + }); + }); + + describe('SAML configuration operations', () => { + const authProviderConfig1 = { + providerId: randomSamlProviderId(), + displayName: 'SAML_DISPLAY_NAME1', + enabled: true, + idpEntityId: 'IDP_ENTITY_ID1', + ssoURL: 'https://example.com/login1', + x509Certificates: [mocks.x509CertPairs[0].public], + rpEntityId: 'RP_ENTITY_ID1', + callbackURL: 'https://projectId.firebaseapp.com/__/auth/handler', + enableRequestSigning: true, + }; + const authProviderConfig2 = { + providerId: randomSamlProviderId(), + displayName: 'SAML_DISPLAY_NAME2', + enabled: true, + idpEntityId: 'IDP_ENTITY_ID2', + ssoURL: 'https://example.com/login2', + x509Certificates: [mocks.x509CertPairs[1].public], + rpEntityId: 'RP_ENTITY_ID2', + callbackURL: 'https://projectId.firebaseapp.com/__/auth/handler', + enableRequestSigning: true, + }; + + const removeTempConfigs = (): Promise => { + return Promise.all([ + getAuth().deleteProviderConfig(authProviderConfig1.providerId).catch(() => {/* empty */ }), + getAuth().deleteProviderConfig(authProviderConfig2.providerId).catch(() => {/* empty */ }), + ]); + }; + + // Clean up temp configurations used for test. + before(function () { + if (authEmulatorHost) { + return this.skip(); // Not implemented. + } + return removeTempConfigs().then(() => getAuth().createProviderConfig(authProviderConfig1)); + }); + + after(() => { + return removeTempConfigs(); + }); + + it('createProviderConfig() successfully creates a SAML config', () => { + return getAuth().createProviderConfig(authProviderConfig2) + .then((config) => { + assertDeepEqualUnordered(authProviderConfig2, config); + }); + }); + + it('getProviderConfig() successfully returns the expected SAML config', () => { + return getAuth().getProviderConfig(authProviderConfig1.providerId) + .then((config) => { + assertDeepEqualUnordered(authProviderConfig1, config); + }); + }); + + it('listProviderConfig() successfully returns the list of SAML providers', () => { + const configs: AuthProviderConfig[] = []; + const listProviders: any = (type: 'saml' | 'oidc', maxResults?: number, pageToken?: string) => { + return getAuth().listProviderConfigs({ type, maxResults, pageToken }) + .then((result) => { + result.providerConfigs.forEach((config: AuthProviderConfig) => { + configs.push(config); + }); + if (result.pageToken) { + return listProviders(type, maxResults, result.pageToken); + } + }); + }; + // In case the project already has existing providers, list all configurations and then + // check the 2 test configs are available. + return listProviders('saml', 1) + .then(() => { + let index1 = 0; + let index2 = 0; + for (let i = 0; i < configs.length; i++) { + if (configs[i].providerId === authProviderConfig1.providerId) { + index1 = i; + } else if (configs[i].providerId === authProviderConfig2.providerId) { + index2 = i; + } + } + assertDeepEqualUnordered(authProviderConfig1, configs[index1]); + assertDeepEqualUnordered(authProviderConfig2, configs[index2]); + }); + }); + + it('updateProviderConfig() successfully overwrites a SAML config', () => { + const modifiedConfigOptions = { + displayName: 'SAML_DISPLAY_NAME3', + enabled: false, + idpEntityId: 'IDP_ENTITY_ID3', + ssoURL: 'https://example.com/login3', + x509Certificates: [mocks.x509CertPairs[1].public], + rpEntityId: 'RP_ENTITY_ID3', + callbackURL: 'https://projectId3.firebaseapp.com/__/auth/handler', + enableRequestSigning: false, + }; + return getAuth().updateProviderConfig(authProviderConfig1.providerId, modifiedConfigOptions) + .then((config) => { + const modifiedConfig = deepExtend( + { providerId: authProviderConfig1.providerId }, modifiedConfigOptions); + assertDeepEqualUnordered(modifiedConfig, config); + }); + }); + + it('updateProviderConfig() successfully partially modifies a SAML config', () => { + const deltaChanges = { + displayName: 'SAML_DISPLAY_NAME4', + x509Certificates: [mocks.x509CertPairs[0].public], + // Note, currently backend has a bug where error is thrown when callbackURL is not + // passed event though it is not required. Fix is on the way. + callbackURL: 'https://projectId3.firebaseapp.com/__/auth/handler', + rpEntityId: 'RP_ENTITY_ID4', + }; + // Only above fields should be modified. + const modifiedConfigOptions = { + displayName: 'SAML_DISPLAY_NAME4', + enabled: false, + idpEntityId: 'IDP_ENTITY_ID3', + ssoURL: 'https://example.com/login3', + x509Certificates: [mocks.x509CertPairs[0].public], + rpEntityId: 'RP_ENTITY_ID4', + callbackURL: 'https://projectId3.firebaseapp.com/__/auth/handler', + enableRequestSigning: false, + }; + return getAuth().updateProviderConfig(authProviderConfig1.providerId, deltaChanges) + .then((config) => { + const modifiedConfig = deepExtend( + { providerId: authProviderConfig1.providerId }, modifiedConfigOptions); + assertDeepEqualUnordered(modifiedConfig, config); + }); + }); + + it('deleteProviderConfig() successfully deletes an existing SAML config', () => { + return getAuth().deleteProviderConfig(authProviderConfig1.providerId).then(() => { + return getAuth().getProviderConfig(authProviderConfig1.providerId) + .should.eventually.be.rejected.and.have.property('code', 'auth/configuration-not-found'); + }); + }); + }); + + describe('OIDC configuration operations', () => { + const authProviderConfig1 = { + providerId: randomOidcProviderId(), + displayName: 'OIDC_DISPLAY_NAME1', + enabled: true, + issuer: 'https://oidc.com/issuer1', + clientId: 'CLIENT_ID1', + responseType: { + idToken: true, + }, + }; + const authProviderConfig2 = { + providerId: randomOidcProviderId(), + displayName: 'OIDC_DISPLAY_NAME2', + enabled: true, + issuer: 'https://oidc.com/issuer2', + clientId: 'CLIENT_ID2', + clientSecret: 'CLIENT_SECRET', + responseType: { + code: true, + }, + }; + + const removeTempConfigs = (): Promise => { + return Promise.all([ + getAuth().deleteProviderConfig(authProviderConfig1.providerId).catch(() => {/* empty */ }), + getAuth().deleteProviderConfig(authProviderConfig2.providerId).catch(() => {/* empty */ }), + ]); + }; + + // Clean up temp configurations used for test. + before(function () { + if (authEmulatorHost) { + return this.skip(); // Not implemented. + } + return removeTempConfigs().then(() => getAuth().createProviderConfig(authProviderConfig1)); + }); + + after(() => { + return removeTempConfigs(); + }); + + it('createProviderConfig() successfully creates an OIDC config', () => { + return getAuth().createProviderConfig(authProviderConfig2) + .then((config) => { + assertDeepEqualUnordered(authProviderConfig2, config); + }); + }); + + it('getProviderConfig() successfully returns the expected OIDC config', () => { + return getAuth().getProviderConfig(authProviderConfig1.providerId) + .then((config) => { + assertDeepEqualUnordered(authProviderConfig1, config); + }); + }); + + it('listProviderConfig() successfully returns the list of OIDC providers', () => { + const configs: AuthProviderConfig[] = []; + const listProviders: any = (type: 'saml' | 'oidc', maxResults?: number, pageToken?: string) => { + return getAuth().listProviderConfigs({ type, maxResults, pageToken }) + .then((result) => { + result.providerConfigs.forEach((config: AuthProviderConfig) => { + configs.push(config); + }); + if (result.pageToken) { + return listProviders(type, maxResults, result.pageToken); + } + }); + }; + // In case the project already has existing providers, list all configurations and then + // check the 2 test configs are available. + return listProviders('oidc', 1) + .then(() => { + let index1 = 0; + let index2 = 0; + for (let i = 0; i < configs.length; i++) { + if (configs[i].providerId === authProviderConfig1.providerId) { + index1 = i; + } else if (configs[i].providerId === authProviderConfig2.providerId) { + index2 = i; + } + } + assertDeepEqualUnordered(authProviderConfig1, configs[index1]); + assertDeepEqualUnordered(authProviderConfig2, configs[index2]); + }); + }); + + 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 getAuth().updateProviderConfig(authProviderConfig1.providerId, deltaChanges) + .then((config) => { + assertDeepEqualUnordered(modifiedConfigOptions, 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, + }, + }; + return getAuth().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/issuer5', + clientId: 'CLIENT_ID5', + responseType: { + idToken: false, + code: true, + }, + }; + return getAuth().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', () => { + return getAuth().deleteProviderConfig(authProviderConfig1.providerId).then(() => { + return getAuth().getProviderConfig(authProviderConfig1.providerId) + .should.eventually.be.rejected.and.have.property('code', 'auth/configuration-not-found'); + }); + }); + }); + + it('deleteUser() deletes the user with the given UID', () => { + return Promise.all([ + getAuth().deleteUser(newUserUid), + getAuth().deleteUser(uidFromCreateUserWithoutUid), + ]).should.eventually.be.fulfilled; + }); + + describe('deleteUsers()', () => { + it('deletes users', async () => { + const uid1 = await getAuth().createUser({}).then((ur) => ur.uid); + const uid2 = await getAuth().createUser({}).then((ur) => ur.uid); + const uid3 = await getAuth().createUser({}).then((ur) => ur.uid); + const ids = [{ uid: uid1 }, { uid: uid2 }, { uid: uid3 }]; + + return deleteUsersWithDelay([uid1, uid2, uid3]) + .then((deleteUsersResult) => { + expect(deleteUsersResult.successCount).to.equal(3); + expect(deleteUsersResult.failureCount).to.equal(0); + expect(deleteUsersResult.errors).to.have.length(0); + + return getAuth().getUsers(ids); + }) + .then((getUsersResult) => { + expect(getUsersResult.users).to.have.length(0); + expect(getUsersResult.notFound).to.have.deep.members(ids); + }); + }); + + it('deletes users that exist even when non-existing users also specified', async () => { + const uid1 = await getAuth().createUser({}).then((ur) => ur.uid); + const uid2 = 'uid-that-doesnt-exist'; + const ids = [{ uid: uid1 }, { uid: uid2 }]; + + return deleteUsersWithDelay([uid1, uid2]) + .then((deleteUsersResult) => { + expect(deleteUsersResult.successCount).to.equal(2); + expect(deleteUsersResult.failureCount).to.equal(0); + expect(deleteUsersResult.errors).to.have.length(0); + + return getAuth().getUsers(ids); + }) + .then((getUsersResult) => { + expect(getUsersResult.users).to.have.length(0); + expect(getUsersResult.notFound).to.have.deep.members(ids); + }); + }); + + it('is idempotent', async () => { + const uid = await getAuth().createUser({}).then((ur) => ur.uid); + + return deleteUsersWithDelay([uid]) + .then((deleteUsersResult) => { + expect(deleteUsersResult.successCount).to.equal(1); + expect(deleteUsersResult.failureCount).to.equal(0); + }) + // Delete the user again, ensuring that everything still counts as a success. + .then(() => deleteUsersWithDelay([uid])) + .then((deleteUsersResult) => { + expect(deleteUsersResult.successCount).to.equal(1); + expect(deleteUsersResult.failureCount).to.equal(0); + }); + }); + }); + + describe('createSessionCookie()', () => { + let expectedExp: number; + let expectedIat: number; + const expiresIn = 24 * 60 * 60 * 1000; + let payloadClaims: any; + let currentIdToken: string; + const uid = sessionCookieUids[0]; + const uid2 = sessionCookieUids[1]; + const uid3 = sessionCookieUids[2]; + const uid4 = sessionCookieUids[3]; + + it('creates a valid Firebase session cookie', () => { + return getAuth().createCustomToken(uid, { admin: true, groupId: '1234' }) + .then((customToken) => clientAuth().signInWithCustomToken(customToken)) + .then(({ user }) => { + expect(user).to.exist; + return user!.getIdToken(); + }) + .then((idToken) => { + currentIdToken = idToken; + return getAuth().verifyIdToken(idToken); + }).then((decodedIdTokenClaims) => { + expectedExp = Math.floor((new Date().getTime() + expiresIn) / 1000); + payloadClaims = decodedIdTokenClaims; + payloadClaims.iss = payloadClaims.iss.replace( + 'securetoken.google.com', 'session.firebase.google.com'); + delete payloadClaims.exp; + delete payloadClaims.iat; + expectedIat = Math.floor(new Date().getTime() / 1000); + // One day long session cookie. + return getAuth().createSessionCookie(currentIdToken, { expiresIn }); + }) + .then((sessionCookie) => getAuth().verifySessionCookie(sessionCookie)) + .then((decodedIdToken) => { + // Check for expected expiration with +/-5 seconds of variation. + expect(decodedIdToken.exp).to.be.within(expectedExp - 5, expectedExp + 5); + expect(decodedIdToken.iat).to.be.within(expectedIat - 5, expectedIat + 5); + // Not supported in ID token, + delete decodedIdToken.nonce; + // exp and iat may vary depending on network connection latency. + delete (decodedIdToken as any).exp; + delete (decodedIdToken as any).iat; + expect(decodedIdToken).to.deep.equal(payloadClaims); + }); + }); + + it('creates a revocable session cookie', () => { + let currentSessionCookie: string; + return getAuth().createCustomToken(uid2) + .then((customToken) => clientAuth().signInWithCustomToken(customToken)) + .then(({ user }) => { + expect(user).to.exist; + return user!.getIdToken(); + }) + .then((idToken) => { + // One day long session cookie. + return getAuth().createSessionCookie(idToken, { expiresIn }); + }) + .then((sessionCookie) => { + currentSessionCookie = sessionCookie; + return new Promise((resolve) => setTimeout(() => resolve( + getAuth().revokeRefreshTokens(uid2), + ), 1000)); + }) + .then(() => { + const verifyingSessionCookie = getAuth().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 getAuth().verifySessionCookie(currentSessionCookie, true) + .should.eventually.be.rejected.and.have.property('code', 'auth/session-cookie-revoked'); + }); + }); + + it('fails when called with a revoked ID token', () => { + return getAuth().createCustomToken(uid3, { admin: true, groupId: '1234' }) + .then((customToken) => clientAuth().signInWithCustomToken(customToken)) + .then(({ user }) => { + expect(user).to.exist; + return user!.getIdToken(); + }) + .then((idToken) => { + currentIdToken = idToken; + return new Promise((resolve) => setTimeout(() => resolve( + getAuth().revokeRefreshTokens(uid3), + ), 1000)); + }) + .then(() => { + return getAuth().createSessionCookie(currentIdToken, { expiresIn }) + .should.eventually.be.rejected.and.have.property('code', 'auth/id-token-expired'); + }); + }); + + it('fails when called with user disabled', async () => { + const expiresIn = 24 * 60 * 60 * 1000; + const customToken = await getAuth().createCustomToken(uid4, { admin: true, groupId: '1234' }); + const { user } = await clientAuth().signInWithCustomToken(customToken); + expect(user).to.exist; + + const idToken = await user!.getIdToken(); + const decodedIdTokenClaims = await getAuth().verifyIdToken(idToken); + expect(decodedIdTokenClaims.uid).to.be.equal(uid4); + + const sessionCookie = await getAuth().createSessionCookie(idToken, { expiresIn }); + const decodedIdToken = await getAuth().verifySessionCookie(sessionCookie, true); + expect(decodedIdToken.uid).to.equal(uid4); + + const userRecord = await getAuth().updateUser(uid4, { disabled: true }); + // Ensure disabled field has been updated. + expect(userRecord.uid).to.equal(uid4); + expect(userRecord.disabled).to.equal(true); + + return getAuth().createSessionCookie(idToken, { expiresIn }) + .should.eventually.be.rejected.and.have.property('code', 'auth/user-disabled'); + }); + }); + + describe('verifySessionCookie()', () => { + const uid = sessionCookieUids[0]; + it('fails when called with an invalid session cookie', () => { + return getAuth().verifySessionCookie('invalid-token') + .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); + }); + + it('fails when called with a Firebase ID token', () => { + return getAuth().createCustomToken(uid) + .then((customToken) => clientAuth().signInWithCustomToken(customToken)) + .then(({ user }) => { + expect(user).to.exist; + return user!.getIdToken(); + }) + .then((idToken) => { + return getAuth().verifySessionCookie(idToken) + .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); + }); + }); + + it('fails with checkRevoked set to true and corresponding user disabled', async () => { + const expiresIn = 24 * 60 * 60 * 1000; + const customToken = await getAuth().createCustomToken(uid, { admin: true, groupId: '1234' }); + const { user } = await clientAuth().signInWithCustomToken(customToken); + expect(user).to.exist; + + const idToken = await user!.getIdToken(); + const decodedIdTokenClaims = await getAuth().verifyIdToken(idToken); + expect(decodedIdTokenClaims.uid).to.be.equal(uid); + + const sessionCookie = await getAuth().createSessionCookie(idToken, { expiresIn }); + let decodedIdToken = await getAuth().verifySessionCookie(sessionCookie, true); + expect(decodedIdToken.uid).to.equal(uid); + + const userRecord = await getAuth().updateUser(uid, { disabled: true }); + // Ensure disabled field has been updated. + expect(userRecord.uid).to.equal(uid); + expect(userRecord.disabled).to.equal(true); + + try { + // If it is in emulator mode, a user-disabled error will be thrown. + decodedIdToken = await getAuth().verifySessionCookie(sessionCookie, false); + expect(decodedIdToken.uid).to.equal(uid); + } catch (error) { + if (authEmulatorHost) { + expect(error).to.have.property('code', 'auth/user-disabled'); + } else { + throw error; + } + } + + try { + await getAuth().verifySessionCookie(sessionCookie, true); + } catch (error) { + expect(error).to.have.property('code', 'auth/user-disabled'); + } + }); + }); + + describe('importUsers()', () => { + const randomUid = 'import_' + generateRandomString(20).toLowerCase(); + let importUserRecord: UserImportRecord; + const rawPassword = 'password'; + const rawSalt = 'NaCl'; + // Simulate a user stored using SCRYPT being migrated to Firebase Auth via importUsers. + // Obtained from https://github.com/firebase/scrypt. + const scryptHashKey = 'jxspr8Ki0RYycVU8zykbdLGjFQ3McFUH0uiiTvC8pVMXAn210wjLNmdZ' + + 'JzxUECKbm0QsEmYUSDzZvpjeJ9WmXA=='; + const scryptPasswordHash = 'V358E8LdWJXAO7muq0CufVpEOXaj8aFiC7T/rcaGieN04q/ZPJ0' + + '8WhJEHGjj9lz/2TT+/86N5VjVoc5DdBhBiw=='; + const scryptHashOptions = { + hash: { + algorithm: 'SCRYPT', + key: Buffer.from(scryptHashKey, 'base64'), + saltSeparator: Buffer.from('Bw==', 'base64'), + rounds: 8, + memoryCost: 14, + }, + }; + + afterEach(() => { + return safeDelete(randomUid); + }); + + const fixtures: UserImportTest[] = [ + { + name: 'HMAC_SHA256', + importOptions: { + hash: { + algorithm: 'HMAC_SHA256', + key: Buffer.from('secret'), + }, + } as any, + computePasswordHash: (userImportTest: UserImportTest): Buffer => { + expect(userImportTest.importOptions.hash.key).to.exist; + const currentHashKey = userImportTest.importOptions.hash.key!.toString('utf8'); + const currentRawPassword = userImportTest.rawPassword; + const currentRawSalt = userImportTest.rawSalt; + return crypto.createHmac('sha256', currentHashKey) + .update(currentRawPassword + currentRawSalt).digest(); + }, + rawPassword, + rawSalt, + }, + { + name: 'SHA256', + importOptions: { + hash: { + algorithm: 'SHA256', + rounds: 1, + }, + } as any, + computePasswordHash: (userImportTest: UserImportTest): Buffer => { + const currentRawPassword = userImportTest.rawPassword; + const currentRawSalt = userImportTest.rawSalt; + return crypto.createHash('sha256').update(currentRawSalt + currentRawPassword).digest(); + }, + rawPassword, + rawSalt, + }, + { + name: 'MD5', + importOptions: { + hash: { + algorithm: 'MD5', + rounds: 0, + }, + } as any, + computePasswordHash: (userImportTest: UserImportTest): Buffer => { + const currentRawPassword = userImportTest.rawPassword; + const currentRawSalt = userImportTest.rawSalt; + return Buffer.from(crypto.createHash('md5') + .update(currentRawSalt + currentRawPassword).digest('hex')); + }, + rawPassword, + rawSalt, + }, + { + name: 'BCRYPT', + importOptions: { + hash: { + algorithm: 'BCRYPT', + }, + } as any, + computePasswordHash: (userImportTest: UserImportTest): Buffer => { + return Buffer.from(bcrypt.hashSync(userImportTest.rawPassword, 10)); + }, + rawPassword, + }, + { + name: 'STANDARD_SCRYPT', + importOptions: { + hash: { + algorithm: 'STANDARD_SCRYPT', + memoryCost: 1024, + parallelization: 16, + blockSize: 8, + derivedKeyLength: 64, + }, + } as any, + computePasswordHash: (userImportTest: UserImportTest): Buffer => { + const currentRawPassword = userImportTest.rawPassword; + + expect(userImportTest.rawSalt).to.exist; + const currentRawSalt = userImportTest.rawSalt!; + + expect(userImportTest.importOptions.hash.memoryCost).to.exist; + const N = userImportTest.importOptions.hash.memoryCost!; + + expect(userImportTest.importOptions.hash.blockSize).to.exist; + const r = userImportTest.importOptions.hash.blockSize!; + + expect(userImportTest.importOptions.hash.parallelization).to.exist; + const p = userImportTest.importOptions.hash.parallelization!; + + expect(userImportTest.importOptions.hash.derivedKeyLength).to.exist; + const dkLen = userImportTest.importOptions.hash.derivedKeyLength!; + + return Buffer.from( + crypto.scryptSync( + currentRawPassword, + Buffer.from(currentRawSalt), + dkLen, + { + N, r, p, + })); + }, + rawPassword, + rawSalt, + }, + { + name: 'PBKDF2_SHA256', + importOptions: { + hash: { + algorithm: 'PBKDF2_SHA256', + rounds: 100000, + }, + } as any, + computePasswordHash: (userImportTest: UserImportTest): Buffer => { + const currentRawPassword = userImportTest.rawPassword; + expect(userImportTest.rawSalt).to.exist; + const currentRawSalt = userImportTest.rawSalt!; + expect(userImportTest.importOptions.hash.rounds).to.exist; + const currentRounds = userImportTest.importOptions.hash.rounds!; + return crypto.pbkdf2Sync( + currentRawPassword, currentRawSalt, currentRounds, 64, 'sha256'); + }, + rawPassword, + rawSalt, + }, + { + name: 'SCRYPT', + importOptions: scryptHashOptions as any, + computePasswordHash: (): Buffer => { + return Buffer.from(scryptPasswordHash, 'base64'); + }, + rawPassword, + rawSalt, + }, + ]; + + fixtures.forEach((fixture) => { + 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', + }; + importUserRecord.passwordHash = fixture.computePasswordHash(fixture); + if (typeof fixture.rawSalt !== 'undefined') { + importUserRecord.passwordSalt = Buffer.from(fixture.rawSalt); + } + return testImportAndSignInUser( + importUserRecord, fixture.importOptions, fixture.rawPassword) + .should.eventually.be.fulfilled; + + }); + }); + + it('successfully imports users with multiple OAuth providers', () => { + const uid = randomUid; + const email = uid + '@example.com'; + const now = new Date(1476235905000).toUTCString(); + const photoURL = 'http://www.example.com/' + uid + '/photo.png'; + importUserRecord = { + uid, + email, + emailVerified: true, + displayName: 'Test User', + photoURL, + phoneNumber: '+15554446666', + disabled: false, + customClaims: { admin: true }, + metadata: { + lastSignInTime: now, + creationTime: now, + // TODO(rsgowman): Enable once importing users supports lastRefreshTime + //lastRefreshTime: now, + }, + providerData: [ + { + uid: uid + '-facebook', + displayName: 'Facebook User', + email, + photoURL: photoURL + '?providerId=facebook.com', + providerId: 'facebook.com', + }, + { + uid: uid + '-twitter', + displayName: 'Twitter User', + photoURL: photoURL + '?providerId=twitter.com', + providerId: 'twitter.com', + }, + ], + }; + uids.push(importUserRecord.uid); + return getAuth().importUsers([importUserRecord]) + .then((result) => { + expect(result.failureCount).to.equal(0); + expect(result.successCount).to.equal(1); + expect(result.errors.length).to.equal(0); + return getAuth().getUser(uid); + }).then((userRecord) => { + // The phone number provider will be appended to the list of accounts. + importUserRecord.providerData?.push({ + uid: importUserRecord.phoneNumber!, + providerId: 'phone', + phoneNumber: importUserRecord.phoneNumber!, + }); + // The lastRefreshTime should be set to null + type Writable = { + -readonly [k in keyof UserMetadata]: UserMetadata[k]; + }; + (importUserRecord.metadata as Writable).lastRefreshTime = null; + const actualUserRecord: { [key: string]: any } = userRecord.toJSON(); + for (const key of Object.keys(importUserRecord)) { + expect(JSON.stringify(actualUserRecord[key])) + .to.be.equal(JSON.stringify((importUserRecord as any)[key])); + } + }); + }); + + 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(); + const enrolledFactors: UpdatePhoneMultiFactorInfoRequest[] = [ + { + uid: 'mfaUid1', + phoneNumber: '+16505550001', + displayName: 'Work phone number', + factorId: 'phone', + enrollmentTime: now, + }, + { + uid: 'mfaUid2', + phoneNumber: '+16505550002', + displayName: 'Personal phone number', + factorId: 'phone', + enrollmentTime: now, + }, + ]; + + importUserRecord = { + uid, + email, + emailVerified: true, + displayName: 'Test User', + disabled: false, + metadata: { + lastSignInTime: now, + creationTime: now, + }, + providerData: [ + { + uid: uid + '-facebook', + displayName: 'Facebook User', + email, + providerId: 'facebook.com', + }, + ], + multiFactor: { + enrolledFactors, + }, + }; + uids.push(importUserRecord.uid); + + return getAuth().importUsers([importUserRecord]) + .then((result) => { + expect(result.failureCount).to.equal(0); + expect(result.successCount).to.equal(1); + expect(result.errors.length).to.equal(0); + return getAuth().getUser(uid); + }).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(importUserRecord.multiFactor?.enrolledFactors); + }).should.eventually.be.fulfilled; + }); + + it('fails when invalid users are provided', () => { + const users = [ + { uid: generateRandomString(20).toLowerCase(), email: 'invalid' }, + { uid: generateRandomString(20).toLowerCase(), emailVerified: 'invalid' } as any, + ]; + return getAuth().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-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 getAuth().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-user-import'); + }); + }); + }); +}); + +/** + * Imports the provided user record with the specified hashing options and then + * validates the import was successful by signing in to the imported account using + * the corresponding plain text password. + * @param importUserRecord The user record to import. + * @param importOptions The import hashing options. + * @param rawPassword The plain unhashed password string. + * @retunr A promise that resolved on success. + */ +function testImportAndSignInUser( + importUserRecord: UserImportRecord, + importOptions: any, + rawPassword: string): Promise { + const users = [importUserRecord]; + // Import the user record. + return getAuth().importUsers(users, importOptions) + .then((result) => { + // Verify the import result. + expect(result.failureCount).to.equal(0); + expect(result.successCount).to.equal(1); + expect(result.errors.length).to.equal(0); + // Sign in with an email and password to the imported account. + return clientAuth().signInWithEmailAndPassword(users[0].email!, rawPassword); + }) + .then(({ user }) => { + // Confirm successful sign-in. + expect(user).to.exist; + expect(user!.email).to.equal(users[0].email); + expect(user!.providerData[0]).to.exist; + expect(user!.providerData[0]!.providerId).to.equal('password'); + }); +} + +/** + * Helper function that deletes the user with the specified phone number + * if it exists. + * @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 { + return getAuth().getUserByPhoneNumber(phoneNumber) + .then((userRecord) => { + return safeDelete(userRecord.uid); + }) + .catch((error) => { + // Suppress user not found error. + if (error.code !== 'auth/user-not-found') { + throw error; + } + }); +} + +/** + * Runs cleanup routine that could affect outcome of tests and removes any + * intermediate users created. + * + * @return A promise that resolves when test preparations are ready. + */ +function cleanup(): Promise { + // Delete any existing users that could affect the test outcome. + const promises: Array> = [ + deletePhoneNumberUser(testPhoneNumber), + deletePhoneNumberUser(testPhoneNumber2), + deletePhoneNumberUser(nonexistentPhoneNumber), + deletePhoneNumberUser(updatedPhone), + ]; + // Delete users created for session cookie tests. + sessionCookieUids.forEach((uid) => uids.push(uid)); + // Delete list of users for testing listUsers. + uids.forEach((uid) => { + // Use safeDelete to avoid getting throttled. + promises.push(safeDelete(uid)); + }); + return Promise.all(promises); +} + +/** + * Returns the action code corresponding to the link. + * + * @param link The link to parse for the action code. + * @return The link's corresponding action code. + */ +function getActionCode(link: string): string { + const parsedUrl = new url.URL(link); + const oobCode = parsedUrl.searchParams.get('oobCode'); + expect(oobCode).to.exist; + return oobCode!; +} + +/** + * Returns the continue URL corresponding to the link. + * + * @param link The link to parse for the continue URL. + * @return The link's corresponding continue URL. + */ +function getContinueUrl(link: string): string { + const parsedUrl = new url.URL(link); + const continueUrl = parsedUrl.searchParams.get('continueUrl'); + expect(continueUrl).to.exist; + return continueUrl!; +} + +/** + * Returns the tenant ID corresponding to the link. + * + * @param link The link to parse for the tenant ID. + * @return The link's corresponding tenant ID. + */ +function getTenantId(link: string): string { + const parsedUrl = new url.URL(link); + const tenantId = parsedUrl.searchParams.get('tenantId'); + expect(tenantId).to.exist; + return tenantId!; +} + +/** + * Safely deletes a specificed user identified by uid. This API chains all delete + * requests and throttles them as the Auth backend rate limits this endpoint. + * A bulk delete API is being designed to help solve this issue. + * + * @param uid The identifier of the user to delete. + * @return A promise that resolves when delete operation resolves. + */ +function safeDelete(uid: string): Promise { + // Wait for delete queue to empty. + const deletePromise = deleteQueue + .then(() => { + return getAuth().deleteUser(uid); + }) + .catch((error) => { + // Suppress user not found error. + if (error.code !== 'auth/user-not-found') { + throw error; + } + }); + // Suppress errors in delete queue to not spill over to next item in queue. + deleteQueue = deletePromise.catch(() => { + // Do nothing. + }); + return deletePromise; +} + +/** + * Deletes the specified list of users by calling the `deleteUsers()` API. This + * API is rate limited at 1 QPS, and therefore this helper function staggers + * subsequent invocations by adding 1 second delay to each call. + * + * @param uids The list of user identifiers to delete. + * @return A promise that resolves when delete operation resolves. + */ +async function deleteUsersWithDelay(uids: string[]): Promise { + if (!authEmulatorHost) { + await new Promise((resolve) => { setTimeout(resolve, 1000); }); + } + return getAuth().deleteUsers(uids); +} + +/** + * Asserts actual object is equal to expected object while ignoring key order. + * This is useful since to.deep.equal fails when order differs. + * + * @param expected object. + * @param actual object. + */ +function assertDeepEqualUnordered(expected: { [key: string]: any }, actual: { [key: string]: any }): void { + for (const key in expected) { + if (Object.prototype.hasOwnProperty.call(expected, key)) { + expect(actual[key]) + .to.deep.equal(expected[key]); + } + } + expect(Object.keys(actual).length).to.be.equal(Object.keys(expected).length); } diff --git a/test/integration/database.spec.ts b/test/integration/database.spec.ts index 9437dceb21..40b35484eb 100644 --- a/test/integration/database.spec.ts +++ b/test/integration/database.spec.ts @@ -14,16 +14,16 @@ * limitations under the License. */ -import * as admin from '../../lib/index'; import * as chai from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; -import url = require('url'); -import {defaultApp, nullApp, nonNullApp, cmdArgs, databaseUrl} from './setup'; +import * as admin from '../../lib/index'; +import { + Database, DataSnapshot, EventType, Reference, ServerValue, getDatabase, getDatabaseWithUrl, +} from '../../lib/database/index'; +import { defaultApp, nullApp, nonNullApp, cmdArgs, databaseUrl, isEmulator } from './setup'; -/* tslint:disable:no-var-requires */ -const apiRequest = require('../../lib/utils/api-request'); +// eslint-disable-next-line @typescript-eslint/no-var-requires const chalk = require('chalk'); -/* tslint:enable:no-var-requires */ chai.should(); chai.use(chaiAsPromised); @@ -43,24 +43,24 @@ describe('admin.database', () => { } console.log(chalk.yellow(' Updating security rules to defaults.')); /* tslint:enable:no-console */ - const client = new apiRequest.SignedApiRequestHandler(defaultApp); - const dbUrl = url.parse(databaseUrl); const defaultRules = { rules : { '.read': 'auth != null', '.write': 'auth != null', }, }; - const headers = { - 'Content-Type': 'application/json', - }; - return client.sendRequest(dbUrl.host, 443, '/.settings/rules.json', - 'PUT', defaultRules, headers, 10000); + return getDatabase().setRules(defaultRules); + }); + + it('getDatabase() returns a database client', () => { + const db: Database = getDatabase(); + expect(db).to.not.be.undefined; }); it('admin.database() returns a database client', () => { - const db = admin.database(); - expect(db).to.be.instanceOf((admin.database as any).Database); + const db: admin.database.Database = admin.database(); + expect(db).to.not.be.undefined; + expect(db).to.equal(getDatabase()); }); it('admin.database.ServerValue type is defined', () => { @@ -69,37 +69,42 @@ describe('admin.database', () => { }); it('default App is not blocked by security rules', () => { - return defaultApp.database().ref('blocked').set(admin.database.ServerValue.TIMESTAMP) + return getDatabase(defaultApp).ref('blocked').set(ServerValue.TIMESTAMP) .should.eventually.be.fulfilled; }); - it('App with null auth overrides is blocked by security rules', () => { - return nullApp.database().ref('blocked').set(admin.database.ServerValue.TIMESTAMP) + 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 getDatabase(nullApp).ref('blocked').set(admin.database.ServerValue.TIMESTAMP) .should.eventually.be.rejectedWith('PERMISSION_DENIED: Permission denied'); }); it('App with non-null auth override is not blocked by security rules', () => { - return nonNullApp.database().ref('blocked').set(admin.database.ServerValue.TIMESTAMP) + return getDatabase(nonNullApp).ref('blocked').set(ServerValue.TIMESTAMP) .should.eventually.be.fulfilled; }); - describe('admin.database().ref()', () => { - let ref: admin.database.Reference; + describe('Reference', () => { + let ref: Reference; before(() => { - ref = admin.database().ref(path); + ref = getDatabase().ref(path); }); it('ref() can be called with ref', () => { - const copy = admin.database().ref(ref); - expect(copy).to.be.instanceof((admin.database as any).Reference); + const copy: Reference = getDatabase().ref(ref); expect(copy.key).to.equal(ref.key); }); it('set() completes successfully', () => { return ref.set({ success: true, - timestamp: admin.database.ServerValue.TIMESTAMP, + timestamp: ServerValue.TIMESTAMP, }).should.eventually.be.fulfilled; }); @@ -124,24 +129,29 @@ describe('admin.database', () => { }); }); - describe('app.database(url).ref()', () => { + describe('getDatabaseWithUrl()', () => { - let refWithUrl: admin.database.Reference; + let refWithUrl: Reference; before(() => { - const app = admin.app(); - refWithUrl = app.database(databaseUrl).ref(path); + refWithUrl = getDatabaseWithUrl(databaseUrl).ref(path); + }); + + it('getDatabaseWithUrl(url) returns a Database client for URL', () => { + const db: Database = getDatabaseWithUrl(databaseUrl); + expect(db).to.not.be.undefined; }); it('app.database(url) returns a Database client for URL', () => { - const db = admin.app().database(databaseUrl); - expect(db).to.be.instanceOf((admin.database as any).Database); + const db: Database = admin.app().database(databaseUrl); + expect(db).to.not.be.undefined; + expect(db).to.equal(getDatabaseWithUrl(databaseUrl)); }); it('set() completes successfully', () => { return refWithUrl.set({ success: true, - timestamp: admin.database.ServerValue.TIMESTAMP, + timestamp: ServerValue.TIMESTAMP, }).should.eventually.be.fulfilled; }); @@ -165,13 +175,34 @@ describe('admin.database', () => { return refWithUrl.remove().should.eventually.be.fulfilled; }); }); + + 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 getDatabase().getRules().then((result) => { + return expect(result).to.be.not.empty; + }); + }); + + 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 getDatabase().getRulesJSON().then((result) => { + return expect(result).to.be.not.undefined; + }); + }); }); -function addValueEventListener( - db: admin.database.Database, - callback: (s: admin.database.DataSnapshot) => any) { - // 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. - const eventType: admin.database.EventType = 'value'; +// 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.) +export function addValueEventListener( + db: Database, + callback: (s: DataSnapshot | null) => any): void { + const eventType: EventType = 'value'; db.ref().on(eventType, callback); } diff --git a/test/integration/firestore.spec.ts b/test/integration/firestore.spec.ts index 83fd603a37..9127ef2e17 100644 --- a/test/integration/firestore.spec.ts +++ b/test/integration/firestore.spec.ts @@ -14,11 +14,14 @@ * limitations under the License. */ -import * as admin from '../../lib/index'; import * as chai from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; -import {clone} from 'lodash'; -import {DocumentReference} from '@google-cloud/firestore'; +import { clone } from 'lodash'; +import * as admin from '../../lib/index'; +import { + DocumentReference, DocumentSnapshot, FieldValue, Firestore, FirestoreDataConverter, + QueryDocumentSnapshot, Timestamp, getFirestore, initializeFirestore, setLogFunction, +} from '../../lib/firestore/index'; chai.should(); chai.use(chaiAsPromised); @@ -35,22 +38,34 @@ describe('admin.firestore', () => { let reference: DocumentReference; before(() => { - reference = admin.firestore().collection('cities').doc(); + const db = getFirestore(); + reference = db.collection('cities').doc(); + }); + + it('getFirestore() returns a Firestore client', () => { + const firestore: Firestore = getFirestore(); + expect(firestore).to.not.be.undefined; + }); + + it('initializeFirestore returns a Firestore client', () => { + const firestore: Firestore = initializeFirestore(admin.app()); + expect(firestore).to.not.be.undefined; }); it('admin.firestore() returns a Firestore client', () => { - const firestore = admin.firestore(); - expect(firestore).to.be.instanceOf(admin.firestore.Firestore); + const firestore: admin.firestore.Firestore = admin.firestore(); + expect(firestore).to.not.be.undefined; + expect(firestore).to.equal(getFirestore()); }); it('app.firestore() returns a Firestore client', () => { - const firestore = admin.app().firestore(); - expect(firestore).to.be.instanceOf(admin.firestore.Firestore); + const firestore: admin.firestore.Firestore = admin.app().firestore(); + expect(firestore).to.not.be.undefined; }); it('supports basic data access', () => { return reference.set(mountainView) - .then((result) => { + .then(() => { return reference.get(); }) .then((snapshot) => { @@ -58,29 +73,34 @@ describe('admin.firestore', () => { expect(data).to.deep.equal(mountainView); return reference.delete(); }) - .then((result) => { + .then(() => { return reference.get(); }) .then((snapshot) => { expect(snapshot.exists).to.be.false; }); - }).timeout(5000); + }); - it('admin.firestore.FieldValue.serverTimestamp() provides a server-side timestamp', () => { + it('FieldValue.serverTimestamp() provides a server-side timestamp', () => { const expected: any = clone(mountainView); - expected.timestamp = admin.firestore.FieldValue.serverTimestamp(); + expected.timestamp = FieldValue.serverTimestamp(); return reference.set(expected) - .then((result) => { + .then(() => { return reference.get(); }) .then((snapshot) => { const data = snapshot.data(); - expect(data.timestamp).is.not.null; - expect(data.timestamp instanceof Date).is.true; + expect(data).to.exist; + expect(data!.timestamp).is.not.null; + expect(data!.timestamp).to.be.instanceOf(Timestamp); return reference.delete(); }) .should.eventually.be.fulfilled; - }).timeout(5000); + }); + + it('admin.firestore.CollectionReference type is defined', () => { + expect(typeof admin.firestore.CollectionReference).to.be.not.undefined; + }); it('admin.firestore.FieldPath type is defined', () => { expect(typeof admin.firestore.FieldPath).to.be.not.undefined; @@ -90,43 +110,98 @@ describe('admin.firestore', () => { expect(typeof admin.firestore.FieldValue).to.be.not.undefined; }); + it('admin.firestore.Filter type is defined', () => { + expect(typeof admin.firestore.Filter).to.be.not.undefined; + }); + it('admin.firestore.GeoPoint type is defined', () => { expect(typeof admin.firestore.GeoPoint).to.be.not.undefined; }); + it('admin.firestore.Timestamp type is defined', () => { + const now = admin.firestore.Timestamp.now(); + expect(typeof now.seconds).to.equal('number'); + expect(typeof now.nanoseconds).to.equal('number'); + }); + + it('admin.firestore.WriteBatch type is defined', () => { + expect(typeof admin.firestore.WriteBatch).to.be.not.undefined; + }); + + it('admin.firestore.WriteResult type is defined', () => { + 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: FirestoreDataConverter = { + toFirestore: (city: City) => { + return { + name: city.localId, + population: city.people, + }; + }, + fromFirestore: (snap: QueryDocumentSnapshot) => { + return new City(snap.data().name, snap.data().population); + } + }; + + const expected: City = new City('Sunnyvale', 153185); + const refWithConverter: DocumentReference = getFirestore() + .collection('cities') + .doc() + .withConverter(converter); + return refWithConverter.set(expected) + .then(() => { + return refWithConverter.get(); + }) + .then((snapshot: 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(); + const source = getFirestore().collection('cities').doc(); + const target = getFirestore().collection('cities').doc(); return source.set(mountainView) - .then((result) => { - return target.set({name: 'Palo Alto', sisterCity: source}); + .then(() => { + return target.set({ name: 'Palo Alto', sisterCity: source }); }) - .then((result) => { + .then(() => { return target.get(); }) .then((snapshot) => { const data = snapshot.data(); - expect(data.sisterCity.path).to.deep.equal(source.path); + expect(data).to.exist; + expect(data!.sisterCity.path).to.deep.equal(source.path); const promises = []; promises.push(source.delete()); promises.push(target.delete()); return Promise.all(promises); }) .should.eventually.be.fulfilled; - }).timeout(5000); + }); - it('admin.firestore.setLogFunction() enables logging for the Firestore module', () => { - const logs = []; - const source = admin.firestore().collection('cities').doc(); - admin.firestore.setLogFunction((log) => { + it('setLogFunction() enables logging for the Firestore module', () => { + const logs: string[] = []; + const source = getFirestore().collection('cities').doc(); + setLogFunction((log) => { logs.push(log); }); - return source.set({name: 'San Francisco'}) - .then((result) => { + return source.set({ name: 'San Francisco' }) + .then(() => { return source.delete(); }) - .then((result) => { + .then(() => { expect(logs.length).greaterThan(0); }); - }).timeout(5000); + }); }); + +class City { + constructor(readonly localId: string, readonly people: number) { } +} diff --git a/test/integration/functions.spec.ts b/test/integration/functions.spec.ts new file mode 100644 index 0000000000..9b1f5277f9 --- /dev/null +++ b/test/integration/functions.spec.ts @@ -0,0 +1,35 @@ +/*! + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as chai from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import { getFunctions } from '../../lib/functions/index'; + +chai.should(); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +describe('getFunctions()', () => { + + describe('taskQueue()', () => { + it('successfully returns a taskQueue', () => { + const factorizeQueue = getFunctions().taskQueue('queue-name'); + expect(factorizeQueue).to.be.not.undefined; + expect(typeof factorizeQueue.enqueue).to.equal('function'); + }); + }); +}); diff --git a/test/integration/installations.spec.ts b/test/integration/installations.spec.ts new file mode 100644 index 0000000000..98eb3ad72a --- /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 { getInstallations } from '../../lib/installations/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 getInstallations().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..2155205990 100644 --- a/test/integration/instance-id.spec.ts +++ b/test/integration/instance-id.spec.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import * as admin from '../../lib/index'; import * as chai from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; +import { getInstanceId } from '../../lib/instance-id/index'; chai.should(); chai.use(chaiAsPromised); @@ -24,8 +24,8 @@ chai.use(chaiAsPromised); describe('admin.instanceId', () => { it('deleteInstanceId() fails when called with fictive-ID0 instance ID', () => { // instance ids have to conform to /[cdef][A-Za-z0-9_-]{9}[AEIMQUYcgkosw048]/ - return admin.instanceId().deleteInstanceId('fictive-ID0') + return getInstanceId().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/integration/machine-learning.spec.ts b/test/integration/machine-learning.spec.ts new file mode 100644 index 0000000000..b44ebdc1e2 --- /dev/null +++ b/test/integration/machine-learning.spec.ts @@ -0,0 +1,505 @@ +/*! + * 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 path = require('path'); +import * as chai from 'chai'; +import { Bucket } from '@google-cloud/storage'; +import { getStorage } from '../../lib/storage/index'; +import { + GcsTfliteModelOptions, Model, ModelOptions, getMachineLearning, +} from '../../lib/machine-learning/index'; + +const expect = chai.expect; + +describe('admin.machineLearning', () => { + + const modelsToDelete: string[] = []; + + function scheduleForDelete(model: Model): void { + modelsToDelete.push(model.modelId); + } + + function unscheduleForDelete(model: Model): void { + modelsToDelete.splice(modelsToDelete.indexOf(model.modelId), 1); + } + + function deleteTempModels(): Promise { + const promises: Array> = []; + modelsToDelete.forEach((modelId) => { + promises.push(getMachineLearning().deleteModel(modelId)); + }); + modelsToDelete.splice(0, modelsToDelete.length); // Clear out the array. + return Promise.all(promises); + } + + function createTemporaryModel(options?: ModelOptions): Promise { + let modelOptions: ModelOptions = { + displayName: 'nodejs_integration_temp_model', + }; + if (options) { + modelOptions = options; + } + return getMachineLearning().createModel(modelOptions) + .then((model) => { + scheduleForDelete(model); + return model; + }); + } + + function uploadModelToGcs(localFileName: string, gcsFileName: string): Promise { + const bucket: Bucket = getStorage().bucket(); + const tfliteFileName = path.join(__dirname, `../resources/${localFileName}`); + return bucket.upload(tfliteFileName, { destination: gcsFileName }) + .then(() => { + return `gs://${bucket.name}/${gcsFileName}`; + }); + } + + afterEach(() => { + return deleteTempModels(); + }); + + describe('createModel()', () => { + it('creates a new Model without ModelFormat', () => { + const modelOptions: ModelOptions = { + displayName: 'node-integ-test-create-1', + tags: ['tag123', 'tag345'] + }; + return getMachineLearning().createModel(modelOptions) + .then((model) => { + scheduleForDelete(model); + verifyModel(model, modelOptions); + }); + }); + + it('creates a new Model with valid GCS TFLite ModelFormat', () => { + const modelOptions: ModelOptions = { + displayName: 'node-integ-test-create-2', + tags: ['tag234', 'tag456'], + tfliteModel: { gcsTfliteUri: 'this will be replaced below' }, + }; + return uploadModelToGcs('model1.tflite', 'valid_model.tflite') + .then((fileName: string) => { + modelOptions.tfliteModel!.gcsTfliteUri = fileName; + return getMachineLearning().createModel(modelOptions) + .then((model) => { + scheduleForDelete(model); + verifyModel(model, modelOptions); + }); + }); + }); + + it('creates a new Model with invalid ModelFormat', () => { + // Upload a file to default gcs bucket + const modelOptions: ModelOptions = { + displayName: 'node-integ-test-create-3', + tags: ['tag234', 'tag456'], + tfliteModel: { gcsTfliteUri: 'this will be replaced below' }, + }; + return uploadModelToGcs('invalid_model.tflite', 'invalid_model.tflite') + .then((fileName: string) => { + modelOptions.tfliteModel!.gcsTfliteUri = fileName; + return getMachineLearning().createModel(modelOptions) + .then((model) => { + scheduleForDelete(model); + verifyModel(model, modelOptions); + }); + }); + }); + + it ('rejects with invalid-argument when modelOptions are invalid', () => { + const modelOptions: ModelOptions = { + displayName: 'Invalid Name#*^!', + }; + return getMachineLearning().createModel(modelOptions) + .should.eventually.be.rejected.and.have.property('code', 'machine-learning/invalid-argument'); + }); + }); + + describe('updateModel()', () => { + + const UPDATE_NAME: ModelOptions = { + displayName: 'update-model-new-name', + }; + + it('rejects with not-found when the Model does not exist', () => { + const nonExistingId = '00000000'; + return getMachineLearning().updateModel(nonExistingId, UPDATE_NAME) + .should.eventually.be.rejected.and.have.property( + 'code', 'machine-learning/not-found'); + }); + + it('rejects with invalid-argument when the ModelId is invalid', () => { + return getMachineLearning().updateModel('invalid-model-id', UPDATE_NAME) + .should.eventually.be.rejected.and.have.property( + 'code', 'machine-learning/invalid-argument'); + }); + + it ('rejects with invalid-argument when modelOptions are invalid', () => { + const modelOptions: ModelOptions = { + displayName: 'Invalid Name#*^!', + }; + return createTemporaryModel({ displayName: 'node-integ-invalid-argument' }) + .then((model) => getMachineLearning().updateModel(model.modelId, modelOptions) + .should.eventually.be.rejected.and.have.property( + 'code', 'machine-learning/invalid-argument')); + }); + + it('updates the displayName', () => { + const DISPLAY_NAME = 'node-integ-test-update-1b'; + return createTemporaryModel({ displayName: 'node-integ-test-update-1a' }) + .then((model) => { + const modelOptions: ModelOptions = { + displayName: DISPLAY_NAME, + }; + return getMachineLearning().updateModel(model.modelId, modelOptions) + .then((updatedModel) => { + verifyModel(updatedModel, modelOptions); + }); + }); + }); + + it('sets tags for a model', () => { + const ORIGINAL_TAGS = ['tag-node-update-1']; + const NEW_TAGS = ['tag-node-update-2', 'tag-node-update-3']; + + return createTemporaryModel({ + displayName: 'node-integ-test-update-2', + tags: ORIGINAL_TAGS, + }).then((expectedModel) => { + const modelOptions: ModelOptions = { + tags: NEW_TAGS, + }; + return getMachineLearning().updateModel(expectedModel.modelId, modelOptions) + .then((actualModel) => { + expect(actualModel.tags!.length).to.equal(2); + expect(actualModel.tags).to.have.same.members(NEW_TAGS); + }); + }); + }); + + it('updates the tflite file', () => { + return Promise.all([ + createTemporaryModel(), + uploadModelToGcs('model1.tflite', 'valid_model.tflite')]) + .then(([model, fileName]) => { + const modelOptions: ModelOptions = { + tfliteModel: { gcsTfliteUri: fileName }, + }; + return getMachineLearning().updateModel(model.modelId, modelOptions) + .then((updatedModel) => { + verifyModel(updatedModel, modelOptions); + }); + }); + }); + + it('can update more than 1 field', () => { + 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: ModelOptions = { + displayName: DISPLAY_NAME, + tags: TAGS, + }; + return getMachineLearning().updateModel(model.modelId, modelOptions) + .then((updatedModel) => { + expect(updatedModel.displayName).to.equal(DISPLAY_NAME); + expect(updatedModel.tags).to.have.same.members(TAGS); + }); + }); + }); + }); + + describe('publishModel()', () => { + it('should reject when model does not exist', () => { + const nonExistingName = '00000000'; + return getMachineLearning().publishModel(nonExistingName) + .should.eventually.be.rejected.and.have.property( + 'code', 'machine-learning/not-found'); + }); + + it('rejects with invalid-argument when the ModelId is invalid', () => { + return getMachineLearning().publishModel('invalid-model-id') + .should.eventually.be.rejected.and.have.property( + 'code', 'machine-learning/invalid-argument'); + }); + + it('publishes the model successfully', () => { + const modelOptions: ModelOptions = { + displayName: 'node-integ-test-publish-1', + tfliteModel: { gcsTfliteUri: 'this will be replaced below' }, + }; + return uploadModelToGcs('model1.tflite', 'valid_model.tflite') + .then((fileName: string) => { + modelOptions.tfliteModel!.gcsTfliteUri = fileName; + return createTemporaryModel(modelOptions) + .then((createdModel) => { + expect(createdModel.validationError).to.be.undefined; + expect(createdModel.published).to.be.false; + return getMachineLearning().publishModel(createdModel.modelId) + .then((publishedModel) => { + expect(publishedModel.published).to.be.true; + }); + }); + }); + }); + }); + + describe('unpublishModel()', () => { + it('should reject when model does not exist', () => { + const nonExistingName = '00000000'; + return getMachineLearning().unpublishModel(nonExistingName) + .should.eventually.be.rejected.and.have.property( + 'code', 'machine-learning/not-found'); + }); + + it('rejects with invalid-argument when the ModelId is invalid', () => { + return getMachineLearning().unpublishModel('invalid-model-id') + .should.eventually.be.rejected.and.have.property( + 'code', 'machine-learning/invalid-argument'); + }); + + it('unpublishes the model successfully', () => { + const modelOptions: ModelOptions = { + displayName: 'node-integ-test-unpublish-1', + tfliteModel: { gcsTfliteUri: 'this will be replaced below' }, + }; + return uploadModelToGcs('model1.tflite', 'valid_model.tflite') + .then((fileName: string) => { + modelOptions.tfliteModel!.gcsTfliteUri = fileName; + return createTemporaryModel(modelOptions) + .then((createdModel) => { + expect(createdModel.validationError).to.be.undefined; + expect(createdModel.published).to.be.false; + return getMachineLearning().publishModel(createdModel.modelId) + .then((publishedModel) => { + expect(publishedModel.published).to.be.true; + return getMachineLearning().unpublishModel(publishedModel.modelId) + .then((unpublishedModel) => { + expect(unpublishedModel.published).to.be.false; + }); + }); + }); + }); + }); + }); + + + describe('getModel()', () => { + it('rejects with not-found when the Model does not exist', () => { + const nonExistingName = '00000000'; + return getMachineLearning().getModel(nonExistingName) + .should.eventually.be.rejected.and.have.property( + 'code', 'machine-learning/not-found'); + }); + + it('rejects with invalid-argument when the ModelId is invalid', () => { + return getMachineLearning().getModel('invalid-model-id') + .should.eventually.be.rejected.and.have.property( + 'code', 'machine-learning/invalid-argument'); + }); + + it('resolves with existing Model', () => { + return createTemporaryModel() + .then((expectedModel) => + getMachineLearning().getModel(expectedModel.modelId) + .then((actualModel) => { + expect(actualModel).to.deep.equal(expectedModel); + }), + ); + }); + }); + + describe('listModels()', () => { + let model1: Model; + let model2: Model; + let model3: Model; + + before(() => { + return Promise.all([ + getMachineLearning().createModel({ + displayName: 'node-integ-list1', + tags: ['node-integ-tag-1'], + }), + getMachineLearning().createModel({ + displayName: 'node-integ-list2', + tags: ['node-integ-tag-1'], + }), + getMachineLearning().createModel({ + displayName: 'node-integ-list3', + tags: ['node-integ-tag-1'], + })]) + .then(([m1, m2, m3]: Model[]) => { + model1 = m1; + model2 = m2; + model3 = m3; + }); + }); + + after(() => { + return Promise.all([ + getMachineLearning().deleteModel(model1.modelId), + getMachineLearning().deleteModel(model2.modelId), + getMachineLearning().deleteModel(model3.modelId), + ]); + }); + + it('resolves with a list of models', () => { + return getMachineLearning().listModels({ pageSize: 100 }) + .then((modelList) => { + 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.undefined; + }); + }); + + it('respects page size', () => { + return getMachineLearning().listModels({ pageSize: 2 }) + .then((modelList) => { + expect(modelList.models.length).to.equal(2); + expect(modelList.pageToken).not.to.be.empty; + }); + }); + + it('filters by exact displayName', () => { + return getMachineLearning().listModels({ filter: 'displayName=node-integ-list1' }) + .then((modelList) => { + expect(modelList.models.length).to.equal(1); + expect(modelList.models[0]).to.deep.equal(model1); + expect(modelList.pageToken).to.be.undefined; + }); + }); + + it('filters by displayName prefix', () => { + return getMachineLearning().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); + expect(modelList.models).to.deep.include(model2); + expect(modelList.models).to.deep.include(model3); + expect(modelList.pageToken).to.be.undefined; + }); + }); + + it('filters by tag', () => { + return getMachineLearning().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); + expect(modelList.models).to.deep.include(model2); + expect(modelList.models).to.deep.include(model3); + expect(modelList.pageToken).to.be.undefined; + }); + }); + + it('handles pageTokens properly', () => { + return getMachineLearning().listModels({ filter: 'displayName:node-integ-list*', pageSize: 2 }) + .then((modelList) => { + expect(modelList.models.length).to.equal(2); + expect(modelList.pageToken).not.to.be.undefined; + return getMachineLearning().listModels({ + filter: 'displayName:node-integ-list*', + pageSize: 2, + pageToken: modelList.pageToken + }) + .then((modelList2) => { + expect(modelList2.models.length).to.be.at.least(1); + expect(modelList2.pageToken).to.be.undefined; + }); + }); + }); + + it('successfully returns an empty list of models', () => { + return getMachineLearning().listModels({ filter: 'displayName=non-existing-model' }) + .then((modelList) => { + expect(modelList.models.length).to.equal(0); + expect(modelList.pageToken).to.be.undefined; + }); + }); + + it('rejects with invalid argument if the filter is invalid', () => { + return getMachineLearning().listModels({ filter: 'invalidFilterItem=foo' }) + .should.eventually.be.rejected.and.have.property( + 'code', 'machine-learning/invalid-argument'); + }); + }); + + describe('deleteModel()', () => { + it('rejects with not-found when the Model does not exist', () => { + const nonExistingName = '00000000'; + return getMachineLearning().deleteModel(nonExistingName) + .should.eventually.be.rejected.and.have.property( + 'code', 'machine-learning/not-found'); + }); + + it('rejects with invalid-argument when the Model ID is invalid', () => { + return getMachineLearning().deleteModel('invalid-model-id') + .should.eventually.be.rejected.and.have.property( + 'code', 'machine-learning/invalid-argument'); + }); + + it('deletes existing Model', () => { + return createTemporaryModel().then((model) => { + return getMachineLearning().deleteModel(model.modelId) + .then(() => { + return getMachineLearning().getModel(model.modelId) + .should.eventually.be.rejected.and.have.property('code', 'machine-learning/not-found'); + }) + .then(() => { + unscheduleForDelete(model); // Already deleted. + }); + }); + }); + }); + +}); + +function verifyModel(model: Model, expectedOptions: 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 { + expect(model.validationError).to.equal('No model file has been uploaded.'); + } +} + +function verifyGcsTfliteModel(model: 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; + expect(model.validationError).to.equal('Invalid flatbuffer format'); + } else { + expect(model.modelHash).to.not.be.undefined; + expect(model.validationError).to.be.undefined; + } +} diff --git a/test/integration/messaging.spec.ts b/test/integration/messaging.spec.ts index 8aacad73b3..0a17a2d750 100644 --- a/test/integration/messaging.spec.ts +++ b/test/integration/messaging.spec.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import * as admin from '../../lib/index'; import * as chai from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; +import { Message, MulticastMessage, getMessaging } from '../../lib/messaging/index'; chai.should(); chai.use(chaiAsPromised); @@ -38,16 +38,36 @@ const condition = '"test0" in topics || ("test1" in topics && "test2" in topics) const invalidTopic = 'topic-$%#^'; -const message: admin.messaging.Message = { +const message: Message = { data: { foo: 'bar', }, notification: { title: 'Message title', body: 'Message body', + imageUrl: 'https://example.com/image.png', }, android: { restrictedPackageName: 'com.google.firebase.testing', + notification: { + title: 'test.title', + ticker: 'test.ticker', + sticky: true, + visibility: 'private', + eventTimestamp: new Date(), + localOnly: true, + priority: 'high', + vibrateTimingsMillis: [100, 50, 250], + defaultVibrateTimings: false, + defaultSound: true, + lightSettings: { + color: '#AABBCC55', + lightOnDurationMillis: 200, + lightOffDurationMillis: 300, + }, + defaultLightSettings: false, + notificationCount: 1, + }, }, apns: { payload: { @@ -82,93 +102,193 @@ const options = { describe('admin.messaging', () => { it('send(message, dryRun) returns a message ID', () => { - return admin.messaging().send(message, true) + return getMessaging().send(message, true) .then((name) => { expect(name).matches(/^projects\/.*\/messages\/.*$/); }); }); + it('sendEach()', () => { + const messages: Message[] = [message, message, message]; + return getMessaging().sendEach(messages, true) + .then((response) => { + expect(response.responses.length).to.equal(messages.length); + expect(response.successCount).to.equal(messages.length); + expect(response.failureCount).to.equal(0); + response.responses.forEach((resp) => { + expect(resp.success).to.be.true; + expect(resp.messageId).matches(/^projects\/.*\/messages\/.*$/); + }); + }); + }); + + it('sendEach(500)', () => { + const messages: Message[] = []; + for (let i = 0; i < 500; i++) { + messages.push({ topic: `foo-bar-${i % 10}` }); + } + return getMessaging().sendEach(messages, true) + .then((response) => { + expect(response.responses.length).to.equal(messages.length); + expect(response.successCount).to.equal(messages.length); + expect(response.failureCount).to.equal(0); + response.responses.forEach((resp) => { + expect(resp.success).to.be.true; + expect(resp.messageId).matches(/^projects\/.*\/messages\/.*$/); + }); + }); + }); + + it('sendAll()', () => { + const messages: Message[] = [message, message, message]; + return getMessaging().sendAll(messages, true) + .then((response) => { + expect(response.responses.length).to.equal(messages.length); + expect(response.successCount).to.equal(messages.length); + expect(response.failureCount).to.equal(0); + response.responses.forEach((resp) => { + expect(resp.success).to.be.true; + expect(resp.messageId).matches(/^projects\/.*\/messages\/.*$/); + }); + }); + }); + + it('sendAll(500)', () => { + const messages: Message[] = []; + for (let i = 0; i < 500; i++) { + messages.push({ topic: `foo-bar-${i % 10}` }); + } + return getMessaging().sendAll(messages, true) + .then((response) => { + expect(response.responses.length).to.equal(messages.length); + expect(response.successCount).to.equal(messages.length); + expect(response.failureCount).to.equal(0); + response.responses.forEach((resp) => { + expect(resp.success).to.be.true; + expect(resp.messageId).matches(/^projects\/.*\/messages\/.*$/); + }); + }); + }); + + it('sendEachForMulticast()', () => { + const multicastMessage: MulticastMessage = { + data: message.data, + android: message.android, + tokens: ['not-a-token', 'also-not-a-token'], + }; + return getMessaging().sendEachForMulticast(multicastMessage, true) + .then((response) => { + expect(response.responses.length).to.equal(2); + expect(response.successCount).to.equal(0); + expect(response.failureCount).to.equal(2); + response.responses.forEach((resp) => { + expect(resp.success).to.be.false; + expect(resp.messageId).to.be.undefined; + expect(resp.error).to.have.property('code', 'messaging/invalid-argument'); + }); + }); + }); + + it('sendMulticast()', () => { + const multicastMessage: MulticastMessage = { + data: message.data, + android: message.android, + tokens: ['not-a-token', 'also-not-a-token'], + }; + return getMessaging().sendMulticast(multicastMessage, true) + .then((response) => { + expect(response.responses.length).to.equal(2); + expect(response.successCount).to.equal(0); + expect(response.failureCount).to.equal(2); + response.responses.forEach((resp) => { + expect(resp.success).to.be.false; + expect(resp.messageId).to.be.undefined; + expect(resp.error).to.have.property('code', 'messaging/invalid-argument'); + }); + }); + }); + it('sendToDevice(token) returns a response with multicast ID', () => { - return admin.messaging().sendToDevice(registrationToken, payload, options) + return getMessaging().sendToDevice(registrationToken, payload, options) .then((response) => { expect(typeof response.multicastId).to.equal('number'); }); }); it('sendToDevice(token-list) returns a response with multicat ID', () => { - return admin.messaging().sendToDevice(registrationTokens, payload, options) + return getMessaging().sendToDevice(registrationTokens, payload, options) .then((response) => { expect(typeof response.multicastId).to.equal('number'); }); }); - it('sendToDeviceGroup() returns a response with success count', () => { - return admin.messaging().sendToDeviceGroup(notificationKey, payload, options) + xit('sendToDeviceGroup() returns a response with success count', () => { + return getMessaging().sendToDeviceGroup(notificationKey, payload, options) .then((response) => { expect(typeof response.successCount).to.equal('number'); }); }); it('sendToTopic() returns a response with message ID', () => { - return admin.messaging().sendToTopic(topic, payload, options) + return getMessaging().sendToTopic(topic, payload, options) .then((response) => { expect(typeof response.messageId).to.equal('number'); }); }); it('sendToCondition() returns a response with message ID', () => { - return admin.messaging().sendToCondition(condition, payload, options) + return getMessaging().sendToCondition(condition, payload, options) .then((response) => { expect(typeof response.messageId).to.equal('number'); }); }); it('sendToDevice(token) fails when called with invalid payload', () => { - return admin.messaging().sendToDevice(registrationToken, invalidPayload, options) + return getMessaging().sendToDevice(registrationToken, invalidPayload, options) .should.eventually.be.rejected.and.have.property('code', 'messaging/invalid-payload'); }); it('sendToDevice(token-list) fails when called with invalid payload', () => { - return admin.messaging().sendToDevice(registrationTokens, invalidPayload, options) + return getMessaging().sendToDevice(registrationTokens, invalidPayload, options) .should.eventually.be.rejected.and.have.property('code', 'messaging/invalid-payload'); }); it('sendToDeviceGroup() fails when called with invalid payload', () => { - return admin.messaging().sendToDeviceGroup(notificationKey, invalidPayload, options) + return getMessaging().sendToDeviceGroup(notificationKey, invalidPayload, options) .should.eventually.be.rejected.and.have.property('code', 'messaging/invalid-payload'); }); it('sendToTopic() fails when called with invalid payload', () => { - return admin.messaging().sendToTopic(topic, invalidPayload, options) + return getMessaging().sendToTopic(topic, invalidPayload, options) .should.eventually.be.rejected.and.have.property('code', 'messaging/invalid-payload'); }); it('sendToCondition() fails when called with invalid payload', () => { - return admin.messaging().sendToCondition(condition, invalidPayload, options) + return getMessaging().sendToCondition(condition, invalidPayload, options) .should.eventually.be.rejected.and.have.property('code', 'messaging/invalid-payload'); }); it('subscribeToTopic() returns a response with success count', () => { - return admin.messaging().subscribeToTopic(registrationToken, topic) + return getMessaging().subscribeToTopic(registrationToken, topic) .then((response) => { expect(typeof response.successCount).to.equal('number'); }); }); it('unsubscribeFromTopic() returns a response with success count', () => { - return admin.messaging().unsubscribeFromTopic(registrationToken, topic) + return getMessaging().unsubscribeFromTopic(registrationToken, topic) .then((response) => { expect(typeof response.successCount).to.equal('number'); }); }); it('subscribeToTopic() fails when called with invalid topic', () => { - return admin.messaging().subscribeToTopic(registrationToken, invalidTopic) + return getMessaging().subscribeToTopic(registrationToken, invalidTopic) .should.eventually.be.rejected.and.have.property('code', 'messaging/invalid-argument'); }); it('unsubscribeFromTopic() fails when called with invalid topic', () => { - return admin.messaging().unsubscribeFromTopic(registrationToken, invalidTopic) + return getMessaging().unsubscribeFromTopic(registrationToken, invalidTopic) .should.eventually.be.rejected.and.have.property('code', 'messaging/invalid-argument'); }); }); diff --git a/test/integration/postcheck/esm/example.test.js b/test/integration/postcheck/esm/example.test.js new file mode 100644 index 0000000000..29d0654374 --- /dev/null +++ b/test/integration/postcheck/esm/example.test.js @@ -0,0 +1,134 @@ +/*! + * @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 { expect } from 'chai'; + +import { cert, deleteApp, initializeApp } from 'firebase-admin/app'; +import { getAppCheck, AppCheck } from 'firebase-admin/app-check'; +import { getAuth, Auth } from 'firebase-admin/auth'; +import { getDatabase, getDatabaseWithUrl, ServerValue } from 'firebase-admin/database'; +import { getFirestore, DocumentReference, Firestore, FieldValue } from 'firebase-admin/firestore'; +import { getFunctions } from 'firebase-admin/functions'; +import { getInstanceId, InstanceId } from 'firebase-admin/instance-id'; +import { getMachineLearning, MachineLearning } from 'firebase-admin/machine-learning'; +import { getMessaging, Messaging } from 'firebase-admin/messaging'; +import { getProjectManagement, ProjectManagement } from 'firebase-admin/project-management'; +import { getRemoteConfig, RemoteConfig } from 'firebase-admin/remote-config'; +import { getSecurityRules, SecurityRules } from 'firebase-admin/security-rules'; +import { getStorage, Storage } from 'firebase-admin/storage'; + +describe('ESM entry points', () => { + let app; + + before(() => { + app = initializeApp({ + credential: cert('mock.key.json'), + databaseURL: 'https://mock.firebaseio.com' + }, 'TestApp'); + }); + + after(() => { + return deleteApp(app); + }); + + it('Should return an initialized App', () => { + expect(app.name).to.equal('TestApp'); + }); + + it('Should return an AppCheck client', () => { + const client = getAppCheck(app); + expect(client).to.be.instanceOf(AppCheck); + }); + + it('Should return an Auth client', () => { + const client = getAuth(app); + expect(client).to.be.instanceOf(Auth); + }); + + it('Should return a Messaging client', () => { + const client = getMessaging(app); + expect(client).to.be.instanceOf(Messaging); + }); + + it('Should return a ProjectManagement client', () => { + const client = getProjectManagement(app); + expect(client).to.be.instanceOf(ProjectManagement); + }); + + it('Should return a SecurityRules client', () => { + const client = getSecurityRules(app); + expect(client).to.be.instanceOf(SecurityRules); + }); + + it('Should return a Database client', () => { + const db = getDatabase(app); + expect(db).to.be.not.undefined; + expect(typeof db.getRules).to.equal('function'); + }); + + it('Should return a Database client for URL', () => { + const db = getDatabaseWithUrl('https://other-mock.firebaseio.com', app); + expect(db).to.be.not.undefined; + expect(typeof db.getRules).to.equal('function'); + }); + + it('Should return a Database ServerValue', () => { + expect(ServerValue.increment(1)).to.be.not.undefined; + }); + + it('Should return a Cloud Storage client', () => { + const storage = getStorage(app); + expect(storage).to.be.instanceOf(Storage) + const bucket = storage.bucket('TestBucket'); + expect(bucket.name).to.equal('TestBucket'); + }); + + it('Should return a Firestore client', () => { + const firestore = getFirestore(app); + expect(firestore).to.be.instanceOf(Firestore); + }); + + it('Should return a Firestore FieldValue', () => { + expect(FieldValue.increment(1)).to.be.not.undefined; + }); + + it('Should return a DocumentReference', () => { + const ref = getFirestore(app).collection('test').doc(); + expect(ref).to.be.instanceOf(DocumentReference); + }); + + it('Should return a Functions client', () => { + const fn = getFunctions(app); + expect(fn).to.be.not.undefined; + expect(typeof fn.taskQueue).to.equal('function'); + }); + + it('Should return an InstanceId client', () => { + const client = getInstanceId(app); + expect(client).to.be.instanceOf(InstanceId); + }); + + it('Should return a MachineLearning client', () => { + const client = getMachineLearning(app); + expect(client).to.be.instanceOf(MachineLearning); + }); + + it('Should return a RemoteConfig client', () => { + const client = getRemoteConfig(app); + expect(client).to.be.instanceOf(RemoteConfig); + }); +}); diff --git a/test/integration/postcheck/esm/package.json b/test/integration/postcheck/esm/package.json new file mode 100644 index 0000000000..3dbc1ca591 --- /dev/null +++ b/test/integration/postcheck/esm/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/test/integration/postcheck/package.json b/test/integration/postcheck/package.json new file mode 100644 index 0000000000..2b7f8ac6f0 --- /dev/null +++ b/test/integration/postcheck/package.json @@ -0,0 +1,19 @@ +{ + "name": "firebase-admin-postcheck", + "version": "1.0.0", + "description": "Firebase Admin SDK post package test cases", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/firebase/firebase-admin-node" + }, + "devDependencies": { + "@types/chai": "^4.0.0", + "@types/mocha": "^2.2.48", + "@types/node": ">=14.0.0", + "chai": "^4.2.0", + "mocha": "^8.0.0", + "ts-node": "^10.8.1", + "typescript": "^4.6.4" + } +} diff --git a/test/integration/typescript/tsconfig.json b/test/integration/postcheck/tsconfig.json similarity index 71% rename from test/integration/typescript/tsconfig.json rename to test/integration/postcheck/tsconfig.json index 14c4366386..c67333ffa1 100644 --- a/test/integration/typescript/tsconfig.json +++ b/test/integration/postcheck/tsconfig.json @@ -2,15 +2,15 @@ "compilerOptions": { "module": "commonjs", "moduleResolution": "node", - "target": "es5", + "target": "es2020", "noImplicitAny": false, - "lib": ["es5", "es2015.promise"], + "lib": ["es2020"], "outDir": "lib", "typeRoots": [ "node_modules/@types" ] }, "files": [ - "src/example.ts" + "./typescript/example.ts" ] -} \ No newline at end of file +} diff --git a/test/integration/postcheck/typescript/example-modular.test.ts b/test/integration/postcheck/typescript/example-modular.test.ts new file mode 100644 index 0000000000..c5ccf3c8a2 --- /dev/null +++ b/test/integration/postcheck/typescript/example-modular.test.ts @@ -0,0 +1,140 @@ +/*! + * @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 { expect } from 'chai'; + +import { cert, deleteApp, initializeApp, App } from 'firebase-admin/app'; +import { getAppCheck, AppCheck } from 'firebase-admin/app-check'; +import { getAuth, Auth } from 'firebase-admin/auth'; +import { getDatabase, getDatabaseWithUrl, Database, ServerValue } from 'firebase-admin/database'; +import { getFirestore, DocumentReference, Firestore, FieldValue } from 'firebase-admin/firestore'; +import { getFunctions, Functions } from 'firebase-admin/functions'; +import { getInstanceId, InstanceId } from 'firebase-admin/instance-id'; +import { getMachineLearning, MachineLearning } from 'firebase-admin/machine-learning'; +import { getMessaging, Messaging } from 'firebase-admin/messaging'; +import { getProjectManagement, ProjectManagement } from 'firebase-admin/project-management'; +import { getRemoteConfig, RemoteConfig } from 'firebase-admin/remote-config'; +import { getSecurityRules, SecurityRules } from 'firebase-admin/security-rules'; +import { getStorage, Storage } from 'firebase-admin/storage'; + +import { Bucket } from '@google-cloud/storage'; + + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const serviceAccount = require('../mock.key.json'); + +describe('Modular API', () => { + let app: App; + + before(() => { + app = initializeApp({ + credential: cert(serviceAccount), + databaseURL: 'https://mock.firebaseio.com' + }, 'TestApp'); + }); + + after(() => { + return deleteApp(app); + }); + + it('Should return an initialized App', () => { + expect(app.name).to.equal('TestApp'); + }); + + it('Should return an AppCheck client', () => { + const client = getAppCheck(app); + expect(client).to.be.instanceOf(AppCheck); + }); + + it('Should return an Auth client', () => { + const client = getAuth(app); + expect(client).to.be.instanceOf(Auth); + }); + + it('Should return a Messaging client', () => { + const client = getMessaging(app); + expect(client).to.be.instanceOf(Messaging); + }); + + it('Should return a ProjectManagement client', () => { + const client = getProjectManagement(app); + expect(client).to.be.instanceOf(ProjectManagement); + }); + + it('Should return a SecurityRules client', () => { + const client = getSecurityRules(app); + expect(client).to.be.instanceOf(SecurityRules); + }); + + it('Should return a Database client', () => { + const db: Database = getDatabase(app); + expect(db).to.be.not.undefined; + expect(typeof db.getRules).to.equal('function'); + }); + + it('Should return a Database client for URL', () => { + const db: Database = getDatabaseWithUrl('https://other-mock.firebaseio.com', app); + expect(db).to.be.not.undefined; + expect(typeof db.getRules).to.equal('function'); + }); + + it('Should return a Database ServerValue', () => { + expect(ServerValue.increment(1)).to.be.not.undefined; + }); + + it('Should return a Cloud Storage client', () => { + const storage = getStorage(app); + expect(storage).to.be.instanceOf(Storage) + const bucket: Bucket = storage.bucket('TestBucket'); + expect(bucket.name).to.equal('TestBucket'); + }); + + it('Should return a Firestore client', () => { + const firestore = getFirestore(app); + expect(firestore).to.be.instanceOf(Firestore); + }); + + it('Should return a Firestore FieldValue', () => { + expect(FieldValue.increment(1)).to.be.not.undefined; + }); + + it('Should return a DocumentReference', () => { + const ref = getFirestore(app).collection('test').doc(); + expect(ref).to.be.instanceOf(DocumentReference); + }); + + it('Should return a Functions client', () => { + const fn: Functions = getFunctions(app); + expect(fn).to.be.not.undefined; + expect(typeof fn.taskQueue).to.equal('function'); + }); + + it('Should return an InstanceId client', () => { + const client = getInstanceId(app); + expect(client).to.be.instanceOf(InstanceId); + }); + + it('Should return a MachineLearning client', () => { + const client = getMachineLearning(app); + expect(client).to.be.instanceOf(MachineLearning); + }); + + it('Should return a RemoteConfig client', () => { + const client = getRemoteConfig(app); + expect(client).to.be.instanceOf(RemoteConfig); + }); +}); diff --git a/test/integration/postcheck/typescript/example.test.ts b/test/integration/postcheck/typescript/example.test.ts new file mode 100644 index 0000000000..2d97196477 --- /dev/null +++ b/test/integration/postcheck/typescript/example.test.ts @@ -0,0 +1,103 @@ +/*! + * @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 initApp from './example'; +import { expect } from 'chai'; +import { Bucket } from '@google-cloud/storage'; +import { Firestore } from '@google-cloud/firestore'; + +import * as admin from 'firebase-admin'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const serviceAccount = require('../mock.key.json'); + +describe('Legacy API', () => { + let app: admin.app.App; + + before(() => { + app = initApp(serviceAccount, 'TestApp'); + }); + + after(() => { + return app.delete(); + }); + + it('Should return an initialized App', () => { + expect(app.name).to.equal('TestApp'); + }); + + it('Should return an Auth client', () => { + const client = admin.auth(app); + expect(client).to.be.instanceOf((admin.auth as any).Auth); + }); + + it('Should return a Messaging client', () => { + const client = admin.messaging(app); + expect(client).to.be.instanceOf((admin.messaging as any).Messaging); + }); + + it('Should return a ProjectManagement client', () => { + const client = admin.projectManagement(app); + expect(client).to.be.instanceOf((admin.projectManagement as any).ProjectManagement); + }); + + it('Should return a SecurityRules client', () => { + const client = admin.securityRules(app); + expect(client).to.be.instanceOf((admin.securityRules as any).SecurityRules); + }); + + it('Should return a Database client', () => { + const db = admin.database(app); + expect(db).to.be.instanceOf((admin.database as any).Database); + }); + + it('Should return a Database client for URL', () => { + const db = app.database('https://other-mock.firebaseio.com'); + expect(db).to.be.instanceOf((admin.database as any).Database); + }); + + it('Should return a Database ServerValue', () => { + const serverValue = admin.database.ServerValue; + expect(serverValue).to.not.be.null; + }); + + it('Should return a Cloud Storage client', () => { + const storage: admin.storage.Storage = app.storage(); + const bucket: Bucket = storage.bucket('TestBucket'); + expect(bucket.name).to.equal('TestBucket'); + }); + + it('Should return a Firestore client from the app', () => { + const firestore: Firestore = app.firestore(); + expect(firestore).to.be.instanceOf(admin.firestore.Firestore); + }); + + it('Should return a Firestore client', () => { + const firestore: Firestore = admin.firestore(app); + expect(firestore).to.be.instanceOf(admin.firestore.Firestore); + }); + + it('Should return a Firestore FieldValue', () => { + const fieldValue = admin.firestore.FieldValue; + expect(fieldValue).to.not.be.null; + }); + + it('Should return a DocumentReference', () => { + const ref: admin.firestore.DocumentReference = admin.firestore(app).collection('test').doc(); + expect(ref).to.not.be.null; + }); +}); diff --git a/test/integration/typescript/src/example.ts b/test/integration/postcheck/typescript/example.ts similarity index 60% rename from test/integration/typescript/src/example.ts rename to test/integration/postcheck/typescript/example.ts index 0de5332cc2..b44dbcc163 100644 --- a/test/integration/typescript/src/example.ts +++ b/test/integration/postcheck/typescript/example.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,19 +17,19 @@ import * as firebase from 'firebase-admin'; -export function initApp(serviceAcct: any, name: string) { - return firebase.initializeApp({ - credential: firebase.credential.cert(serviceAcct), - databaseURL: 'https://mock.firebaseio.com' - }, name); +export function initApp(serviceAcct: any, name: string): firebase.app.App { + return firebase.initializeApp({ + credential: firebase.credential.cert(serviceAcct), + databaseURL: 'https://mock.firebaseio.com' + }, name); } export function addValueEventListener( - // Check for type compilation - db: firebase.database.Database, - callback: (s: firebase.database.DataSnapshot) => any) { - let eventType: firebase.database.EventType = 'value'; - db.ref().on(eventType, callback); + // Check for type compilation + db: firebase.database.Database, + callback: (s: firebase.database.DataSnapshot) => any): void { + const eventType: firebase.database.EventType = 'value'; + db.ref().on(eventType, callback); } export default initApp; diff --git a/test/integration/project-management.spec.ts b/test/integration/project-management.spec.ts new file mode 100644 index 0000000000..0d24566394 --- /dev/null +++ b/test/integration/project-management.spec.ts @@ -0,0 +1,306 @@ +/*! + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as _ from 'lodash'; +import * as chai from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import { projectId } from './setup'; +import { + AndroidApp, IosApp, ShaCertificate, getProjectManagement, +} from '../../lib/project-management/index'; + +const APP_NAMESPACE_PREFIX = 'com.adminsdkintegrationtest.a'; +const APP_NAMESPACE_SUFFIX_LENGTH = 15; + +const APP_DISPLAY_NAME_PREFIX = 'Created By Firebase AdminSDK Nodejs Integration Testing '; +const PROJECT_DISPLAY_NAME_PREFIX = 'Nodejs AdminSDK Testing '; +const APP_DISPLAY_NAME_SUFFIX_LENGTH = 15; +const PROJECT_DISPLAY_NAME_SUFFIX_LENGTH = 6; + +const SHA_256_HASH = 'aaaaccccaaaaccccaaaaccccaaaaccccaaaaccccaaaaccccaaaaccccaaaacccc'; + +const expect = chai.expect; + +chai.should(); +chai.use(chaiAsPromised); + +describe('admin.projectManagement', () => { + + let androidApp: AndroidApp; + let iosApp: IosApp; + + before(() => { + const androidPromise = ensureAndroidApp() + .then((app) => { + androidApp = app; + return deleteAllShaCertificates(androidApp); + }); + const iosPromise = ensureIosApp().then((app) => { + iosApp = app; + }); + + return Promise.all([androidPromise, iosPromise]); + }); + + describe('listAndroidApps()', () => { + it('successfully lists Android apps', () => { + return getProjectManagement().listAndroidApps() + .then((apps) => Promise.all(apps.map((app) => app.getMetadata()))) + .then((metadatas) => { + expect(metadatas.length).to.be.at.least(1); + const metadataOwnedByTest = + metadatas.find((metadata) => isIntegrationTestApp(metadata.packageName)); + expect(metadataOwnedByTest).to.exist; + expect(metadataOwnedByTest!.appId).to.equal(androidApp.appId); + }); + }); + }); + + describe('listIosApps()', () => { + it('successfully lists iOS apps', () => { + return getProjectManagement().listIosApps() + .then((apps) => Promise.all(apps.map((app) => app.getMetadata()))) + .then((metadatas) => { + expect(metadatas.length).to.be.at.least(1); + const metadataOwnedByTest = + metadatas.find((metadata) => isIntegrationTestApp(metadata.bundleId)); + expect(metadataOwnedByTest).to.exist; + expect(metadataOwnedByTest!.appId).to.equal(iosApp.appId); + }); + }); + }); + + describe('setDisplayName()', () => { + it('successfully set project\'s display name', () => { + const newDisplayName = generateUniqueProjectDisplayName(); + // TODO(caot): verify that project name has been renamed successfully after adding the ability + // to get project metadata. + return getProjectManagement().setDisplayName(newDisplayName) + .should.eventually.be.fulfilled; + }); + }); + + describe('listAppMetadata()', () => { + it('successfully lists metadata of all apps', () => { + return getProjectManagement().listAppMetadata() + .then((metadatas) => { + expect(metadatas.length).to.be.at.least(2); + const testAppMetadatas = metadatas.filter((metadata) => + isIntegrationTestAppDisplayName(metadata.displayName) && + (metadata.appId === androidApp.appId || metadata.appId === iosApp.appId)); + expect(testAppMetadatas).to.have.length(2); + }); + }); + }); + + describe('androidApp.getMetadata()', () => { + it('successfully sets Android app\'s display name', () => { + return androidApp.getMetadata().then((appMetadata) => { + expect(appMetadata.displayName).to.include(APP_DISPLAY_NAME_PREFIX); + expect(appMetadata.projectId).to.equal(projectId); + expect(appMetadata.packageName).to.include(APP_NAMESPACE_PREFIX); + }); + }); + }); + + describe('iosApp.getMetadata()', () => { + it('successfully sets iOS app\'s display name', () => { + return iosApp.getMetadata().then((appMetadata) => { + expect(appMetadata.displayName).to.include(APP_DISPLAY_NAME_PREFIX); + expect(appMetadata.projectId).to.equal(projectId); + expect(appMetadata.bundleId).to.include(APP_NAMESPACE_PREFIX); + }); + }); + }); + + describe('androidApp.setDisplayName()', () => { + it('successfully sets Android app\'s display name', () => { + const newDisplayName = generateUniqueAppDisplayName(); + return androidApp.setDisplayName(newDisplayName) + .then(() => androidApp.getMetadata()) + .then((appMetadata) => { + expect(appMetadata.displayName).to.equal(newDisplayName); + }); + }); + }); + + describe('iosApp.setDisplayName()', () => { + it('successfully sets iOS app\'s display name', () => { + const newDisplayName = generateUniqueAppDisplayName(); + return iosApp.setDisplayName(newDisplayName) + .then(() => iosApp.getMetadata()) + .then((appMetadata) => { + expect(appMetadata.displayName).to.equal(newDisplayName); + }); + }); + }); + + describe('androidApp.{get,add,delete}ShaCertificate()', () => { + it('successfully gets, adds, and deletes SHA certificates', () => { + // Steps: + // 1. Check that this app has no certs. + // 2. Add a cert to this app. + // 3. Check that the cert was added successfully. + // 4. Delete the cert we just created. + // 5. Check that this app has no certs. + return androidApp.getShaCertificates() + .then((certs) => { + expect(certs.length).to.equal(0); + + const shaCertificate = getProjectManagement().shaCertificate(SHA_256_HASH); + return androidApp.addShaCertificate(shaCertificate); + }) + .then(() => androidApp.getShaCertificates()) + .then((certs) => { + expect(certs.length).to.equal(1); + expect(certs[0].shaHash).to.equal(SHA_256_HASH); + expect(certs[0].certType).to.equal('sha256'); + expect(certs[0].resourceName).to.not.be.empty; + + return androidApp.deleteShaCertificate(certs[0]); + }) + .then(() => androidApp.getShaCertificates()) + .then((certs) => { + expect(certs.length).to.equal(0); + }); + }); + + it('add a cert and then remove it fails due to missing resourceName', + () => { + const shaCertificate = + getProjectManagement().shaCertificate(SHA_256_HASH); + return androidApp.addShaCertificate(shaCertificate) + .then(() => androidApp.deleteShaCertificate(shaCertificate)) + .should.eventually.be + .rejectedWith( + 'Specified certificate does not include a resourceName') + .with.property('code', 'project-management/invalid-argument'); + }); + }); + + describe('androidApp.getConfig()', () => { + it('successfully gets the Android app\'s config', () => { + return androidApp.getConfig().then((config) => { + expect(config).is.not.empty; + expect(config).includes(androidApp.appId); + }); + }); + }); + + describe('iosApp.getConfig()', () => { + it('successfully gets the iOS app\'s config', () => { + return iosApp.getConfig().then((config) => { + expect(config).is.not.empty; + expect(config).includes(iosApp.appId); + }); + }); + }); +}); + +/** + * Ensures that an Android app owned by these integration tests exist. If not one will be created. + * + * @return Android app owned by these integration tests. + */ +function ensureAndroidApp(): Promise { + return getProjectManagement().listAndroidApps() + .then((apps) => Promise.all(apps.map((app) => app.getMetadata()))) + .then((metadatas) => { + const metadataOwnedByTest = + metadatas.find((metadata) => isIntegrationTestApp(metadata.packageName)); + if (metadataOwnedByTest) { + return getProjectManagement().androidApp(metadataOwnedByTest.appId); + } + + // If no Android app owned by these integration tests was found, then create one. + return getProjectManagement() + .createAndroidApp(generateUniqueAppNamespace(), generateUniqueAppDisplayName()); + }); +} + +/** + * Ensures that an iOS app owned by these integration tests exist. If not one will be created. + * + * @return iOS app owned by these integration tests. + */ +function ensureIosApp(): Promise { + return getProjectManagement().listIosApps() + .then((apps) => Promise.all(apps.map((app) => app.getMetadata()))) + .then((metadatas) => { + const metadataOwnedByTest = + metadatas.find((metadata) => isIntegrationTestApp(metadata.bundleId)); + if (metadataOwnedByTest) { + return getProjectManagement().iosApp(metadataOwnedByTest.appId); + } + + // If no iOS app owned by these integration tests was found, then create one. + return getProjectManagement() + .createIosApp(generateUniqueAppNamespace(), generateUniqueAppDisplayName()); + }); +} + +/** + * Deletes all SHA certificates from the specified Android app. + */ +function deleteAllShaCertificates(androidApp: AndroidApp): Promise { + return androidApp.getShaCertificates() + .then((shaCertificates: ShaCertificate[]) => { + return Promise.all(shaCertificates.map((cert) => androidApp.deleteShaCertificate(cert))); + }) + .then(() => undefined); +} + +/** + * @return Dot-separated string that can be used as a unique package name or bundle ID. + */ +function generateUniqueAppNamespace(): string { + return APP_NAMESPACE_PREFIX + generateRandomString(APP_NAMESPACE_SUFFIX_LENGTH); +} + +/** + * @return Dot-separated string that can be used as a unique app display name. + */ +function generateUniqueAppDisplayName(): string { + return APP_DISPLAY_NAME_PREFIX + generateRandomString(APP_DISPLAY_NAME_SUFFIX_LENGTH); +} + +/** + * @return string that can be used as a unique project display name. + */ +function generateUniqueProjectDisplayName(): string { + return PROJECT_DISPLAY_NAME_PREFIX + generateRandomString(PROJECT_DISPLAY_NAME_SUFFIX_LENGTH); +} + +/** + * @return True if the specified appNamespace belongs to these integration tests. + */ +function isIntegrationTestApp(appNamespace: string): boolean { + return appNamespace ? appNamespace.startsWith(APP_NAMESPACE_PREFIX) : false; +} + +/** + * @return True if the specified appDisplayName belongs to these integration tests. + */ +function isIntegrationTestAppDisplayName(appDisplayName: string | undefined): boolean { + return appDisplayName ? appDisplayName.startsWith(APP_DISPLAY_NAME_PREFIX) : false; +} + +/** + * @return A randomly generated alphanumeric string, of the specified length. + */ +function generateRandomString(stringLength: number): string { + return _.times(stringLength, () => _.random(35).toString(36)).join(''); +} diff --git a/test/integration/remote-config.spec.ts b/test/integration/remote-config.spec.ts new file mode 100644 index 0000000000..79689a64e6 --- /dev/null +++ b/test/integration/remote-config.spec.ts @@ -0,0 +1,290 @@ +/*! + * 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 chai from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import { deepCopy } from '../../src/utils/deep-copy'; +import { + getRemoteConfig, + ParameterValueType, + RemoteConfigCondition, + RemoteConfigTemplate, +} from '../../lib/remote-config/index'; + +chai.should(); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +const VALID_PARAMETERS = { + holiday_promo_enabled: { + defaultValue: { useInAppDefault: true }, + description: 'promo indicator', + valueType: 'STRING' as ParameterValueType, + }, + welcome_message: { + defaultValue: { value: `welcome text ${Date.now()}` }, + valueType: 'STRING' as ParameterValueType, + conditionalValues: { + ios: { value: 'welcome ios text' }, + android: { value: 'welcome android text' }, + }, + } +}; + +const VALID_PARAMETER_GROUPS = { + new_menu: { + description: 'Description of the group.', + parameters: { + pumpkin_spice_season: { + defaultValue: { value: 'A Gryffindor must love a pumpkin spice latte.' }, + conditionalValues: { + 'android': { value: 'A Droid must love a pumpkin spice latte.' }, + }, + description: 'Description of the parameter.', + valueType: 'STRING' as ParameterValueType, + }, + }, + }, +}; + +const VALID_CONDITIONS: RemoteConfigCondition[] = [ + { + name: 'ios', + expression: 'device.os == \'ios\'', + tagColor: 'INDIGO', + }, + { + name: 'android', + expression: 'device.os == \'android\'', + tagColor: 'GREEN', + }, +]; + +const VALID_VERSION = { + description: `template description ${Date.now()}`, +} + +let currentTemplate: RemoteConfigTemplate; + +describe('admin.remoteConfig', () => { + before(async () => { + // obtain the most recent template (etag) to perform operations + currentTemplate = await getRemoteConfig().getTemplate(); + }); + + it('verify that the etag is read-only', () => { + expect(() => { + (currentTemplate as any).etag = 'new-etag'; + }).to.throw('Cannot set property etag of # which has only a getter'); + }); + + describe('validateTemplate', () => { + it('should succeed with a vaild template', () => { + // set parameters, groups, and conditions + currentTemplate.conditions = VALID_CONDITIONS; + currentTemplate.parameters = VALID_PARAMETERS; + currentTemplate.parameterGroups = VALID_PARAMETER_GROUPS; + currentTemplate.version = VALID_VERSION; + return getRemoteConfig().validateTemplate(currentTemplate) + .then((template) => { + expect(template.etag).matches(/^etag-[0-9]*-[0-9]*$/); + expect(template.conditions.length).to.equal(2); + expect(template.conditions).to.deep.equal(VALID_CONDITIONS); + expect(template.parameters).to.deep.equal(VALID_PARAMETERS); + expect(template.parameterGroups).to.deep.equal(VALID_PARAMETER_GROUPS); + expect(template.version).to.be.not.undefined; + expect(template.version!.description).equals(VALID_VERSION.description); + }); + }); + + it('should propagate API errors', () => { + // rejects with invalid-argument when conditions used in parameters do not exist + currentTemplate.conditions = []; + currentTemplate.parameters = VALID_PARAMETERS; + currentTemplate.parameterGroups = VALID_PARAMETER_GROUPS; + currentTemplate.version = VALID_VERSION; + return getRemoteConfig().validateTemplate(currentTemplate) + .should.eventually.be.rejected.and.have.property('code', 'remote-config/invalid-argument'); + }); + }); + + describe('publishTemplate', () => { + it('should succeed with a vaild template', () => { + // set parameters and conditions + currentTemplate.conditions = VALID_CONDITIONS; + currentTemplate.parameters = VALID_PARAMETERS; + currentTemplate.parameterGroups = VALID_PARAMETER_GROUPS; + currentTemplate.version = VALID_VERSION; + return getRemoteConfig().publishTemplate(currentTemplate) + .then((template) => { + expect(template.etag).matches(/^etag-[0-9]*-[0-9]*$/); + expect(template.conditions.length).to.equal(2); + expect(template.conditions).to.deep.equal(VALID_CONDITIONS); + expect(template.parameters).to.deep.equal(VALID_PARAMETERS); + expect(template.parameterGroups).to.deep.equal(VALID_PARAMETER_GROUPS); + expect(template.version).to.be.not.undefined; + expect(template.version!.description).equals(VALID_VERSION.description); + }); + }); + + it('should propagate API errors', () => { + // rejects with invalid-argument when conditions used in parameters do not exist + currentTemplate.conditions = []; + currentTemplate.parameters = VALID_PARAMETERS; + currentTemplate.parameterGroups = VALID_PARAMETER_GROUPS; + currentTemplate.version = VALID_VERSION; + return getRemoteConfig().publishTemplate(currentTemplate) + .should.eventually.be.rejected.and.have.property('code', 'remote-config/invalid-argument'); + }); + }); + + describe('getTemplate', () => { + it('should return the most recently published template', () => { + return getRemoteConfig().getTemplate() + .then((template) => { + expect(template.etag).matches(/^etag-[0-9]*-[0-9]*$/); + expect(template.conditions.length).to.equal(2); + expect(template.conditions).to.deep.equal(VALID_CONDITIONS); + expect(template.parameters).to.deep.equal(VALID_PARAMETERS); + expect(template.parameterGroups).to.deep.equal(VALID_PARAMETER_GROUPS); + expect(template.version).to.be.not.undefined; + expect(template.version!.description).equals(VALID_VERSION.description); + }); + }); + }); + + let versionOneNumber: string; + let versionTwoNumber: string; + const versionOneDescription = `getTemplateAtVersion test v1 ${Date.now()}`; + const versionTwoDescription = `getTemplateAtVersion test v2 ${Date.now()}`; + + describe('getTemplateAtVersion', () => { + before(async () => { + // obtain the current active template + let activeTemplate = await getRemoteConfig().getTemplate(); + + // publish a new template to create a new version number + activeTemplate.version = { description: versionOneDescription }; + activeTemplate = await getRemoteConfig().publishTemplate(activeTemplate) + expect(activeTemplate.version).to.be.not.undefined; + versionOneNumber = activeTemplate.version!.versionNumber!; + + // publish another template to create a second version number + activeTemplate.version = { description: versionTwoDescription }; + activeTemplate = await getRemoteConfig().publishTemplate(activeTemplate) + expect(activeTemplate.version).to.be.not.undefined; + versionTwoNumber = activeTemplate.version!.versionNumber!; + }); + + it('should return the requested template version v1', () => { + return getRemoteConfig().getTemplateAtVersion(versionOneNumber) + .then((template) => { + expect(template.etag).matches(/^etag-[0-9]*-[0-9]*$/); + expect(template.version).to.be.not.undefined; + expect(template.version!.versionNumber).equals(versionOneNumber); + expect(template.version!.description).equals(versionOneDescription); + }); + }); + }); + + describe('listVersions', () => { + it('should return the most recently published 2 versions', () => { + return getRemoteConfig().listVersions({ + pageSize: 2, + }) + .then((response) => { + expect(response.versions.length).to.equal(2); + // versions should be in reverse chronological order + expect(response.versions[0].description).equals(versionTwoDescription); + expect(response.versions[0].versionNumber).equals(versionTwoNumber); + expect(response.versions[1].description).equals(versionOneDescription); + expect(response.versions[1].versionNumber).equals(versionOneNumber); + }); + }); + }); + + describe('rollback', () => { + it('verify the most recent template version before rollback to the one prior', () => { + return getRemoteConfig().getTemplate() + .then((template) => { + expect(template.version).to.be.not.undefined; + expect(template.version!.versionNumber).equals(versionTwoNumber); + }); + }); + + it('should rollback to the requested version', () => { + return getRemoteConfig().rollback(versionOneNumber) + .then((template) => { + expect(template.version).to.be.not.undefined; + expect(template.version!.updateType).equals('ROLLBACK'); + expect(template.version!.description).equals(`Rollback to version ${versionOneNumber}`); + }); + }); + }); + + describe('createTemplateFromJSON', () => { + const INVALID_STRINGS: any[] = [null, undefined, '', 1, true, {}, []]; + const INVALID_JSON_STRINGS: any[] = ['abc', 'foo', 'a:a', '1:1']; + + INVALID_STRINGS.forEach((invalidJson) => { + it(`should throw if the json string is ${JSON.stringify(invalidJson)}`, () => { + expect(() => getRemoteConfig().createTemplateFromJSON(invalidJson)) + .to.throw('JSON string must be a valid non-empty string'); + }); + }); + + INVALID_JSON_STRINGS.forEach((invalidJson) => { + it(`should throw if the json string is ${JSON.stringify(invalidJson)}`, () => { + expect(() => getRemoteConfig().createTemplateFromJSON(invalidJson)) + .to.throw(/Failed to parse the JSON string/); + }); + }); + + const invalidEtags = [...INVALID_STRINGS]; + const sourceTemplate = { + parameters: VALID_PARAMETERS, + parameterGroups: VALID_PARAMETER_GROUPS, + conditions: VALID_CONDITIONS, + etag: 'etag-1234-1', + }; + + const invalidEtagTemplate = deepCopy(sourceTemplate) + invalidEtags.forEach((invalidEtag) => { + invalidEtagTemplate.etag = invalidEtag; + const jsonString = JSON.stringify(invalidEtagTemplate); + it(`should throw if the ETag is ${JSON.stringify(invalidEtag)}`, () => { + expect(() => getRemoteConfig().createTemplateFromJSON(jsonString)) + .to.throw(`Invalid Remote Config template: ${jsonString}`); + }); + }); + + it('should succeed when a valid json string is provided', () => { + const jsonString = JSON.stringify(sourceTemplate); + const newTemplate = getRemoteConfig().createTemplateFromJSON(jsonString); + expect(newTemplate.etag).to.equal(sourceTemplate.etag); + expect(() => { + (currentTemplate as any).etag = 'new-etag'; + }).to.throw( + 'Cannot set property etag of # which has only a getter' + ); + expect(newTemplate.conditions.length).to.equal(2); + expect(newTemplate.conditions).to.deep.equal(VALID_CONDITIONS); + expect(newTemplate.parameters).to.deep.equal(VALID_PARAMETERS); + expect(newTemplate.parameterGroups).to.deep.equal(VALID_PARAMETER_GROUPS); + }); + }); +}); diff --git a/test/integration/security-rules.spec.ts b/test/integration/security-rules.spec.ts new file mode 100644 index 0000000000..eda5f000f4 --- /dev/null +++ b/test/integration/security-rules.spec.ts @@ -0,0 +1,305 @@ +/*! + * 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 * as chai from 'chai'; +import { Ruleset, RulesetMetadata, getSecurityRules } from '../../lib/security-rules/index'; + +const expect = chai.expect; + +const RULES_FILE_NAME = 'firestore.rules'; + +const SAMPLE_FIRESTORE_RULES = `service cloud.firestore { + // Admin Node.js integration test run at ${new Date().toUTCString()} + match /databases/{database}/documents { + match /{document=**} { + allow read, write: if false; + } + } +}`; + +const SAMPLE_STORAGE_RULES = `service firebase.storage { + // Admin Node.js integration test run at ${new Date().toUTCString()} + match /b/{bucket}/o { + match /{allPaths=**} { + allow read, write: if request.auth != null; + } + } +}`; + +const RULESET_NAME_PATTERN = /[0-9a-zA-Z-]+/; + + +describe('admin.securityRules', () => { + + const rulesetsToDelete: string[] = []; + + function scheduleForDelete(ruleset: Ruleset): void { + rulesetsToDelete.push(ruleset.name); + } + + function unscheduleForDelete(ruleset: Ruleset): void { + rulesetsToDelete.splice(rulesetsToDelete.indexOf(ruleset.name), 1); + } + + function deleteTempRulesets(): Promise { + const promises: Array> = []; + rulesetsToDelete.forEach((rs) => { + promises.push(getSecurityRules().deleteRuleset(rs)); + }); + rulesetsToDelete.splice(0, rulesetsToDelete.length); // Clear out the array. + return Promise.all(promises); + } + + function createTemporaryRuleset(): Promise { + const name = 'firestore.rules'; + const rulesFile = getSecurityRules().createRulesFileFromSource(name, SAMPLE_FIRESTORE_RULES); + return getSecurityRules().createRuleset(rulesFile) + .then((ruleset) => { + scheduleForDelete(ruleset); + return ruleset; + }); + } + + afterEach(() => { + return deleteTempRulesets(); + }); + + describe('createRulesFileFromSource()', () => { + it('creates a RulesFile from the source string', () => { + const rulesFile = getSecurityRules().createRulesFileFromSource( + RULES_FILE_NAME, SAMPLE_FIRESTORE_RULES); + expect(rulesFile.name).to.equal(RULES_FILE_NAME); + expect(rulesFile.content).to.equal(SAMPLE_FIRESTORE_RULES); + }); + + it('creates a RulesFile from the source Buffer', () => { + const rulesFile = getSecurityRules().createRulesFileFromSource( + 'firestore.rules', Buffer.from(SAMPLE_FIRESTORE_RULES, 'utf-8')); + expect(rulesFile.name).to.equal(RULES_FILE_NAME); + expect(rulesFile.content).to.equal(SAMPLE_FIRESTORE_RULES); + }); + }); + + describe('createRuleset()', () => { + it('creates a new Ruleset from a given RulesFile', () => { + const rulesFile = getSecurityRules().createRulesFileFromSource( + RULES_FILE_NAME, SAMPLE_FIRESTORE_RULES); + return getSecurityRules().createRuleset(rulesFile) + .then((ruleset) => { + scheduleForDelete(ruleset); + verifyFirestoreRuleset(ruleset); + }); + }); + + it('rejects with invalid-argument when the source is invalid', () => { + const rulesFile = getSecurityRules().createRulesFileFromSource( + RULES_FILE_NAME, 'invalid syntax'); + return getSecurityRules().createRuleset(rulesFile) + .should.eventually.be.rejected.and.have.property('code', 'security-rules/invalid-argument'); + }); + }); + + describe('getRuleset()', () => { + it('rejects with not-found when the Ruleset does not exist', () => { + const nonExistingName = '00000000-1111-2222-3333-444444444444'; + return getSecurityRules().getRuleset(nonExistingName) + .should.eventually.be.rejected.and.have.property('code', 'security-rules/not-found'); + }); + + it('rejects with invalid-argument when the Ruleset name is invalid', () => { + return getSecurityRules().getRuleset('invalid uuid') + .should.eventually.be.rejected.and.have.property('code', 'security-rules/invalid-argument'); + }); + + it('resolves with existing Ruleset', () => { + return createTemporaryRuleset() + .then((expectedRuleset) => + getSecurityRules().getRuleset(expectedRuleset.name) + .then((actualRuleset) => { + expect(actualRuleset).to.deep.equal(expectedRuleset); + }), + ); + }); + }); + + describe('Cloud Firestore', () => { + let oldRuleset: Ruleset | null = null; + let newRuleset: Ruleset | null = null; + + function revertFirestoreRulesetIfModified(): Promise { + if (!newRuleset || !oldRuleset) { + return Promise.resolve(); + } + + return getSecurityRules().releaseFirestoreRuleset(oldRuleset); + } + + afterEach(() => { + return revertFirestoreRulesetIfModified(); + }); + + it('getFirestoreRuleset() returns the Ruleset currently in effect', () => { + return getSecurityRules().getFirestoreRuleset() + .then((ruleset) => { + expect(ruleset.name).to.match(RULESET_NAME_PATTERN); + const createTime = new Date(ruleset.createTime); + expect(ruleset.createTime).equals(createTime.toUTCString()); + + expect(ruleset.source.length).to.equal(1); + }); + }); + + it('releaseFirestoreRulesetFromSource() applies the specified Ruleset to Firestore', () => { + return getSecurityRules().getFirestoreRuleset() + .then((ruleset) => { + oldRuleset = ruleset; + return getSecurityRules().releaseFirestoreRulesetFromSource(SAMPLE_FIRESTORE_RULES); + }) + .then((ruleset) => { + scheduleForDelete(ruleset); + newRuleset = ruleset; + + expect(ruleset.name).to.not.equal(oldRuleset!.name); + verifyFirestoreRuleset(ruleset); + return getSecurityRules().getFirestoreRuleset(); + }) + .then((ruleset) => { + expect(ruleset.name).to.equal(newRuleset!.name); + verifyFirestoreRuleset(ruleset); + }); + }); + }); + + describe('Cloud Storage', () => { + let oldRuleset: Ruleset | null = null; + let newRuleset: Ruleset | null = null; + + function revertStorageRulesetIfModified(): Promise { + if (!newRuleset || !oldRuleset) { + return Promise.resolve(); + } + + return getSecurityRules().releaseStorageRuleset(oldRuleset); + } + + afterEach(() => { + return revertStorageRulesetIfModified(); + }); + + it('getStorageRuleset() returns the currently applied Storage rules', () => { + return getSecurityRules().getStorageRuleset() + .then((ruleset) => { + expect(ruleset.name).to.match(RULESET_NAME_PATTERN); + const createTime = new Date(ruleset.createTime); + expect(ruleset.createTime).equals(createTime.toUTCString()); + + expect(ruleset.source.length).to.equal(1); + }); + }); + + it('releaseStorageRulesetFromSource() applies the specified Ruleset to Storage', () => { + return getSecurityRules().getStorageRuleset() + .then((ruleset) => { + oldRuleset = ruleset; + return getSecurityRules().releaseStorageRulesetFromSource(SAMPLE_STORAGE_RULES); + }) + .then((ruleset) => { + scheduleForDelete(ruleset); + newRuleset = ruleset; + + expect(ruleset.name).to.not.equal(oldRuleset!.name); + expect(ruleset.name).to.match(RULESET_NAME_PATTERN); + const createTime = new Date(ruleset.createTime); + expect(ruleset.createTime).equals(createTime.toUTCString()); + return getSecurityRules().getStorageRuleset(); + }) + .then((ruleset) => { + expect(ruleset.name).to.equal(newRuleset!.name); + }); + }); + }); + + describe('listRulesetMetadata()', () => { + it('lists all available Rulesets in pages', () => { + function listAllRulesets( + pageToken?: string, results: RulesetMetadata[] = []): Promise { + + return getSecurityRules().listRulesetMetadata(100, pageToken) + .then((page) => { + results.push(...page.rulesets); + if (page.nextPageToken) { + return listAllRulesets(page.nextPageToken, results); + } + + return results; + }); + } + + return Promise.all([createTemporaryRuleset(), createTemporaryRuleset()]) + .then((expectedRulesets) => { + return listAllRulesets().then((actualRulesets) => { + expectedRulesets.forEach((expectedRuleset) => { + expect(actualRulesets.map((r) => r.name)).to.deep.include(expectedRuleset.name); + }); + }); + }); + }); + + it('lists the specified number of Rulesets', () => { + return getSecurityRules().listRulesetMetadata(2) + .then((page) => { + expect(page.rulesets.length).to.be.at.most(2); + expect(page.rulesets.length).to.be.at.least(1); + }); + }); + }); + + describe('deleteRuleset()', () => { + it('rejects with not-found when the Ruleset does not exist', () => { + const nonExistingName = '00000000-1111-2222-3333-444444444444'; + return getSecurityRules().deleteRuleset(nonExistingName) + .should.eventually.be.rejected.and.have.property('code', 'security-rules/not-found'); + }); + + it('rejects with invalid-argument when the Ruleset name is invalid', () => { + return getSecurityRules().deleteRuleset('invalid uuid') + .should.eventually.be.rejected.and.have.property('code', 'security-rules/invalid-argument'); + }); + + it('deletes existing Ruleset', () => { + return createTemporaryRuleset().then((ruleset) => { + return getSecurityRules().deleteRuleset(ruleset.name) + .then(() => { + return getSecurityRules().getRuleset(ruleset.name) + .should.eventually.be.rejected.and.have.property('code', 'security-rules/not-found'); + }) + .then(() => { + unscheduleForDelete(ruleset); // Already deleted. + }); + }); + }); + }); + + function verifyFirestoreRuleset(ruleset: Ruleset): void { + expect(ruleset.name).to.match(RULESET_NAME_PATTERN); + const createTime = new Date(ruleset.createTime); + expect(ruleset.createTime).equals(createTime.toUTCString()); + + expect(ruleset.source.length).to.equal(1); + expect(ruleset.source[0].name).to.equal(RULES_FILE_NAME); + expect(ruleset.source[0].content).to.equal(SAMPLE_FIRESTORE_RULES); + } +}); diff --git a/test/integration/setup.ts b/test/integration/setup.ts index 8d842d1b72..fa217120f2 100644 --- a/test/integration/setup.ts +++ b/test/integration/setup.ts @@ -14,72 +14,95 @@ * limitations under the License. */ -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 { + App, Credential, GoogleOAuthAccessToken, cert, deleteApp, initializeApp, +} from '../../lib/app/index' -/* tslint:disable:no-var-requires */ +// eslint-disable-next-line @typescript-eslint/no-var-requires const chalk = require('chalk'); -/* tslint:enable:no-var-requires */ export let databaseUrl: string; export let storageBucket: string; export let projectId: string; export let apiKey: string; -export let defaultApp: admin.app.App; -export let nullApp: admin.app.App; -export let nonNullApp: admin.app.App; +export let defaultApp: App; +export let nullApp: App; +export let nonNullApp: App; +export let noServiceAccountApp: App; export let cmdArgs: any; +export const isEmulator = !!process.env.FIREBASE_EMULATOR_HUB; + before(() => { + let getCredential: () => {credential?: Credential}; + let serviceAccountId: string; + /* 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, + 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(); - } 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; + 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: 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), + defaultApp = initializeApp({ + ...getCredential(), + projectId, databaseURL: databaseUrl, storageBucket, }); - nullApp = admin.initializeApp({ - credential: admin.credential.cert(serviceAccount), + nullApp = initializeApp({ + ...getCredential(), + projectId, databaseURL: databaseUrl, databaseAuthVariableOverride: null, storageBucket, }, 'null'); - nonNullApp = admin.initializeApp({ - credential: admin.credential.cert(serviceAccount), + nonNullApp = initializeApp({ + ...getCredential(), + projectId, databaseURL: databaseUrl, databaseAuthVariableOverride: { uid: generateRandomString(20), @@ -87,17 +110,51 @@ before(() => { storageBucket, }, 'nonNull'); + const noServiceAccountAppCreds = getCredential(); + if (noServiceAccountAppCreds.credential) { + noServiceAccountAppCreds.credential = new CertificatelessCredential( + noServiceAccountAppCreds.credential) + } + noServiceAccountApp = initializeApp({ + ...noServiceAccountAppCreds, + serviceAccountId, + projectId, + }, 'noServiceAccount'); + cmdArgs = minimist(process.argv.slice(2)); }); +after(() => { + return Promise.all([ + deleteApp(defaultApp), + deleteApp(nullApp), + deleteApp(nonNullApp), + deleteApp(noServiceAccountApp), + ]); +}); + +class CertificatelessCredential implements Credential { + private readonly delegate: Credential; + + constructor(delegate: Credential) { + this.delegate = delegate; + } + + public getAccessToken(): Promise { + return this.delegate.getAccessToken(); + } +} + /** * Generate a random string of the specified length, optionally using the specified alphabet. * - * @param {number} length The length of the string to generate. - * @return {string} A random string of the provided length. + * @param length The length of the string to generate. + * @param allowNumbers Whether to allow numbers in the generated string. The default is true. + * @return A random string of the provided length. */ -export function generateRandomString(length: number): string { - const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; +export function generateRandomString(length: number, allowNumbers = true): string { + const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' + + (allowNumbers ? '0123456789' : ''); let text = ''; for (let i = 0; i < length; i++) { text += alphabet.charAt(random(alphabet.length - 1)); diff --git a/test/integration/storage.spec.ts b/test/integration/storage.spec.ts index 284297e4f5..78d1c731b5 100644 --- a/test/integration/storage.spec.ts +++ b/test/integration/storage.spec.ts @@ -14,12 +14,14 @@ * limitations under the License. */ -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'; +import { getDownloadURL, getStorage } from '../../lib/storage/index'; +import { getFirebaseMetadata } from '../../src/storage/utils'; +import { FirebaseError } from '../../src/utils/error'; chai.should(); chai.use(chaiAsPromised); @@ -27,25 +29,62 @@ chai.use(chaiAsPromised); const expect = chai.expect; describe('admin.storage', () => { + let currentRef: File | null = null; + afterEach(async () => { + if (currentRef) { + await currentRef.delete(); + } + currentRef = null; + }); it('bucket() returns a handle to the default bucket', () => { - const bucket: Bucket = admin.storage().bucket(); + const bucket: Bucket = getStorage().bucket(); return verifyBucket(bucket, 'storage().bucket()') .should.eventually.be.fulfilled; - }).timeout(5000); + }); it('bucket(string) returns a handle to the specified bucket', () => { - const bucket: Bucket = admin.storage().bucket(projectId + '.appspot.com'); + const bucket: Bucket = getStorage().bucket(projectId + '.appspot.com'); return verifyBucket(bucket, 'storage().bucket(string)') .should.eventually.be.fulfilled; - }).timeout(5000); + }); + + it('getDownloadUrl returns a download URL', async () => { + const bucket = getStorage().bucket(projectId + '.appspot.com'); + currentRef = await verifyBucketDownloadUrl(bucket, 'testName'); + // Note: For now, this generates a download token when needed, but in the future it may not. + const metadata = await getFirebaseMetadata( + 'https://firebasestorage.googleapis.com/v0', + currentRef + ); + if (!metadata.downloadTokens) { + expect(getDownloadURL(currentRef)).to.eventually.throw( + new FirebaseError({ + code: 'storage/invalid-argument', + message: + '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.', + }) + ); + return; + } + const downloadUrl = await getDownloadURL(currentRef); + + const [token] = metadata.downloadTokens.split(','); + const storageEndpoint = `https://firebasestorage.googleapis.com/v0/b/${ + bucket.name + }/o/${encodeURIComponent(currentRef.name)}?alt=media&token=${token}`; + expect(downloadUrl).to.equal(storageEndpoint); + }); it('bucket(non-existing) returns a handle which can be queried for existence', () => { - const bucket: Bucket = admin.storage().bucket('non.existing'); + const bucket: Bucket = getStorage().bucket('non.existing'); return bucket.exists() .then((data) => { expect(data[0]).to.be.false; }); }); + }); function verifyBucket(bucket: Bucket, testName: string): Promise { @@ -59,10 +98,17 @@ function verifyBucket(bucket: Bucket, testName: string): Promise { expect(data[0].toString()).to.equal(expected); return file.delete(); }) - .then((resp) => { + .then(() => { return file.exists(); }) .then((data) => { expect(data[0], 'File not deleted').to.be.false; }); } + +async function verifyBucketDownloadUrl(bucket: Bucket, testName: string): Promise { + const expected: string = 'Hello World: ' + testName; + const file: File = bucket.file('data_' + Date.now() + '.txt'); + await file.save(expected) + return file; +} diff --git a/test/integration/typescript/package.json b/test/integration/typescript/package.json deleted file mode 100644 index 85395e6792..0000000000 --- a/test/integration/typescript/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "firebase-admin-typescript-test", - "version": "1.0.0", - "devDependencies": { - "@types/google-cloud__storage": "^1.1.1", - "@types/chai": "^3.4.35", - "@types/mocha": "^2.2.39", - "@types/node": "^7.0.8", - "chai": "^3.5.0", - "mocha": "^3.5.0", - "ts-node": "^3.3.0", - "typescript": "^2.4.2" - } -} \ No newline at end of file diff --git a/test/integration/typescript/src/example.test.ts b/test/integration/typescript/src/example.test.ts deleted file mode 100644 index b52e203f5c..0000000000 --- a/test/integration/typescript/src/example.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -/*! - * 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 initApp from './example' -import {expect} from 'chai'; -import {Bucket} from '@google-cloud/storage'; -import {Firestore} from '@google-cloud/firestore'; - -import * as admin from 'firebase-admin'; - -const serviceAccount = require('../mock.key.json'); - -describe('Init App', () => { - const app: admin.app.App = initApp(serviceAccount, 'TestApp'); - - it('Should return an initialized App', () => { - expect(app.name).to.equal('TestApp'); - }); - - it('Should return a Database client', () => { - const db = admin.database(app); - expect(db).to.be.instanceOf((admin.database as any).Database); - }); - - it('Should return a Database client for URL', () => { - const db = app.database('https://other-mock.firebaseio.com'); - expect(db).to.be.instanceOf((admin.database as any).Database); - }); - - it('Should return a Database ServerValue', () => { - const serverValue = admin.database.ServerValue; - expect(serverValue).to.not.be.null; - }); - - it('Should return a Cloud Storage client', () => { - const bucket: Bucket = app.storage().bucket('TestBucket'); - expect(bucket.name).to.equal('TestBucket') - }); - - it('Should return a Firestore client from the app', () => { - const firestore: Firestore = app.firestore(); - expect(firestore).to.be.instanceOf(admin.firestore.Firestore); - }); - - it('Should return a Firestore client', () => { - const firestore: Firestore = admin.firestore(app); - expect(firestore).to.be.instanceOf(admin.firestore.Firestore); - }); - - it('Should return a Firestore FieldValue', () => { - const fieldValue = admin.firestore.FieldValue; - expect(fieldValue).to.not.be.null; - }); - - it('Should return a DocumentReference', () => { - const ref: admin.firestore.DocumentReference = admin.firestore(app).collection('test').doc(); - expect(ref).to.not.be.null; - }); -}); diff --git a/test/resources/invalid_model.tflite b/test/resources/invalid_model.tflite new file mode 100644 index 0000000000..d8482f4362 --- /dev/null +++ b/test/resources/invalid_model.tflite @@ -0,0 +1 @@ +This is not a tflite file. diff --git a/test/resources/mock.impersonated_key.json b/test/resources/mock.impersonated_key.json new file mode 100644 index 0000000000..debc6a2d31 --- /dev/null +++ b/test/resources/mock.impersonated_key.json @@ -0,0 +1,11 @@ +{ + "delegates": [], + "service_account_impersonation_url": "", + "source_credentials": { + "client_id": "client_id", + "client_secret": "client_secret", + "refresh_token": "refresh_token", + "type": "authorized_user" + }, + "type": "impersonated_service_account" + } 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 465d1ff7f4..a528dd5497 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"); @@ -24,40 +25,43 @@ 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 {Certificate, Credential, CertCredential, GoogleOAuthAccessToken} from '../../src/auth/credential'; +import { AppOptions } from '../../src/firebase-namespace-api'; +import { FirebaseApp } from '../../src/app/firebase-app'; +import { Credential, GoogleOAuthAccessToken, cert } from '../../src/app/index'; +import { ComputeEngineCredential } from '../../src/app/credential-internal'; -const ALGORITHM = 'RS256'; +const ALGORITHM = 'RS256' as const; const ONE_HOUR_IN_SECONDS = 60 * 60; +const TEN_MINUTES_IN_SECONDS = 10 * 60; -export let uid = 'someUid'; -export let projectId = 'project_id'; -export let developerClaims = { +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', }; -export let appName = 'mock-app-name'; +export const appName = 'mock-app-name'; -export let serviceName = 'mock-service-name'; +export const serviceName = 'mock-service-name'; -export let databaseURL = 'https://databaseName.firebaseio.com'; +export const databaseURL = 'https://databaseName.firebaseio.com'; -export let databaseAuthVariableOverride = { 'some#string': 'some#val' }; +export const databaseAuthVariableOverride = { 'some#string': 'some#val' }; -export let storageBucket = 'bucketName.appspot.com'; +export const storageBucket = 'bucketName.appspot.com'; -export let credential = new CertCredential(path.resolve(__dirname, './mock.key.json')); +export const credential = cert(path.resolve(__dirname, './mock.key.json')); -export let appOptions: FirebaseAppOptions = { +export const appOptions: AppOptions = { credential, databaseURL, storageBucket, }; -export let appOptionsWithOverride: FirebaseAppOptions = { +export const appOptionsWithOverride: AppOptions = { credential, databaseAuthVariableOverride, databaseURL, @@ -65,15 +69,15 @@ export let appOptionsWithOverride: FirebaseAppOptions = { projectId, }; -export let appOptionsNoAuth: FirebaseAppOptions = { +export const appOptionsNoAuth: AppOptions = { databaseURL, }; -export let appOptionsNoDatabaseUrl: FirebaseAppOptions = { +export const appOptionsNoDatabaseUrl: AppOptions = { credential, }; -export let appOptionsAuthDB: FirebaseAppOptions = { +export const appOptionsAuthDB: AppOptions = { credential, databaseURL, }; @@ -85,73 +89,84 @@ export class MockCredential implements Credential { expires_in: 3600, }); } +} - public getCertificate(): Certificate { - return null; +export class MockComputeEngineCredential extends ComputeEngineCredential { + public getAccessToken(): Promise { + return Promise.resolve({ + access_token: 'mock-token', + expires_in: 3600, + }); + } + + public getIDToken(): Promise { + return Promise.resolve('mockIdToken'); } } -export function app(): FirebaseApp { - const namespaceInternals = new FirebaseNamespace().INTERNAL; - namespaceInternals.removeApp = _.noop; - return new FirebaseApp(appOptions, appName, namespaceInternals); +export function app(altName?: string): FirebaseApp { + return new FirebaseApp(appOptions, altName || appName); } export function mockCredentialApp(): FirebaseApp { return new FirebaseApp({ credential: new MockCredential(), databaseURL, - }, appName, new FirebaseNamespace().INTERNAL); + }, appName); } -export function appWithOptions(options: FirebaseAppOptions): FirebaseApp { - return new FirebaseApp(options, appName, new FirebaseNamespace().INTERNAL); +export function appWithOptions(options: AppOptions): FirebaseApp { + return new FirebaseApp(options, appName); } export function appReturningNullAccessToken(): FirebaseApp { + const nullFn: () => Promise | null = () => null; return new FirebaseApp({ credential: { - getAccessToken: () => null, - getCertificate: () => credential.getCertificate(), + getAccessToken: nullFn, } as any, databaseURL, - }, appName, new FirebaseNamespace().INTERNAL); + projectId, + }, appName); } export function appReturningMalformedAccessToken(): FirebaseApp { return new FirebaseApp({ credential: { getAccessToken: () => 5, - getCertificate: () => credential.getCertificate(), } as any, databaseURL, - }, appName, new FirebaseNamespace().INTERNAL); + projectId, + }, appName); } export function appRejectedWhileFetchingAccessToken(): FirebaseApp { return new FirebaseApp({ credential: { getAccessToken: () => Promise.reject(new Error('Promise intentionally rejected.')), - getCertificate: () => credential.getCertificate(), } as any, databaseURL, - }, appName, new FirebaseNamespace().INTERNAL); + projectId, + }, appName); } -export let refreshToken = { +export const refreshToken = { clientId: 'mock-client-id', clientSecret: 'mock-client-secret', refreshToken: 'mock-refresh-token', type: 'refreshToken', }; -/* tslint:disable:no-var-requires */ -export let certificateObject = require('./mock.key.json'); -/* tslint:enable:no-var-requires */ +// 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'); // Randomly generated key pairs that don't correspond to anything related to Firebase or GCP -export let keyPairs = [ - /* tslint:disable:max-line-length */ +export const keyPairs = [ + /* 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', @@ -161,16 +176,40 @@ export let 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 = [ + /* 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', + }, + { + 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', + }, + /* 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. * * @param {object} overrides Overrides for the generated token's attributes. + * @param {object} claims Extra claims to add to the token. + * @param {string} secret Custom key to sign the token with. * @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, secret?: string): string { const options = _.assign({ audience: projectId, expiresIn: ONE_HOUR_IN_SECONDS, @@ -182,28 +221,93 @@ export function generateIdToken(overrides?: object): string { }, }, overrides); - return jwt.sign(developerClaims, certificateObject.private_key, options); + const payload = { + ...developerClaims, + ...claims, + }; + + return jwt.sign(payload, secret ?? certificateObject.private_key, options); } -export function firebaseServiceFactory( - firebaseApp: FirebaseApp, - extendApp: (props: object) => void, -): FirebaseServiceInterface { - const result = { - app: firebaseApp, - INTERNAL: {}, +/** + * Generates a mocked Auth Blocking token. + * + * @param overrides Overrides for the generated token's attributes. + * @param claims Extra claims to add to the token. + * @param {string} secret Custom key to sign the token with. + * @return A mocked Auth Blocking token with any provided overrides included. + */ +export function generateAuthBlockingToken(overrides?: object, claims?: object, secret?: string): string { + const options = _.assign({ + audience: `https://us-central1-${projectId}.cloudfunctions.net/functionName`, + expiresIn: TEN_MINUTES_IN_SECONDS, + issuer: 'https://securetoken.google.com/' + projectId, + subject: uid, + algorithm: ALGORITHM, + header: { + kid: certificateObject.private_key_id, + }, + }, overrides); + + const payload = { + ...developerClaims, + ...claims, }; - return result as FirebaseServiceInterface; + + return jwt.sign(payload, secret ?? certificateObject.private_key, options); +} + +/** + * Generates a mocked Firebase session cookie. + * + * @param {object=} overrides Overrides for the generated token's attributes. + * @param {number=} expiresIn Optional custom session cookie expiration in seconds. + * @return {string} A mocked Firebase session cookie with any provided overrides included. + */ +export function generateSessionCookie(overrides?: object, expiresIn?: number): string { + const options = _.assign({ + audience: projectId, + expiresIn: expiresIn || ONE_HOUR_IN_SECONDS, + issuer: 'https://session.firebase.google.com/' + projectId, + subject: uid, + algorithm: ALGORITHM, + header: { + kid: certificateObject.private_key_id, + }, + }, overrides); + + 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 = (timeout: number) => undefined; + public setTimeout: (_: number) => void = () => undefined; } /** Mock stream passthrough class with dummy abort method. */ export class MockStream extends stream.PassThrough { - public abort = () => undefined; + public abort: () => void = () => undefined; } /** diff --git a/test/resources/model1.tflite b/test/resources/model1.tflite new file mode 100644 index 0000000000..c4b71b7a22 Binary files /dev/null and b/test/resources/model1.tflite differ 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..bd048bd9e4 --- /dev/null +++ b/test/unit/app-check/app-check-api-client-internal.spec.ts @@ -0,0 +1,367 @@ +/*! + * @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/app/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 = { + token: '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.token); + expect(resp.ttlMillis).to.deep.equal(3000); + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: `https://firebaseappcheck.googleapis.com/v1/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.token); + expect(resp.ttlMillis).to.deep.equal(ttlMillis); + }); + }); + }); + }); + + describe('verifyReplayProtection', () => { + it('should reject when project id is not available', () => { + return clientWithoutProjectId.verifyReplayProtection(TEST_TOKEN_TO_EXCHANGE) + .should.eventually.be.rejectedWith(noProjectId); + }); + + it('should throw given no token', () => { + expect(() => { + (apiClient as any).verifyReplayProtection(undefined); + }).to.throw('`token` must be a non-empty string.'); + }); + + [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop].forEach((invalidToken) => { + it('should throw given a non-string token: ' + JSON.stringify(invalidToken), () => { + expect(() => { + apiClient.verifyReplayProtection(invalidToken as any); + }).to.throw('`token` must be a non-empty string.'); + }); + }); + + it('should throw given an empty string token', () => { + expect(() => { + apiClient.verifyReplayProtection(''); + }).to.throw('`token` 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.verifyReplayProtection(TEST_TOKEN_TO_EXCHANGE) + .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.verifyReplayProtection(TEST_TOKEN_TO_EXCHANGE) + .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.verifyReplayProtection(TEST_TOKEN_TO_EXCHANGE) + .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.verifyReplayProtection(TEST_TOKEN_TO_EXCHANGE) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + ['', 'abc', '3s2', 'sssa', '3.000000001', '3.2', null, NaN, [], {}, 100, 1.2, -200, -2.4] + .forEach((invalidAlreadyConsumed) => { + it(`should throw if the returned alreadyConsumed value is: ${invalidAlreadyConsumed}`, () => { + const response = { alreadyConsumed: invalidAlreadyConsumed }; + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(response, 200)); + stubs.push(stub); + const expected = new FirebaseAppCheckError( + 'invalid-argument', '`alreadyConsumed` must be a boolean value.'); + return apiClient.verifyReplayProtection(TEST_TOKEN_TO_EXCHANGE) + .should.eventually.be.rejected.and.deep.include(expected); + }); + }); + + it('should resolve with the alreadyConsumed status on success', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({ alreadyConsumed: true }, 200)); + stubs.push(stub); + return apiClient.verifyReplayProtection(TEST_TOKEN_TO_EXCHANGE) + .then((alreadyConsumed) => { + expect(alreadyConsumed).to.equal(true); + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://firebaseappcheck.googleapis.com/v1beta/projects/test-project:verifyAppCheckToken', + headers: EXPECTED_HEADERS, + data: { app_check_token: TEST_TOKEN_TO_EXCHANGE } + }); + }); + }); + + [true, false].forEach((expectedAlreadyConsumed) => { + it(`should resolve with alreadyConsumed as ${expectedAlreadyConsumed} when alreadyConsumed + from server is: ${expectedAlreadyConsumed}`, () => { + const response = { alreadyConsumed: expectedAlreadyConsumed }; + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(response, 200)); + stubs.push(stub); + return apiClient.verifyReplayProtection(TEST_TOKEN_TO_EXCHANGE) + .then((alreadyConsumed) => { + expect(alreadyConsumed).to.equal(expectedAlreadyConsumed); + }); + }); + }); + + it(`should resolve with alreadyConsumed as false when alreadyConsumed + from server is: undefined`, () => { + const response = { }; + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(response, 200)); + stubs.push(stub); + return apiClient.verifyReplayProtection(TEST_TOKEN_TO_EXCHANGE) + .then((alreadyConsumed) => { + expect(alreadyConsumed).to.equal(false); + }); + }); + }); + +}); 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..62b8eeac64 --- /dev/null +++ b/test/unit/app-check/app-check.spec.ts @@ -0,0 +1,340 @@ +/*! + * @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/app/firebase-app'; +import { AppCheck } from '../../../src/app-check/index'; +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 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 + .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', + 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); + expect(tokenResponse.alreadyConsumed).equals(undefined); + }); + }); + + it('should throw given an invalid options', () => { + [null, 100, -100, 'abc', [], true].forEach((invalidOptions) => { + expect(() => { + return appCheck.verifyToken('token', invalidOptions as any) + }).to.throw( + 'VerifyAppCheckTokenOptions must be a non-null object.'); + }); + }); + + it('should call verifyReplayProtection when consume is set to true', () => { + const response = { + sub: 'app-id', + iss: 'https://firebaseappcheck.googleapis.com/123456', + app_id: 'app-id', + aud: ['123456', 'project-id'], + exp: 1617741496, + iat: 1516239022, + }; + const verifierStub = sinon + .stub(AppCheckTokenVerifier.prototype, 'verifyToken') + .resolves(response); + const replayStub = sinon + .stub(AppCheckApiClient.prototype, 'verifyReplayProtection') + .resolves(true); + stubs.push(verifierStub); + stubs.push(replayStub); + return appCheck.verifyToken('token', { consume: true }) + .then((tokenResponse) => { + expect(tokenResponse.appId).equals('app-id'); + expect(tokenResponse.token).equals(response); + expect(tokenResponse.alreadyConsumed).equals(true); + + expect(verifierStub).to.have.been.calledOnce.and.calledWith('token'); + expect(replayStub).to.have.been.calledOnce.and.calledWith('token'); + }); + }); + + it('should not call verifyReplayProtection when consume is set to false', () => { + const response = { + sub: 'app-id', + iss: 'https://firebaseappcheck.googleapis.com/123456', + app_id: 'app-id', + aud: ['123456', 'project-id'], + exp: 1617741496, + iat: 1516239022, + }; + const verifierStub = sinon + .stub(AppCheckTokenVerifier.prototype, 'verifyToken') + .resolves(response); + const replayStub = sinon + .stub(AppCheckApiClient.prototype, 'verifyReplayProtection') + .resolves(true); + stubs.push(verifierStub); + stubs.push(replayStub); + return appCheck.verifyToken('token', { consume: false }) + .then((tokenResponse) => { + expect(tokenResponse.appId).equals('app-id'); + expect(tokenResponse.token).equals(response); + expect(tokenResponse.alreadyConsumed).equals(undefined); + + expect(verifierStub).to.have.been.calledOnce.and.calledWith('token'); + expect(replayStub).to.not.have.been.called; + }); + }); + + it('should not call verifyReplayProtection when consume is set to undefined', () => { + const response = { + sub: 'app-id', + iss: 'https://firebaseappcheck.googleapis.com/123456', + app_id: 'app-id', + aud: ['123456', 'project-id'], + exp: 1617741496, + iat: 1516239022, + }; + const verifierStub = sinon + .stub(AppCheckTokenVerifier.prototype, 'verifyToken') + .resolves(response); + const replayStub = sinon + .stub(AppCheckApiClient.prototype, 'verifyReplayProtection') + .resolves(true); + stubs.push(verifierStub); + stubs.push(replayStub); + return appCheck.verifyToken('token', { consume: undefined }) + .then((tokenResponse) => { + expect(tokenResponse.appId).equals('app-id'); + expect(tokenResponse.token).equals(response); + expect(tokenResponse.alreadyConsumed).equals(undefined); + + expect(verifierStub).to.have.been.calledOnce.and.calledWith('token'); + expect(replayStub).to.not.have.been.called; + }); + }); + + it('should not call verifyReplayProtection for an invalid token when consume is set to true', () => { + const verifierStub = sinon + .stub(AppCheckTokenVerifier.prototype, 'verifyToken') + .rejects(INTERNAL_ERROR); + const replayStub = sinon + .stub(AppCheckApiClient.prototype, 'verifyReplayProtection') + .resolves(true); + stubs.push(verifierStub); + stubs.push(replayStub); + appCheck.verifyToken('token', { consume: true }) + .should.eventually.be.rejected.and.deep.equal(INTERNAL_ERROR); + expect(verifierStub).to.have.been.calledOnce.and.calledWith('token'); + return expect(replayStub).to.not.have.been.called; + }); + + it('should resolve with VerifyAppCheckTokenResponse on success with alreadyConsumed set', () => { + const response = { + sub: 'app-id', + iss: 'https://firebaseappcheck.googleapis.com/123456', + app_id: 'app-id', + aud: ['123456', 'project-id'], + exp: 1617741496, + iat: 1516239022, + }; + const verifierStub = sinon + .stub(AppCheckTokenVerifier.prototype, 'verifyToken') + .resolves(response); + const replayStub = sinon + .stub(AppCheckApiClient.prototype, 'verifyReplayProtection') + .resolves(false); + stubs.push(verifierStub); + stubs.push(replayStub); + return appCheck.verifyToken('token', { consume: true }) + .then((tokenResponse) => { + expect(tokenResponse.appId).equals('app-id'); + expect(tokenResponse.token).equals(response); + expect(tokenResponse.alreadyConsumed).equals(false); + + expect(verifierStub).to.have.been.calledOnce.and.calledWith('token'); + expect(replayStub).to.have.been.calledOnce.and.calledWith('token'); + }); + }); + }); +}); 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..f892c9fa08 --- /dev/null +++ b/test/unit/app-check/token-generator.spec.ts @@ -0,0 +1,356 @@ +/*! + * @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/app/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 FIVE_MIN_IN_SECONDS = 5 * 60; +const FIREBASE_APP_CHECK_AUDIENCE = 'https://firebaseappcheck.googleapis.com/google.firebase.appcheck.v1.TokenExchangeService'; + +/** + * Verifies a token is signed with the private key corresponding to the provided public key. + * + * @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'); + }); + + 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); + + return tokenGenerator.createCustomToken(APP_ID) + .then((token) => { + const decoded = jwt.decode(token); + const expected: { [key: string]: any } = { + app_id: APP_ID, + iat: 1, + exp: FIVE_MIN_IN_SECONDS + 1, + aud: FIREBASE_APP_CHECK_AUDIENCE, + iss: mocks.certificateObject.client_email, + sub: mocks.certificateObject.client_email, + }; + + expect(decoded).to.deep.equal(expected); + }); + }); + + [{}, { 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 } = { + app_id: APP_ID, + iat: 1, + exp: FIVE_MIN_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 } = { + app_id: APP_ID, + iat: 1, + exp: FIVE_MIN_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); + + 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 five minutes', () => { + clock = sinon.useFakeTimers(1000); + + let token: string; + return tokenGenerator.createCustomToken(APP_ID) + .then((result) => { + token = result; + + clock!.tick((FIVE_MIN_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 signing 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 signing 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..c6cf897c05 --- /dev/null +++ b/test/unit/app-check/token-verifier.spec.ts @@ -0,0 +1,243 @@ +/*! + * @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: 'PS256', + }); + 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, + 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, + app_id: mocks.appId, + }); + }); + + }); +}); diff --git a/test/unit/app/credential-internal.spec.ts b/test/unit/app/credential-internal.spec.ts new file mode 100644 index 0000000000..93afa1fe9f --- /dev/null +++ b/test/unit/app/credential-internal.spec.ts @@ -0,0 +1,752 @@ +/*! + * @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'; + +// Use untyped import syntax for Node built-ins +import fs = require('fs'); +import path = require('path'); + +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 utils from '../utils'; +import * as mocks from '../../resources/mocks'; + +import { + GoogleOAuthAccessToken, Credential +} from '../../../src/app/index'; +import { + RefreshTokenCredential, ServiceAccountCredential, + ComputeEngineCredential, getApplicationDefault, isApplicationDefault, ImpersonatedServiceAccountCredential +} from '../../../src/app/credential-internal'; +import { HttpClient } from '../../../src/utils/api-request'; +import { Agent } from 'https'; +import { FirebaseAppError } from '../../../src/utils/error'; +import { deepCopy } from '../../../src/utils/deep-copy'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +const GCLOUD_CREDENTIAL_SUFFIX = 'gcloud/application_default_credentials.json'; +if (!process.env.HOME) { + throw new Error('$HOME environment variable must be set to run the tests.'); +} +const GCLOUD_CREDENTIAL_PATH = path.resolve(process.env.HOME!, '.config', GCLOUD_CREDENTIAL_SUFFIX); +const MOCK_REFRESH_TOKEN_CONFIG = { + client_id: 'test_client_id', + client_secret: 'test_client_secret', + type: 'authorized_user', + refresh_token: 'test_token', +}; +const MOCK_IMPERSONATED_TOKEN_CONFIG = { + delegates: [], + service_account_impersonation_url: '', + source_credentials: { + client_id: 'test_client_id', + client_secret: 'test_client_secret', + refresh_token: 'test_refresh_token', + type: 'authorized_user' + }, + type: 'impersonated_service_account' +} + +const ONE_HOUR_IN_SECONDS = 60 * 60; +const FIVE_MINUTES_IN_SECONDS = 5 * 60; + + +describe('Credential', () => { + let mockCertificateObject: any; + let oldProcessEnv: NodeJS.ProcessEnv; + let getTokenScope: nock.Scope; + let mockedRequests: nock.Scope[] = []; + + before(() => { + getTokenScope = nock('https://accounts.google.com') + .persist() + .post('/o/oauth2/token') + .reply(200, { + access_token: utils.generateRandomAccessToken(), + token_type: 'Bearer', + expires_in: 3600, + }, { + 'cache-control': 'no-cache, no-store, max-age=0, must-revalidate', + }); + }); + + after(() => getTokenScope.done()); + + beforeEach(() => { + mockCertificateObject = _.clone(mocks.certificateObject); + oldProcessEnv = process.env; + }); + + afterEach(() => { + _.forEach(mockedRequests, (mockedRequest) => mockedRequest.done()); + mockedRequests = []; + process.env = oldProcessEnv; + }); + + describe('ServiceAccountCredential', () => { + const invalidFilePaths = [null, NaN, 0, 1, true, false, undefined, _.noop]; + invalidFilePaths.forEach((invalidFilePath) => { + it('should throw if called with non-string argument: ' + JSON.stringify(invalidFilePath), () => { + expect(() => new ServiceAccountCredential(invalidFilePath as any)) + .to.throw('Service account must be an object'); + }); + }); + + it('should throw if called with the path to a non-existent file', () => { + expect(() => new ServiceAccountCredential('invalid-file')) + .to.throw('Failed to parse service account json file: Error: ENOENT: no such file or directory'); + }); + + it('should throw if called with the path to an invalid file', () => { + const invalidPath = path.resolve(__dirname, '../../resources/unparsable.key.json'); + expect(() => new ServiceAccountCredential(invalidPath)) + .to.throw('Failed to parse service account json file: SyntaxError'); + }); + + it('should throw if called with an empty string path', () => { + expect(() => new ServiceAccountCredential('')) + .to.throw('Failed to parse service account json file: Error: ENOENT: no such file or directory'); + }); + + it('should throw given an object without a "project_id" property', () => { + const invalidCertificate = _.omit(mocks.certificateObject, 'project_id'); + expect(() => new ServiceAccountCredential(invalidCertificate as any)) + .to.throw('Service account object must contain a string "project_id" property'); + }); + + it('should throw given an object without a "private_key" property', () => { + const invalidCertificate = _.omit(mocks.certificateObject, 'private_key'); + expect(() => new ServiceAccountCredential(invalidCertificate as any)) + .to.throw('Service account object must contain a string "private_key" property'); + }); + + it('should throw given an object with an empty string "private_key" property', () => { + const invalidCertificate = _.clone(mocks.certificateObject); + invalidCertificate.private_key = ''; + expect(() => new ServiceAccountCredential(invalidCertificate as any)) + .to.throw('Service account object must contain a string "private_key" property'); + }); + + it('should throw given an object without a "client_email" property', () => { + const invalidCertificate = _.omit(mocks.certificateObject, 'client_email'); + expect(() => new ServiceAccountCredential(invalidCertificate as any)) + .to.throw('Service account object must contain a string "client_email" property'); + }); + + it('should throw given an object with an empty string "client_email" property', () => { + const invalidCertificate = _.clone(mocks.certificateObject); + invalidCertificate.client_email = ''; + expect(() => new ServiceAccountCredential(invalidCertificate as any)) + .to.throw('Service account object must contain a string "client_email" property'); + }); + + it('should throw given an object with a malformed "private_key" property', () => { + const invalidCertificate = _.clone(mocks.certificateObject); + invalidCertificate.private_key = 'malformed'; + expect(() => new ServiceAccountCredential(invalidCertificate as any)) + .to.throw('Failed to parse private key'); + }); + + it('should not throw given a valid path to a key file', () => { + const validPath = path.resolve(__dirname, '../../resources/mock.key.json'); + expect(() => new ServiceAccountCredential(validPath)).not.to.throw(); + }); + + it('should accept "clientEmail" in place of "client_email" for the certificate object', () => { + mockCertificateObject.clientEmail = mockCertificateObject.client_email; + delete mockCertificateObject.client_email; + + expect(() => new ServiceAccountCredential(mockCertificateObject)) + .not.to.throw(); + }); + + it('should accept "privateKey" in place of "private_key" for the certificate object', () => { + mockCertificateObject.privateKey = mockCertificateObject.private_key; + delete mockCertificateObject.private_key; + + expect(() => new ServiceAccountCredential(mockCertificateObject)) + .not.to.throw(); + }); + + it('should return a Credential', () => { + const c = new ServiceAccountCredential(mockCertificateObject); + expect(c).to.deep.include({ + projectId: mockCertificateObject.project_id, + clientEmail: mockCertificateObject.client_email, + privateKey: mockCertificateObject.private_key, + implicit: false, + }); + }); + + it('should return an implicit Credential', () => { + const c = new ServiceAccountCredential(mockCertificateObject, undefined, true); + expect(c).to.deep.include({ + projectId: mockCertificateObject.project_id, + clientEmail: mockCertificateObject.client_email, + privateKey: mockCertificateObject.private_key, + implicit: true, + }); + }); + + it('should create access tokens', () => { + const c = new ServiceAccountCredential(mockCertificateObject); + return c.getAccessToken().then((token) => { + expect(token.access_token).to.be.a('string').and.to.not.be.empty; + expect(token.expires_in).to.equal(ONE_HOUR_IN_SECONDS); + }); + }); + + describe('Error Handling', () => { + let httpStub: sinon.SinonStub; + before(() => { + httpStub = sinon.stub(HttpClient.prototype, 'send'); + }); + after(() => httpStub.restore()); + + it('should throw an error including error details', () => { + httpStub.rejects(utils.errorFrom({ + error: 'invalid_grant', + error_description: 'reason', + })); + const c = new ServiceAccountCredential(mockCertificateObject); + return expect(c.getAccessToken()).to.be + .rejectedWith('Error fetching access token: invalid_grant (reason)'); + }); + + it('should throw an error including error text payload', () => { + httpStub.rejects(utils.errorFrom('not json')); + const c = new ServiceAccountCredential(mockCertificateObject); + return expect(c.getAccessToken()).to.be + .rejectedWith('Error fetching access token: not json'); + }); + + it('should throw when the success response is malformed', () => { + httpStub.resolves(utils.responseFrom({})); + const c = new ServiceAccountCredential(mockCertificateObject); + return expect(c.getAccessToken()).to.be + .rejectedWith('Unexpected response while fetching access token'); + }); + }); + }); + + describe('RefreshTokenCredential', () => { + it('should throw if called with the path to an invalid file', () => { + const invalidPath = path.resolve(__dirname, '../../resources/unparsable.key.json'); + expect(() => new RefreshTokenCredential(invalidPath)) + .to.throw('Failed to parse refresh token file'); + }); + + it('should throw given an object without a "clientId" property', () => { + const invalidCredential = _.omit(mocks.refreshToken, 'clientId'); + expect(() => new RefreshTokenCredential(invalidCredential as any)) + .to.throw('Refresh token must contain a "client_id" property'); + }); + + it('should throw given an object without a "clientSecret" property', () => { + const invalidCredential = _.omit(mocks.refreshToken, 'clientSecret'); + expect(() => new RefreshTokenCredential(invalidCredential as any)) + .to.throw('Refresh token must contain a "client_secret" property'); + }); + + it('should throw given an object without a "refreshToken" property', () => { + const invalidCredential = _.omit(mocks.refreshToken, 'refreshToken'); + expect(() => new RefreshTokenCredential(invalidCredential as any)) + .to.throw('Refresh token must contain a "refresh_token" property'); + }); + + it('should throw given an object without a "type" property', () => { + const invalidCredential = _.omit(mocks.refreshToken, 'type'); + expect(() => new RefreshTokenCredential(invalidCredential as any)) + .to.throw('Refresh token must contain a "type" property'); + }); + + it('should return a Credential', () => { + const c = new RefreshTokenCredential(mocks.refreshToken); + expect(c).to.deep.include({ + implicit: false, + }); + }); + + it('should return an implicit Credential', () => { + const c = new RefreshTokenCredential(mocks.refreshToken, undefined, true); + expect(c).to.deep.include({ + implicit: true, + }); + }); + + it('should create access tokens', () => { + const scope = nock('https://www.googleapis.com') + .post('/oauth2/v4/token') + .reply(200, { + access_token: 'token', + token_type: 'Bearer', + expires_in: 60 * 60, + }, { + 'cache-control': 'no-cache, no-store, max-age=0, must-revalidate', + }); + mockedRequests.push(scope); + + const c = new RefreshTokenCredential(mocks.refreshToken); + return c.getAccessToken().then((token) => { + expect(token.access_token).to.be.a('string').and.to.not.be.empty; + expect(token.expires_in).to.greaterThan(FIVE_MINUTES_IN_SECONDS); + }); + }); + }); + + describe('ComputeEngineCredential', () => { + let httpStub: sinon.SinonStub; + beforeEach(() => httpStub = sinon.stub(HttpClient.prototype, 'send')); + afterEach(() => httpStub.restore()); + + it('should create access tokens', () => { + const expected: GoogleOAuthAccessToken = { + access_token: 'anAccessToken', + expires_in: 42, + }; + const response = utils.responseFrom(expected); + httpStub.resolves(response); + + const c = new ComputeEngineCredential(); + return c.getAccessToken().then((token) => { + expect(token.access_token).to.equal('anAccessToken'); + expect(token.expires_in).to.equal(42); + 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' }, + httpAgent: undefined, + }); + }); + }); + + it('should create id tokens', () => { + const expected = 'an-id-token-encoded'; + const response = utils.responseFrom(expected); + httpStub.resolves(response); + + const c = new ComputeEngineCredential(); + return c.getIDToken('my-audience.cloudfunctions.net').then((token) => { + expect(token).to.equal(expected); + expect(httpStub).to.have.been.calledOnce.and.calledWith({ + method: 'GET', + url: 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=my-audience.cloudfunctions.net', + headers: { 'Metadata-Flavor': 'Google' }, + httpAgent: undefined, + }); + }); + }); + + it('should discover project id', () => { + const expectedProjectId = 'test-project-id'; + const response = utils.responseFrom(expectedProjectId); + httpStub.resolves(response); + + const c = new ComputeEngineCredential(); + return c.getProjectId().then((projectId) => { + expect(projectId).to.equal(expectedProjectId); + expect(httpStub).to.have.been.calledOnce.and.calledWith({ + method: 'GET', + url: 'http://metadata.google.internal/computeMetadata/v1/project/project-id', + headers: { 'Metadata-Flavor': 'Google' }, + httpAgent: undefined, + }); + }); + }); + + it('should cache discovered project id', () => { + const expectedProjectId = 'test-project-id'; + const response = utils.responseFrom(expectedProjectId); + httpStub.resolves(response); + + const c = new ComputeEngineCredential(); + return c.getProjectId() + .then((projectId) => { + expect(projectId).to.equal(expectedProjectId); + return c.getProjectId(); + }) + .then((projectId) => { + expect(projectId).to.equal(expectedProjectId); + expect(httpStub).to.have.been.calledOnce.and.calledWith({ + method: 'GET', + url: 'http://metadata.google.internal/computeMetadata/v1/project/project-id', + headers: { 'Metadata-Flavor': 'Google' }, + httpAgent: undefined, + }); + }); + }); + + it('should reject when the metadata service is not available', () => { + httpStub.rejects(new FirebaseAppError('network-error', 'Failed to connect')); + + const c = new ComputeEngineCredential(); + return c.getProjectId().should.eventually + .rejectedWith('Failed to determine project ID: Failed to connect') + .and.have.property('code', 'app/invalid-credential'); + }); + + it('should reject when the metadata service responds with an error', () => { + const response = utils.errorFrom('Unexpected error'); + httpStub.rejects(response); + + const c = new ComputeEngineCredential(); + return c.getProjectId().should.eventually + .rejectedWith('Failed to determine project ID: Unexpected error') + .and.have.property('code', 'app/invalid-credential'); + }); + }); + + describe('ImpersonatedServiceAccountCredential', () => { + it('should throw if called with the path to an invalid file', () => { + const invalidPath = path.resolve(__dirname, '../../resources/unparsable.key.json'); + expect(() => new ImpersonatedServiceAccountCredential(invalidPath)) + .to.throw('Failed to parse impersonated service account file'); + }); + + it('should throw given an object without a "clientId" property', () => { + const invalidCredential = deepCopy(MOCK_IMPERSONATED_TOKEN_CONFIG); + invalidCredential.source_credentials.client_id = ''; + expect(() => new ImpersonatedServiceAccountCredential(invalidCredential as any)) + .to.throw('Impersonated Service Account must contain a "source_credentials.client_id" property.'); + }); + + it('should throw given an object without a "clientSecret" property', () => { + const invalidCredential = deepCopy(MOCK_IMPERSONATED_TOKEN_CONFIG); + invalidCredential.source_credentials.client_secret = ''; + expect(() => new ImpersonatedServiceAccountCredential(invalidCredential as any)) + .to.throw('Impersonated Service Account must contain a "source_credentials.client_secret" property.'); + }); + + it('should throw given an object without a "refreshToken" property', () => { + const invalidCredential = deepCopy(MOCK_IMPERSONATED_TOKEN_CONFIG); + invalidCredential.source_credentials.refresh_token = ''; + expect(() => new ImpersonatedServiceAccountCredential(invalidCredential as any)) + .to.throw('Impersonated Service Account must contain a "source_credentials.refresh_token" property.'); + }); + + it('should throw given an object without a "type" property', () => { + const invalidCredential = deepCopy(MOCK_IMPERSONATED_TOKEN_CONFIG); + invalidCredential.source_credentials.type = ''; + expect(() => new ImpersonatedServiceAccountCredential(invalidCredential as any)) + .to.throw('Impersonated Service Account must contain a "source_credentials.type" property.'); + }); + + it('should return a Credential', () => { + const c = new ImpersonatedServiceAccountCredential(MOCK_IMPERSONATED_TOKEN_CONFIG); + expect(c).to.deep.include({ + implicit: false, + }); + }); + + it('should return an implicit Credential', () => { + const c = new ImpersonatedServiceAccountCredential(MOCK_IMPERSONATED_TOKEN_CONFIG, undefined, true); + expect(c).to.deep.include({ + implicit: true, + }); + }); + + it('should create access tokens', () => { + const scope = nock('https://www.googleapis.com') + .post('/oauth2/v4/token') + .reply(200, { + access_token: 'token', + token_type: 'Bearer', + expires_in: 60 * 60, + }, { + 'cache-control': 'no-cache, no-store, max-age=0, must-revalidate', + }); + mockedRequests.push(scope); + + const c = new ImpersonatedServiceAccountCredential(MOCK_IMPERSONATED_TOKEN_CONFIG); + return c.getAccessToken().then((token) => { + expect(token.access_token).to.be.a('string').and.to.not.be.empty; + expect(token.expires_in).to.greaterThan(FIVE_MINUTES_IN_SECONDS); + }); + }); + }); + + describe('getApplicationDefault()', () => { + let fsStub: sinon.SinonStub; + + afterEach(() => { + if (fsStub) { + fsStub.restore(); + } + }); + + it('should return a CertCredential with GOOGLE_APPLICATION_CREDENTIALS set', () => { + process.env.GOOGLE_APPLICATION_CREDENTIALS = path.resolve(__dirname, '../../resources/mock.key.json'); + const c = getApplicationDefault(); + expect(c).to.be.an.instanceof(ServiceAccountCredential); + }); + + it('should return a ImpersonatedCredential with impersonated GOOGLE_APPLICATION_CREDENTIALS set', () => { + process.env.GOOGLE_APPLICATION_CREDENTIALS + = path.resolve(__dirname, '../../resources/mock.impersonated_key.json'); + const c = getApplicationDefault(); + expect(c).to.be.an.instanceof(ImpersonatedServiceAccountCredential); + }); + + it('should throw if explicitly pointing to an invalid path', () => { + process.env.GOOGLE_APPLICATION_CREDENTIALS = 'invalidpath'; + expect(() => getApplicationDefault()).to.throw(Error); + }); + + it('should throw if explicitly pointing to an invalid cert file', () => { + fsStub = sinon.stub(fs, 'readFileSync').returns('invalidjson'); + expect(() => getApplicationDefault()).to.throw(Error); + }); + + it('should throw error if type not specified on cert file', () => { + fsStub = sinon.stub(fs, 'readFileSync').returns(JSON.stringify({})); + expect(() => getApplicationDefault()) + .to.throw(Error, 'Invalid contents in the credentials file'); + }); + + it('should throw error if type is unknown on cert file', () => { + fsStub = sinon.stub(fs, 'readFileSync').returns(JSON.stringify({ + type: 'foo', + })); + expect(() => getApplicationDefault()).to.throw(Error, 'Invalid contents in the credentials file'); + }); + + it('should return a RefreshTokenCredential with gcloud login', () => { + if (!fs.existsSync(GCLOUD_CREDENTIAL_PATH)) { + // tslint:disable-next-line:no-console + console.log( + 'WARNING: Test being skipped because gcloud credentials not found. Run `gcloud beta auth ' + + 'application-default login`.'); + return; + } + delete process.env.GOOGLE_APPLICATION_CREDENTIALS; + expect((getApplicationDefault())).to.be.an.instanceof(RefreshTokenCredential); + }); + + it('should throw if a the gcloud login cache is invalid', () => { + delete process.env.GOOGLE_APPLICATION_CREDENTIALS; + fsStub = sinon.stub(fs, 'readFileSync').returns('invalidjson'); + expect(() => getApplicationDefault()).to.throw(Error); + }); + + it('should throw if the credentials file content is not an object', () => { + process.env.GOOGLE_APPLICATION_CREDENTIALS = path.resolve(__dirname, '../../resources/mock.key.json'); + fsStub = sinon.stub(fs, 'readFileSync').returns('2'); + expect(() => getApplicationDefault()).to.throw(Error); + }); + + it('should return a MetadataServiceCredential as a last resort', () => { + delete process.env.GOOGLE_APPLICATION_CREDENTIALS; + fsStub = sinon.stub(fs, 'readFileSync').throws(new Error('no gcloud credential file')); + expect(getApplicationDefault()).to.be.an.instanceof(ComputeEngineCredential); + }); + + it('should create access tokens', () => { + process.env.GOOGLE_APPLICATION_CREDENTIALS = path.resolve(__dirname, '../../resources/mock.key.json'); + const c = getApplicationDefault(); + return c.getAccessToken().then((token) => { + expect(token.access_token).to.be.a('string').and.to.not.be.empty; + expect(token.expires_in).to.equal(ONE_HOUR_IN_SECONDS); + }); + }); + + it('should return a Credential', () => { + process.env.GOOGLE_APPLICATION_CREDENTIALS = path.resolve(__dirname, '../../resources/mock.key.json'); + const c = getApplicationDefault(); + expect(c).to.deep.include({ + projectId: mockCertificateObject.project_id, + clientEmail: mockCertificateObject.client_email, + privateKey: mockCertificateObject.private_key, + }); + }); + + it('should parse valid RefreshTokenCredential if GOOGLE_APPLICATION_CREDENTIALS environment variable ' + + 'points to default refresh token location', () => { + process.env.GOOGLE_APPLICATION_CREDENTIALS = GCLOUD_CREDENTIAL_PATH; + + fsStub = sinon.stub(fs, 'readFileSync').returns(JSON.stringify(MOCK_REFRESH_TOKEN_CONFIG)); + + const c = getApplicationDefault(); + expect(c).is.instanceOf(RefreshTokenCredential); + expect(c).to.have.property('refreshToken').that.includes({ + clientId: MOCK_REFRESH_TOKEN_CONFIG.client_id, + clientSecret: MOCK_REFRESH_TOKEN_CONFIG.client_secret, + refreshToken: MOCK_REFRESH_TOKEN_CONFIG.refresh_token, + type: MOCK_REFRESH_TOKEN_CONFIG.type, + }); + expect(fsStub.alwaysCalledWith(GCLOUD_CREDENTIAL_PATH, 'utf8')).to.be.true; + }); + }); + + describe('isApplicationDefault()', () => { + let fsStub: sinon.SinonStub; + + afterEach(() => { + if (fsStub) { + fsStub.restore(); + } + }); + + it('should return true for ServiceAccountCredential loaded from GOOGLE_APPLICATION_CREDENTIALS', () => { + process.env.GOOGLE_APPLICATION_CREDENTIALS = path.resolve(__dirname, '../../resources/mock.key.json'); + const c = getApplicationDefault(); + expect(c).to.be.an.instanceof(ServiceAccountCredential); + expect(isApplicationDefault(c)).to.be.true; + }); + + it('should return true for RefreshTokenCredential loaded from GOOGLE_APPLICATION_CREDENTIALS', () => { + process.env.GOOGLE_APPLICATION_CREDENTIALS = GCLOUD_CREDENTIAL_PATH; + fsStub = sinon.stub(fs, 'readFileSync').returns(JSON.stringify(MOCK_REFRESH_TOKEN_CONFIG)); + const c = getApplicationDefault(); + expect(c).is.instanceOf(RefreshTokenCredential); + expect(isApplicationDefault(c)).to.be.true; + }); + + it('should return true for ImpersonatedServiceAccountCredential loaded from GOOGLE_APPLICATION_CREDENTIALS', () => { + process.env.GOOGLE_APPLICATION_CREDENTIALS = path.resolve( + __dirname, '../../resources/mock.impersonated_key.json' + ); + const c = getApplicationDefault(); + expect(c).is.instanceOf(ImpersonatedServiceAccountCredential); + expect(isApplicationDefault(c)).to.be.true; + }); + + it('should return true for credential loaded from gcloud SDK', () => { + if (!fs.existsSync(GCLOUD_CREDENTIAL_PATH)) { + // tslint:disable-next-line:no-console + console.log( + 'WARNING: Test being skipped because gcloud credentials not found. Run `gcloud beta auth ' + + 'application-default login`.'); + return; + } + delete process.env.GOOGLE_APPLICATION_CREDENTIALS; + const c = getApplicationDefault(); + expect(c).to.be.an.instanceof(RefreshTokenCredential); + expect(isApplicationDefault(c)).to.be.true; + }); + + it('should return true for ComputeEngineCredential', () => { + delete process.env.GOOGLE_APPLICATION_CREDENTIALS; + fsStub = sinon.stub(fs, 'readFileSync').throws(new Error('no gcloud credential file')); + const c = getApplicationDefault(); + expect(c).to.be.an.instanceof(ComputeEngineCredential); + expect(isApplicationDefault(c)).to.be.true; + }); + + it('should return false for explicitly loaded ServiceAccountCredential', () => { + const c = new ServiceAccountCredential(mockCertificateObject); + expect(isApplicationDefault(c)).to.be.false; + }); + + it('should return false for explicitly loaded RefreshTokenCredential', () => { + const c = new RefreshTokenCredential(mocks.refreshToken); + expect(isApplicationDefault(c)).to.be.false; + }); + + it('should return false for explicitly loaded ImpersonatedServiceAccountCredential', () => { + const c = new ImpersonatedServiceAccountCredential(MOCK_IMPERSONATED_TOKEN_CONFIG); + expect(isApplicationDefault(c)).to.be.false; + }); + + it('should return false for custom credential', () => { + const c: Credential = { + getAccessToken: () => { + throw new Error(); + }, + }; + expect(isApplicationDefault(c)).to.be.false; + }); + }); + + describe('HTTP Agent', () => { + const expectedToken = utils.generateRandomAccessToken(); + let stub: sinon.SinonStub; + + beforeEach(() => { + stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({ + access_token: expectedToken, + token_type: 'Bearer', + expires_in: 60 * 60, + })); + }); + + afterEach(() => { + stub.restore(); + }); + + it('ServiceAccountCredential should use the provided HTTP Agent', () => { + const agent = new Agent(); + const c = new ServiceAccountCredential(mockCertificateObject, agent); + return c.getAccessToken().then((token) => { + expect(token.access_token).to.equal(expectedToken); + expect(stub).to.have.been.calledOnce; + expect(stub.args[0][0].httpAgent).to.equal(agent); + }); + }); + + it('RefreshTokenCredential should use the provided HTTP Agent', () => { + const agent = new Agent(); + const c = new RefreshTokenCredential(MOCK_REFRESH_TOKEN_CONFIG, agent); + return c.getAccessToken().then((token) => { + expect(token.access_token).to.equal(expectedToken); + expect(stub).to.have.been.calledOnce; + expect(stub.args[0][0].httpAgent).to.equal(agent); + }); + }); + + it('ComputeEngineCredential should use the provided HTTP Agent', () => { + const agent = new Agent(); + const c = new ComputeEngineCredential(agent); + return c.getAccessToken().then((token) => { + expect(token.access_token).to.equal(expectedToken); + expect(stub).to.have.been.calledOnce; + expect(stub.args[0][0].httpAgent).to.equal(agent); + }); + }); + + it('ApplicationDefaultCredential should use the provided HTTP Agent', () => { + process.env.GOOGLE_APPLICATION_CREDENTIALS = path.resolve(__dirname, '../../resources/mock.key.json'); + const agent = new Agent(); + const c = getApplicationDefault(agent); + return c.getAccessToken().then((token) => { + expect(token.access_token).to.equal(expectedToken); + expect(stub).to.have.been.calledOnce; + expect(stub.args[0][0].httpAgent).to.equal(agent); + }); + }); + + it('ImpersonatedServiceAccountCredential should use the provided HTTP Agent', () => { + const agent = new Agent(); + const c = new ImpersonatedServiceAccountCredential(MOCK_IMPERSONATED_TOKEN_CONFIG, agent); + return c.getAccessToken().then((token) => { + expect(token.access_token).to.equal(expectedToken); + expect(stub).to.have.been.calledOnce; + expect(stub.args[0][0].httpAgent).to.equal(agent); + }); + }); + }); +}); diff --git a/test/unit/firebase-app.spec.ts b/test/unit/app/firebase-app.spec.ts similarity index 66% rename from test/unit/firebase-app.spec.ts rename to test/unit/app/firebase-app.spec.ts index 4399442c87..31301b2b1f 100644 --- a/test/unit/firebase-app.spec.ts +++ b/test/unit/app/firebase-app.spec.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,30 +17,38 @@ '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 utils from './utils'; -import * as mocks from '../resources/mocks'; - -import {ApplicationDefaultCredential, CertCredential, GoogleOAuthAccessToken} 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 {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 * as utils from '../utils'; +import * as mocks from '../../resources/mocks'; + +import { GoogleOAuthAccessToken } from '../../../src/app/index'; +import { ServiceAccountCredential } from '../../../src/app/credential-internal'; +import { FirebaseApp, FirebaseAccessToken } from '../../../src/app/firebase-app'; +import { FirebaseNamespace } from '../../../src/app/firebase-namespace'; +import { AppStore, FIREBASE_CONFIG_VAR } from '../../../src/app/lifecycle'; +import { + auth, messaging, machineLearning, storage, firestore, database, + instanceId, installations, projectManagement, securityRules , remoteConfig, appCheck, +} from '../../../src/firebase-namespace-api'; +import { FirebaseAppError, AppErrorCodes } from '../../../src/utils/error'; + +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 Installations = installations.Installations; +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); @@ -51,54 +60,47 @@ 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(); + } } describe('FirebaseApp', () => { let mockApp: FirebaseApp; - let mockedRequests: nock.Scope[] = []; + let clock: sinon.SinonFakeTimers; + let getTokenStub: sinon.SinonStub; let firebaseNamespace: FirebaseNamespace; - let firebaseNamespaceInternals: FirebaseNamespaceInternals; - let firebaseConfigVar: string; + let firebaseConfigVar: string | undefined; beforeEach(() => { - utils.mockFetchAccessTokenRequests(); - - this.clock = sinon.useFakeTimers(1000); - - mockApp = mocks.app(); + getTokenStub = sinon.stub(ServiceAccountCredential.prototype, 'getAccessToken').resolves({ + access_token: 'mock-access-token', + expires_in: 3600, + }); + clock = sinon.useFakeTimers(1000); firebaseConfigVar = process.env[FIREBASE_CONFIG_VAR]; delete process.env[FIREBASE_CONFIG_VAR]; firebaseNamespace = new FirebaseNamespace(); - firebaseNamespaceInternals = firebaseNamespace.INTERNAL; - - sinon.stub(firebaseNamespaceInternals, 'removeApp'); - mockApp = new FirebaseApp(mocks.appOptions, mocks.appName, firebaseNamespaceInternals); + mockApp = new FirebaseApp(mocks.appOptions, mocks.appName); }); afterEach(() => { - this.clock.restore(); + getTokenStub.restore(); + clock.restore(); if (firebaseConfigVar) { process.env[FIREBASE_CONFIG_VAR] = firebaseConfigVar; } else { delete process.env[FIREBASE_CONFIG_VAR]; } - deleteSpy.reset(); - (firebaseNamespaceInternals.removeApp as any).restore(); - - _.forEach(mockedRequests, (mockedRequest) => mockedRequest.done()); - mockedRequests = []; - - nock.cleanAll(); + deleteSpy.resetHistory(); }); describe('#name', () => { @@ -116,14 +118,14 @@ describe('FirebaseApp', () => { it('should be case sensitive', () => { const newMockAppName = mocks.appName.toUpperCase(); - mockApp = new FirebaseApp(mocks.appOptions, newMockAppName, firebaseNamespaceInternals); + mockApp = new FirebaseApp(mocks.appOptions, newMockAppName); expect(mockApp.name).to.not.equal(mocks.appName); expect(mockApp.name).to.equal(newMockAppName); }); it('should respect leading and trailing whitespace', () => { const newMockAppName = ' ' + mocks.appName + ' '; - mockApp = new FirebaseApp(mocks.appOptions, newMockAppName, firebaseNamespaceInternals); + mockApp = new FirebaseApp(mocks.appOptions, newMockAppName); expect(mockApp.name).to.not.equal(mocks.appName); expect(mockApp.name).to.equal(newMockAppName); }); @@ -131,7 +133,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'); }); }); @@ -151,7 +153,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', () => { @@ -173,28 +175,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', () => { @@ -244,7 +246,7 @@ describe('FirebaseApp', () => { it('should use explicitly specified options when available and ignore the config file', () => { process.env[FIREBASE_CONFIG_VAR] = './test/resources/firebase_config.json'; const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); - expect(app.options.credential).to.be.instanceOf(CertCredential); + expect(app.options.credential).to.be.instanceOf(ServiceAccountCredential); expect(app.options.databaseAuthVariableOverride).to.be.undefined; expect(app.options.databaseURL).to.equal('https://databaseName.firebaseio.com'); expect(app.options.projectId).to.be.undefined; @@ -261,7 +263,7 @@ describe('FirebaseApp', () => { it('should not throw when the config environment variable is not set, and some options are present', () => { const app = firebaseNamespace.initializeApp(mocks.appOptionsNoDatabaseUrl, mocks.appName); - expect(app.options.credential).to.be.instanceOf(CertCredential); + expect(app.options.credential).to.be.instanceOf(ServiceAccountCredential); expect(app.options.databaseURL).to.be.undefined; expect(app.options.projectId).to.be.undefined; expect(app.options.storageBucket).to.be.undefined; @@ -269,7 +271,7 @@ describe('FirebaseApp', () => { it('should init with application default creds when no options provided and env variable is not set', () => { const app = firebaseNamespace.initializeApp(); - expect(app.options.credential).to.be.instanceOf(ApplicationDefaultCredential); + expect(app.options.credential).to.not.be.undefined; expect(app.options.databaseURL).to.be.undefined; expect(app.options.projectId).to.be.undefined; expect(app.options.storageBucket).to.be.undefined; @@ -278,7 +280,7 @@ describe('FirebaseApp', () => { it('should init with application default creds when no options provided and env variable is an empty json', () => { process.env[FIREBASE_CONFIG_VAR] = '{}'; const app = firebaseNamespace.initializeApp(); - expect(app.options.credential).to.be.instanceOf(ApplicationDefaultCredential); + expect(app.options.credential).to.not.be.undefined; expect(app.options.databaseURL).to.be.undefined; expect(app.options.projectId).to.be.undefined; expect(app.options.storageBucket).to.be.undefined; @@ -287,7 +289,7 @@ describe('FirebaseApp', () => { it('should init when no init arguments are provided and config var points to a file', () => { process.env[FIREBASE_CONFIG_VAR] = './test/resources/firebase_config.json'; const app = firebaseNamespace.initializeApp(); - expect(app.options.credential).to.be.instanceOf(ApplicationDefaultCredential); + expect(app.options.credential).to.not.be.undefined; expect(app.options.databaseAuthVariableOverride).to.deep.equal({ 'some#key': 'some#val' }); expect(app.options.databaseURL).to.equal('https://hipster-chat.firebaseio.mock'); expect(app.options.projectId).to.equal('hipster-chat-mock'); @@ -302,7 +304,7 @@ describe('FirebaseApp', () => { "storageBucket": "hipster-chat.appspot.mock" }`; const app = firebaseNamespace.initializeApp(); - expect(app.options.credential).to.be.instanceOf(ApplicationDefaultCredential); + expect(app.options.credential).to.not.be.undefined; expect(app.options.databaseAuthVariableOverride).to.deep.equal({ 'some#key': 'some#val' }); expect(app.options.databaseURL).to.equal('https://hipster-chat.firebaseio.mock'); expect(app.options.projectId).to.equal('hipster-chat-mock'); @@ -320,26 +322,24 @@ describe('FirebaseApp', () => { }); it('should call removeApp() on the Firebase namespace internals', () => { - return mockApp.delete().then(() => { - expect(firebaseNamespaceInternals.removeApp) - .to.have.been.calledOnce - .and.calledWith(mocks.appName); + const store = new AppStore(); + const stub = sinon.stub(store, 'removeApp').resolves(); + const app = new FirebaseApp(mockApp.options, mockApp.name, store); + return app.delete().then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith(mocks.appName); }); }); 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[mocks.serviceName](); - app[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; }); }); }); @@ -396,6 +396,32 @@ describe('FirebaseApp', () => { }); }); + describe('machineLearning()', () => { + 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.machineLearning(); + }).to.throw(`Firebase app named "${mocks.appName}" has already been deleted.`); + }); + }); + + it('should return the machineLearning client', () => { + const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); + + const machineLearning: MachineLearning = app.machineLearning(); + expect(machineLearning).to.not.be.null; + }); + + it('should return a cached version of MachineLearning on subsequent calls', () => { + const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); + const service1: MachineLearning = app.machineLearning(); + const service2: MachineLearning = app.machineLearning(); + expect(service1).to.equal(service2); + }); + }); + describe('database()', () => { afterEach(() => { try { @@ -497,7 +523,7 @@ describe('FirebaseApp', () => { expect(gcsNamespace).not.be.null; }); - it('should return a cached version of Messaging on subsequent calls', () => { + it('should return a cached version of Storage on subsequent calls', () => { const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); const serviceNamespace1: Storage = app.storage(); const serviceNamespace2: Storage = app.storage(); @@ -531,6 +557,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); @@ -557,65 +609,111 @@ describe('FirebaseApp', () => { }); }); - describe('#[service]()', () => { + describe('projectManagement()', () => { 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[mocks.serviceName](); + return app.projectManagement(); }).to.throw(`Firebase app named "${mocks.appName}" has already been deleted.`); }); }); - it('should return the service namespace', () => { - firebaseNamespace.INTERNAL.registerService(mocks.serviceName, mockServiceFactory); + it('should return the projectManagement client', () => { + const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); + + const projectManagement: ProjectManagement = app.projectManagement(); + expect(projectManagement).to.not.be.null; + }); + + it('should return a cached version of ProjectManagement on subsequent calls', () => { + const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); + const service1: ProjectManagement = app.projectManagement(); + const service2: ProjectManagement = app.projectManagement(); + expect(service1).to.equal(service2); + }); + }); + + describe('securityRules()', () => { + 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.securityRules(); + }).to.throw(`Firebase app named "${mocks.appName}" has already been deleted.`); + }); + }); + it('should return the securityRules client', () => { const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); - const serviceNamespace = app[mocks.serviceName](); - expect(serviceNamespace).to.have.keys(['app', 'INTERNAL']); + const securityRules: SecurityRules = app.securityRules(); + expect(securityRules).to.not.be.null; }); - it('should return a cached version of the service on subsequent calls', () => { - const createServiceSpy = sinon.spy(); - firebaseNamespace.INTERNAL.registerService(mocks.serviceName, createServiceSpy); + it('should return a cached version of SecurityRules on subsequent calls', () => { + const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); + const service1: SecurityRules = app.securityRules(); + const service2: SecurityRules = app.securityRules(); + expect(service1).to.equal(service2); + }); + }); + describe('remoteConfig()', () => { + it('should throw if the app has already been deleted', () => { const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); - expect(createServiceSpy).to.not.have.been.called; + return app.delete().then(() => { + expect(() => { + return app.remoteConfig(); + }).to.throw(`Firebase app named "${mocks.appName}" has already been deleted.`); + }); + }); - const serviceNamespace1 = app[mocks.serviceName](); - expect(createServiceSpy).to.have.been.calledOnce; + it('should return the RemoteConfig client', () => { + const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); - const serviceNamespace2 = app[mocks.serviceName](); - expect(createServiceSpy).to.have.been.calledOnce; - expect(serviceNamespace1).to.deep.equal(serviceNamespace2); + const remoteConfig: RemoteConfig = app.remoteConfig(); + expect(remoteConfig).to.not.be.null; + }); + + it('should return a cached version of RemoteConfig on subsequent calls', () => { + const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); + const service1: RemoteConfig = app.remoteConfig(); + const service2: RemoteConfig = app.remoteConfig(); + expect(service1).to.equal(service2); }); }); - describe('INTERNAL.getToken()', () => { - let httpsSpy: sinon.SinonSpy; - let getAccessTokenSpy: sinon.SinonSpy; - let getAccessTokenStub: sinon.SinonStub; + describe('appCheck()', () => { + it('should throw if the app has already been deleted', () => { + const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); - beforeEach(() => { - httpsSpy = sinon.spy(https, 'request'); + return app.delete().then(() => { + expect(() => { + return app.appCheck(); + }).to.throw(`Firebase app named "${mocks.appName}" has already been deleted.`); + }); }); - afterEach(() => { - httpsSpy.restore(); + it('should return the AppCheck client', () => { + const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); - if (typeof getAccessTokenSpy !== 'undefined') { - getAccessTokenSpy.restore(); - } + const appCheck: AppCheck = app.appCheck(); + expect(appCheck).to.not.be.null; + }); - if (typeof getAccessTokenStub !== 'undefined') { - getAccessTokenStub.restore(); - } + 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', () => { const credential = { @@ -642,7 +740,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); @@ -668,20 +766,20 @@ describe('FirebaseApp', () => { it('returns the cached token given no arguments', () => { return mockApp.INTERNAL.getToken(true).then((token1) => { - this.clock.tick(1000); + clock.tick(1000); return mockApp.INTERNAL.getToken().then((token2) => { expect(token1).to.deep.equal(token2); - expect(httpsSpy).to.have.been.calledOnce; + expect(getTokenStub).to.have.been.calledOnce; }); }); }); it('returns a new token with force refresh', () => { return mockApp.INTERNAL.getToken(true).then((token1) => { - this.clock.tick(1000); + clock.tick(1000); return mockApp.INTERNAL.getToken(true).then((token2) => { expect(token1).to.not.deep.equal(token2); - expect(httpsSpy).to.have.been.calledTwice; + expect(getTokenStub).to.have.been.calledTwice; }); }); }); @@ -691,211 +789,48 @@ describe('FirebaseApp', () => { return mockApp.INTERNAL.getToken(true).then((token1) => { // Forward the clock to five minutes and one second before expiry. const expiryInMilliseconds = token1.expirationTime - Date.now(); - this.clock.tick(expiryInMilliseconds - (5 * ONE_MINUTE_IN_MILLISECONDS) - 1000); + clock.tick(expiryInMilliseconds - (5 * ONE_MINUTE_IN_MILLISECONDS) - 1000); return mockApp.INTERNAL.getToken().then((token2) => { // Ensure the token has not been proactively refreshed. expect(token1).to.deep.equal(token2); - expect(httpsSpy).to.have.been.calledOnce; + expect(getTokenStub).to.have.been.calledOnce; // Forward the clock to exactly five minutes before expiry. - this.clock.tick(1000); - - return mockApp.INTERNAL.getToken().then((token3) => { - // Ensure the token was proactively refreshed. - expect(token1).to.not.deep.equal(token3); - expect(httpsSpy).to.have.been.calledTwice; - }); - }); - }); - }); - - 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. - getAccessTokenStub = sinon.stub(mockApp.options.credential, 'getAccessToken'); - getAccessTokenStub.returns(Promise.reject(new Error('Intentionally rejected'))); - - // Forward the clock to exactly five minutes before expiry. - const expiryInMilliseconds = token1.expirationTime - Date.now(); - this.clock.tick(expiryInMilliseconds - (5 * ONE_MINUTE_IN_MILLISECONDS)); - - // Forward the clock to exactly four minutes before expiry. - this.clock.tick(60 * 1000); - - // Restore the stubbed getAccessToken() method. - getAccessTokenStub.restore(); - getAccessTokenStub = undefined; - - return mockApp.INTERNAL.getToken().then((token2) => { - // Ensure the token has not been proactively refreshed. - expect(token1).to.deep.equal(token2); - expect(httpsSpy).to.have.been.calledOnce; - - // Forward the clock to exactly three minutes before expiry. - this.clock.tick(60 * 1000); + clock.tick(1000); return mockApp.INTERNAL.getToken().then((token3) => { // Ensure the token was proactively refreshed. expect(token1).to.not.deep.equal(token3); - expect(httpsSpy).to.have.been.calledTwice; + expect(getTokenStub).to.have.been.calledTwice; }); }); }); }); - it('stops retrying to proactively refresh the token after five attempts', () => { - // Force a token refresh. - let originalToken; - return mockApp.INTERNAL.getToken(true).then((token) => { - originalToken = token; - - // Stub the credential's getAccessToken() method to always return a rejected promise. - getAccessTokenStub = sinon.stub(mockApp.options.credential, 'getAccessToken'); - getAccessTokenStub.returns(Promise.reject(new Error('Intentionally rejected'))); - - // Expect the call count to initially be zero. - expect(getAccessTokenStub.callCount).to.equal(0); - - // Forward the clock to exactly five minutes before expiry. - const expiryInMilliseconds = token.expirationTime - Date.now(); - this.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(getAccessTokenStub.callCount).to.equal(1); - - // Ensure the proactive refresh failed. - expect(token).to.deep.equal(originalToken); - - // Forward the clock to four minutes before expiry. - this.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(getAccessTokenStub.callCount).to.equal(2); - - // Ensure the proactive refresh failed. - expect(token).to.deep.equal(originalToken); - - // Forward the clock to three minutes before expiry. - this.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(getAccessTokenStub.callCount).to.equal(3); - - // Ensure the proactive refresh failed. - expect(token).to.deep.equal(originalToken); - - // Forward the clock to two minutes before expiry. - this.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(getAccessTokenStub.callCount).to.equal(4); - - // Ensure the proactive refresh failed. - expect(token).to.deep.equal(originalToken); - - // Forward the clock to one minute before expiry. - this.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(getAccessTokenStub.callCount).to.equal(5); - - // Ensure the proactive refresh failed. - expect(token).to.deep.equal(originalToken); - - // Forward the clock to expiry. - this.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(getAccessTokenStub.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(); - this.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(httpsSpy).to.have.been.calledTwice; - - // Forward the clock to exactly five minutes before the original token's expiry. - this.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(httpsSpy).to.have.been.calledTwice; - - // Forward the clock to exactly five minutes before the refreshed token's expiry. - expiryInMilliseconds = token3.expirationTime - Date.now(); - this.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(httpsSpy).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. - nock.cleanAll(); - utils.mockFetchAccessTokenRequests(/* token */ undefined, /* expiresIn */ 3 * 60 + 10); - - // Force a token refresh. - return mockApp.INTERNAL.getToken(true).then((token1) => { - getAccessTokenSpy = sinon.spy(mockApp.options.credential, 'getAccessToken'); - - // Move the clock forward to three minutes and one second before expiry. - this.clock.tick(9 * 1000); - - // Expect the call count to initially be zero. - expect(getAccessTokenSpy.callCount).to.equal(0); - - // Move the clock forward to exactly three minutes before expiry. - this.clock.tick(1000); - - // Expect the underlying getAccessToken() method to have been called once. - expect(getAccessTokenSpy.callCount).to.equal(1); - - 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( + AppErrorCodes.INVALID_CREDENTIAL, 'Something went wrong'); + getTokenStub = sinon.stub(ServiceAccountCredential.prototype, 'getAccessToken').rejects(mockError); + const detailedMessage = 'Credential implementation provided to initializeApp() via the "credential" property' + + ' failed to fetch a valid Google OAuth2 access token with the following error: "Something went wrong".'; + expect(mockApp.INTERNAL.getToken(true)).to.be.rejectedWith(detailedMessage); + }); + + it('Returns a detailed message when an error is due to an invalid_grant', () => { + getTokenStub.restore(); + const mockError = new FirebaseAppError( + AppErrorCodes.INVALID_CREDENTIAL, 'Failed to get credentials: invalid_grant (reason)'); + getTokenStub = sinon.stub(ServiceAccountCredential.prototype, 'getAccessToken').rejects(mockError); + const detailedMessage = 'Credential implementation provided to initializeApp() via the "credential" property' + + ' failed to fetch a valid Google OAuth2 access token with the following error: "Failed to get credentials:' + + ' invalid_grant (reason)". 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.'; + expect(mockApp.INTERNAL.getToken(true)).to.be.rejectedWith(detailedMessage); }); }); @@ -907,7 +842,7 @@ describe('FirebaseApp', () => { }); afterEach(() => { - addAuthTokenListenerSpy.reset(); + addAuthTokenListenerSpy.resetHistory(); }); it('is notified when the token changes', () => { @@ -935,7 +870,7 @@ describe('FirebaseApp', () => { return mockApp.INTERNAL.getToken().then((token: FirebaseAccessToken) => { expect(addAuthTokenListenerSpy).to.have.been.calledOnce.and.calledWith(token.accessToken); - this.clock.tick(1000); + clock.tick(1000); return mockApp.INTERNAL.getToken(true); }).then((token: FirebaseAccessToken) => { @@ -964,7 +899,7 @@ describe('FirebaseApp', () => { }); afterEach(() => { - addAuthTokenListenerSpies.forEach((spy) => spy.reset()); + addAuthTokenListenerSpies.forEach((spy) => spy.resetHistory()); }); it('removes the listener', () => { @@ -977,7 +912,7 @@ describe('FirebaseApp', () => { mockApp.INTERNAL.removeAuthTokenListener(addAuthTokenListenerSpies[0]); - this.clock.tick(1000); + clock.tick(1000); return mockApp.INTERNAL.getToken(true); }).then((token: FirebaseAccessToken) => { diff --git a/test/unit/app/firebase-namespace.spec.ts b/test/unit/app/firebase-namespace.spec.ts new file mode 100644 index 0000000000..467854d124 --- /dev/null +++ b/test/unit/app/firebase-namespace.spec.ts @@ -0,0 +1,815 @@ +/*! + * @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 path = require('path'); + +import * as _ from 'lodash'; +import * as chai from 'chai'; +import * as sinonChai from 'sinon-chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +import * as mocks from '../../resources/mocks'; + +import { FirebaseNamespace } from '../../../src/app/firebase-namespace'; +import { + enableLogging, + Database as DatabaseImpl, + DataSnapshot, + OnDisconnect, + Query, + Reference, + ServerValue, +} from '@firebase/database-compat/standalone'; + +import { + FieldPath, + FieldValue, + GeoPoint, + v1, + v1beta1, + setLogFunction, +} from '@google-cloud/firestore'; +import { getSdkVersion } from '../../../src/utils/index'; + +import { + app, auth, messaging, machineLearning, storage, firestore, database, + instanceId, installations, projectManagement, securityRules , remoteConfig, appCheck, +} from '../../../src/firebase-namespace-api'; +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 { Installations as InstallationsImpl } from '../../../src/installations/installations'; +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 { clearGlobalAppDefaultCred } from '../../../src/app/credential-factory'; + +import App = app.App; +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; +import ProjectManagement = projectManagement.ProjectManagement; +import RemoteConfig = remoteConfig.RemoteConfig; +import SecurityRules = securityRules.SecurityRules; +import Storage = storage.Storage; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + + +const DEFAULT_APP_NAME = '[DEFAULT]'; +const DEFAULT_APP_NOT_FOUND = 'The default Firebase app does not exist. Make sure you call initializeApp() ' + + 'before using any of the Firebase services.'; + +describe('FirebaseNamespace', () => { + let firebaseNamespace: FirebaseNamespace; + + beforeEach(() => { + firebaseNamespace = new FirebaseNamespace(); + }); + + describe('#SDK_VERSION', () => { + it('should return the SDK version', () => { + expect(firebaseNamespace.SDK_VERSION).to.equal(getSdkVersion()); + }); + }); + + describe('#apps', () => { + it('should return an empty array if there are no apps within this namespace', () => { + expect(firebaseNamespace.apps).to.deep.equal([]); + }); + + it('should return an array of apps within this namespace', () => { + const appNames = ['one', 'two', 'three']; + const apps = appNames.map((appName) => { + return firebaseNamespace.initializeApp(mocks.appOptions, appName); + }); + + expect(firebaseNamespace.apps).to.have.length(apps.length); + expect(firebaseNamespace.apps).to.deep.equal(apps); + }); + + it('should not include apps which have been deleted', () => { + const appNames = ['one', 'two', 'three']; + const apps = appNames.map((appName) => { + return firebaseNamespace.initializeApp(mocks.appOptions, appName); + }); + + return apps[0].delete().then(() => { + apps.shift(); + expect(firebaseNamespace.apps).to.have.length(apps.length); + expect(firebaseNamespace.apps).to.deep.equal(apps); + }); + }); + + it('should be read-only', () => { + expect(() => { + (firebaseNamespace as any).apps = 'foo'; + }).to.throw('Cannot set property apps of # which has only a getter'); + }); + }); + + describe('#app()', () => { + const invalidAppNames = [null, NaN, 0, 1, true, false, [], ['a'], {}, { a: 1 }, _.noop]; + invalidAppNames.forEach((invalidAppName) => { + it('should throw given non-string app name: ' + JSON.stringify(invalidAppName), () => { + expect(() => { + return firebaseNamespace.app(invalidAppName as any); + }).to.throw(`Invalid Firebase app name "${invalidAppName}" provided. App name must be a non-empty string.`); + }); + }); + + 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.'); + }); + + it('should throw given an app name which does not correspond to an existing app', () => { + expect(() => { + return firebaseNamespace.app(mocks.appName); + }).to.throw(`Firebase app named "${mocks.appName}" does not exist.`); + }); + + it('should throw given a deleted app', () => { + const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); + return app.delete().then(() => { + expect(() => { + return firebaseNamespace.app(mocks.appName); + }).to.throw(`Firebase app named "${mocks.appName}" does not exist.`); + }); + }); + + it('should throw given no app name if the default app does not exist', () => { + expect(() => { + return firebaseNamespace.app(); + }).to.throw('The default Firebase app does not exist.'); + }); + + it('should return the app associated with the provided app name', () => { + const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); + expect(firebaseNamespace.app(mocks.appName)).to.deep.equal(app); + }); + + it('should return the default app if no app name is provided', () => { + const app = firebaseNamespace.initializeApp(mocks.appOptions); + expect(firebaseNamespace.app()).to.deep.equal(app); + }); + + it('should return the default app if the default app name is provided', () => { + const app = firebaseNamespace.initializeApp(mocks.appOptions); + expect(firebaseNamespace.app(DEFAULT_APP_NAME)).to.deep.equal(app); + }); + }); + + describe('#initializeApp()', () => { + const invalidAppNames = [null, NaN, 0, 1, true, false, [], ['a'], {}, { a: 1 }, _.noop]; + invalidAppNames.forEach((invalidAppName) => { + it('should throw given non-string app name: ' + JSON.stringify(invalidAppName), () => { + expect(() => { + firebaseNamespace.initializeApp(mocks.appOptions, invalidAppName as any); + }).to.throw(`Invalid Firebase app name "${invalidAppName}" provided. App name must be a non-empty string.`); + }); + }); + + 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.'); + }); + + it('should throw given a name corresponding to an existing app', () => { + expect(() => { + firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); + firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); + }).to.throw(`Firebase app named "${mocks.appName}" already exists.`); + }); + + it('should throw given no app name if the default app already exists', () => { + expect(() => { + firebaseNamespace.initializeApp(mocks.appOptions); + firebaseNamespace.initializeApp(mocks.appOptions); + }).to.throw('The default Firebase app already exists.'); + + expect(() => { + firebaseNamespace.initializeApp(mocks.appOptions); + firebaseNamespace.initializeApp(mocks.appOptions, DEFAULT_APP_NAME); + }).to.throw('The default Firebase app already exists.'); + + expect(() => { + firebaseNamespace.initializeApp(mocks.appOptions, DEFAULT_APP_NAME); + firebaseNamespace.initializeApp(mocks.appOptions); + }).to.throw('The default Firebase app already exists.'); + + expect(() => { + firebaseNamespace.initializeApp(mocks.appOptions, DEFAULT_APP_NAME); + firebaseNamespace.initializeApp(mocks.appOptions, DEFAULT_APP_NAME); + }).to.throw('The default Firebase app already exists.'); + }); + + it('should return a new app with the provided options and app name', () => { + const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); + expect(app.name).to.equal(mocks.appName); + expect(app.options).to.deep.equal(mocks.appOptions); + }); + + it('should return an app with the default app name if no app name is provided', () => { + const app = firebaseNamespace.initializeApp(mocks.appOptions); + expect(app.name).to.deep.equal(DEFAULT_APP_NAME); + }); + + it('should allow re-use of a deleted app name', () => { + let app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); + return app.delete().then(() => { + app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); + expect(firebaseNamespace.app(mocks.appName)).to.deep.equal(app); + }); + }); + + it('should add the new app to the namespace\'s app list', () => { + const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); + expect(firebaseNamespace.app(mocks.appName)).to.deep.equal(app); + }); + }); + + describe('#auth()', () => { + it('should throw when called before initializing an app', () => { + expect(() => { + firebaseNamespace.auth(); + }).to.throw(DEFAULT_APP_NOT_FOUND); + }); + + it('should throw when default app is not initialized', () => { + firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); + expect(() => { + firebaseNamespace.auth(); + }).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 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: 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(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()', () => { + it('should throw when called before initializing an app', () => { + expect(() => { + firebaseNamespace.database(); + }).to.throw(DEFAULT_APP_NOT_FOUND); + }); + + it('should throw when default app is not initialized', () => { + firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); + expect(() => { + firebaseNamespace.database(); + }).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 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: 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(DatabaseImpl); + }); + + it('should return a reference to DataSnapshot type', () => { + expect(firebaseNamespace.database.DataSnapshot).to.be.deep.equal(DataSnapshot); + }); + + it('should return a reference to OnDisconnect type', () => { + expect(firebaseNamespace.database.OnDisconnect).to.be.deep.equal(OnDisconnect); + }); + + it('should return a reference to Query type', () => { + expect(firebaseNamespace.database.Query).to.be.deep.equal(Query); + }); + + it('should return a reference to Reference type', () => { + expect(firebaseNamespace.database.Reference).to.be.deep.equal(Reference); + }); + + it('should return a reference to ServerValue type', () => { + expect(firebaseNamespace.database.ServerValue).to.be.deep.equal(ServerValue); + }); + + 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()', () => { + it('should throw when called before initializing an app', () => { + expect(() => { + firebaseNamespace.messaging(); + }).to.throw(DEFAULT_APP_NOT_FOUND); + }); + + it('should throw when default app is not initialized', () => { + firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); + expect(() => { + firebaseNamespace.messaging(); + }).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 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: 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(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('#machineLearning()', () => { + it('should throw when called before initializating an app', () => { + expect(() => { + firebaseNamespace.machineLearning(); + }).to.throw(DEFAULT_APP_NOT_FOUND); + }); + + it('should throw when default app is not initialized', () => { + firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); + expect(() => { + firebaseNamespace.machineLearning(); + }).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 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: 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(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()', () => { + it('should throw when called before initializing an app', () => { + expect(() => { + firebaseNamespace.storage(); + }).to.throw(DEFAULT_APP_NOT_FOUND); + }); + + it('should throw when default app is not initialized', () => { + firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); + expect(() => { + firebaseNamespace.storage(); + }).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 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: 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(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()', () => { + it('should throw when called before initializing an app', () => { + expect(() => { + firebaseNamespace.firestore(); + }).to.throw(DEFAULT_APP_NOT_FOUND); + }); + + it('should throw when default app is not initialized', () => { + firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); + expect(() => { + firebaseNamespace.firestore(); + }).to.throw(DEFAULT_APP_NOT_FOUND); + }); + + it('should return a valid namespace when the default app is initialized', () => { + firebaseNamespace.initializeApp(mocks.appOptions); + const fs: Firestore = firebaseNamespace.firestore(); + expect(fs).to.not.be.null; + }); + + it('should return a valid namespace when the named app is initialized', () => { + const app: App = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); + const fs: Firestore = firebaseNamespace.firestore(app); + expect(fs).to.not.be.null; + }); + + it('should return a reference to Firestore type', () => { + expect(firebaseNamespace.firestore.Firestore).to.be.deep.equal(Firestore); + }); + + it('should return a reference to FieldPath type', () => { + expect(firebaseNamespace.firestore.FieldPath).to.be.deep.equal(FieldPath); + }); + + it('should return a reference to FieldValue type', () => { + expect(firebaseNamespace.firestore.FieldValue).to.be.deep.equal(FieldValue); + }); + + it('should return a reference to GeoPoint type', () => { + expect(firebaseNamespace.firestore.GeoPoint).to.be.deep.equal(GeoPoint); + }); + + it('should return a reference to setLogFunction', () => { + expect(firebaseNamespace.firestore.setLogFunction).to.be.deep.equal(setLogFunction); + }); + + it('should return a reference to the v1beta1 namespace', () => { + expect(firebaseNamespace.firestore.v1beta1).to.be.deep.equal(v1beta1); + }); + + 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('#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(() => { + firebaseNamespace.instanceId(); + }).to.throw(DEFAULT_APP_NOT_FOUND); + }); + + it('should throw when default app is not initialized', () => { + firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); + expect(() => { + firebaseNamespace.instanceId(); + }).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 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: 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(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()', () => { + it('should throw when called before initializing an app', () => { + expect(() => { + firebaseNamespace.projectManagement(); + }).to.throw(DEFAULT_APP_NOT_FOUND); + }); + + it('should throw when default app is not initialized', () => { + firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); + expect(() => { + firebaseNamespace.projectManagement(); + }).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 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: 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); + }); + + it('should return a reference to ProjectManagement type', () => { + 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()', () => { + it('should throw when called before initializing an app', () => { + expect(() => { + firebaseNamespace.securityRules(); + }).to.throw(DEFAULT_APP_NOT_FOUND); + }); + + it('should throw when default app is not initialized', () => { + firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); + expect(() => { + firebaseNamespace.securityRules(); + }).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 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: 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); + }); + + it('should return a reference to SecurityRules type', () => { + 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()', () => { + it('should throw when called before initializing an app', () => { + expect(() => { + firebaseNamespace.remoteConfig(); + }).to.throw(DEFAULT_APP_NOT_FOUND); + }); + + it('should throw when default app is not initialized', () => { + firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); + expect(() => { + firebaseNamespace.remoteConfig(); + }).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 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: 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(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); + }); + }); + + describe('credentials', () => { + it('should create a service account credential from object', () => { + const mockCertificateObject = mocks.certificateObject; + const credential = firebaseNamespace.credential.cert(mockCertificateObject); + expect(credential).to.deep.include({ + projectId: mockCertificateObject.project_id, + clientEmail: mockCertificateObject.client_email, + privateKey: mockCertificateObject.private_key, + implicit: false, + }); + }); + + it('should create a refresh token credential from object', () => { + const mockRefreshToken = mocks.refreshToken; + const credential = firebaseNamespace.credential.refreshToken(mockRefreshToken); + expect(credential).to.deep.include({ + implicit: false, + }); + }); + + it('should create application default credentials from environment', () => { + process.env.GOOGLE_APPLICATION_CREDENTIALS = path.resolve(__dirname, '../../resources/mock.key.json'); + const mockCertificateObject = mocks.certificateObject; + const credential = firebaseNamespace.credential.applicationDefault(); + expect(credential).to.deep.include({ + projectId: mockCertificateObject.project_id, + clientEmail: mockCertificateObject.client_email, + privateKey: mockCertificateObject.private_key, + implicit: true, + }); + }); + + after(clearGlobalAppDefaultCred); + }); + + 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/app/index.spec.ts b/test/unit/app/index.spec.ts new file mode 100644 index 0000000000..9de58baf84 --- /dev/null +++ b/test/unit/app/index.spec.ts @@ -0,0 +1,251 @@ +/*! + * @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 path = require('path'); + +import * as _ from 'lodash'; +import * as chai from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as mocks from '../../resources/mocks'; +import * as sinon from 'sinon'; + +import { + initializeApp, getApp, getApps, deleteApp, SDK_VERSION, + Credential, applicationDefault, cert, refreshToken, +} from '../../../src/app/index'; +import { clearGlobalAppDefaultCred } from '../../../src/app/credential-factory'; +import { defaultAppStore } from '../../../src/app/lifecycle'; + +chai.should(); +chai.use(chaiAsPromised); + +const expect = chai.expect; + + +describe('firebase-admin/app', () => { + afterEach(() => { + return defaultAppStore.clearAllApps(); + }); + + describe('#initializeApp()', () => { + const invalidOptions: any[] = [null, NaN, 0, 1, true, false, '', 'a', [], _.noop]; + invalidOptions.forEach((invalidOption: any) => { + it('should throw given invalid options object: ' + JSON.stringify(invalidOption), () => { + expect(() => { + initializeApp(invalidOption); + }).to.throw('Invalid Firebase app options'); + }); + }); + + it('should use application default credentials when no credentials are explicitly specified', () => { + const app = initializeApp(mocks.appOptionsNoAuth); + expect(app.options).to.have.property('credential'); + expect(app.options.credential).to.not.be.undefined; + }); + + it('should not modify the provided options object', () => { + const optionsClone = _.clone(mocks.appOptions); + initializeApp(mocks.appOptions); + expect(optionsClone).to.deep.equal(mocks.appOptions); + }); + + const invalidCredentials = [undefined, null, NaN, 0, 1, '', 'a', true, false, '', _.noop]; + invalidCredentials.forEach((invalidCredential) => { + it('should throw given non-object credential: ' + JSON.stringify(invalidCredential), () => { + expect(() => { + initializeApp({ + credential: invalidCredential as any, + }); + }).to.throw('Invalid Firebase app options'); + }); + }); + + it('should throw given a credential which doesn\'t implement the Credential interface', () => { + expect(() => { + initializeApp({ + credential: {}, + } as any); + }).to.throw('Invalid Firebase app options'); + + expect(() => { + initializeApp({ + credential: { + getAccessToken: true, + }, + } as any); + }).to.throw('Invalid Firebase app options'); + }); + + it('should initialize App instance without extended service methods', () => { + const app = initializeApp(mocks.appOptions); + expect((app as any).__extended).to.be.undefined; + expect((app as any).auth).to.be.undefined; + }); + }); + + describe('#getApp()', () => { + const invalidOptions: any[] = [null, NaN, 0, 1, true, false, '', [], _.noop]; + invalidOptions.forEach((invalidOption: any) => { + it('should throw given invalid app name: ' + JSON.stringify(invalidOption), () => { + expect(() => { + getApp(invalidOption); + }).to.throw('Invalid Firebase app name'); + }); + }); + + it('should return default app when name not specified', () => { + initializeApp(mocks.appOptionsNoAuth); + const defaulApp = getApp(); + expect(defaulApp.name).to.equal('[DEFAULT]'); + }); + + it('should return named app when available', () => { + initializeApp(mocks.appOptionsNoAuth, 'testApp'); + const testApp = getApp('testApp'); + expect(testApp.name).to.equal('testApp'); + }); + + it('should throw when the default app does not exist', () => { + expect(() => getApp()).to.throw('The default Firebase app does not exist'); + }); + + it('should throw when the specified app does not exist', () => { + expect(() => getApp('testApp')).to.throw('Firebase app named "testApp" does not exist'); + }); + }); + + describe('#getApps()', () => { + it('should return empty array when no apps available', () => { + const apps = getApps(); + expect(apps).to.be.empty; + }); + + it('should return a non-empty array of apps', () => { + initializeApp(mocks.appOptionsNoAuth); + initializeApp(mocks.appOptionsNoAuth, 'testApp'); + const apps = getApps(); + expect(apps.length).to.equal(2); + + const appNames = apps.map((a) => a.name); + expect(appNames).to.contain('[DEFAULT]'); + expect(appNames).to.contain('testApp'); + }); + + it('apps array is immutable', () => { + initializeApp(mocks.appOptionsNoAuth); + const apps = getApps(); + expect(apps.length).to.equal(1); + apps.push({} as any); + + expect(getApps().length).to.equal(1); + }); + }); + + describe('#deleteApp()', () => { + it('should delete the specified app', () => { + const app = initializeApp(mocks.appOptionsNoAuth); + const spy = sinon.spy(app as any, 'delete'); + deleteApp(app); + expect(getApps()).to.be.empty; + expect(spy.calledOnce); + }); + + it('should throw if the app is already deleted', () => { + const app = initializeApp(mocks.appOptionsNoAuth); + deleteApp(app); + expect(() => deleteApp(app)).to.throw('The default Firebase app does not exist'); + }); + + const invalidOptions: any[] = [null, NaN, 0, 1, true, false, '', [], _.noop]; + invalidOptions.forEach((invalidOption: any) => { + it('should throw given invalid app: ' + JSON.stringify(invalidOption), () => { + expect(() => { + deleteApp(invalidOption); + }).to.throw('Invalid app argument'); + }); + }); + }); + + describe('SDK_VERSION', () => { + it('should indicate the current version of the SDK', () => { + const { version } = require('../../../package.json'); // eslint-disable-line @typescript-eslint/no-var-requires + expect(SDK_VERSION).to.equal(version); + }); + }); + + describe('#cert()', () => { + it('should create a service account credential from object', () => { + const mockCertificateObject = mocks.certificateObject; + const credential: Credential = cert(mockCertificateObject); + expect(credential).to.deep.include({ + projectId: mockCertificateObject.project_id, + clientEmail: mockCertificateObject.client_email, + privateKey: mockCertificateObject.private_key, + implicit: false, + }); + }); + + it('should create a service account credential from file path', () => { + const filePath = path.resolve(__dirname, '../../resources/mock.key.json'); + const mockCertificateObject = mocks.certificateObject; + const credential: Credential = cert(filePath); + expect(credential).to.deep.include({ + projectId: mockCertificateObject.project_id, + clientEmail: mockCertificateObject.client_email, + privateKey: mockCertificateObject.private_key, + implicit: false, + }); + }); + }); + + describe('#refreshToken()', () => { + it('should create a refresh token credential from object', () => { + const mockRefreshToken = mocks.refreshToken; + const credential: Credential = refreshToken(mockRefreshToken); + expect(credential).to.deep.include({ + implicit: false, + }); + }); + }); + + describe('#applicationDefault()', () => { + before(() => { + process.env.GOOGLE_APPLICATION_CREDENTIALS = path.resolve(__dirname, '../../resources/mock.key.json'); + }); + + it('should create application default credentials from environment', () => { + const mockCertificateObject = mocks.certificateObject; + const credential: Credential = applicationDefault(); + expect(credential).to.deep.include({ + projectId: mockCertificateObject.project_id, + clientEmail: mockCertificateObject.client_email, + privateKey: mockCertificateObject.private_key, + implicit: true, + }); + }); + + it('should cache application default credentials globally', () => { + const credential1: Credential = applicationDefault(); + const credential2: Credential = applicationDefault(); + expect(credential1).to.equal(credential2); + }); + + after(clearGlobalAppDefaultCred); + }); +}); diff --git a/test/unit/auth/action-code-settings-builder.spec.ts b/test/unit/auth/action-code-settings-builder.spec.ts new file mode 100644 index 0000000000..ceadde3e9b --- /dev/null +++ b/test/unit/auth/action-code-settings-builder.spec.ts @@ -0,0 +1,262 @@ +/*! + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as _ from 'lodash'; +import * as chai from 'chai'; +import * as sinonChai from 'sinon-chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +import { ActionCodeSettingsBuilder } from '../../../src/auth/action-code-settings-builder'; +import { AuthClientErrorCode } from '../../../src/utils/error'; + + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +describe('ActionCodeSettingsBuilder', () => { + describe('constructor', () => { + it('should not throw on valid parameters', () => { + expect(new ActionCodeSettingsBuilder({ + url: 'https://www.example.com/path/file?a=1&b=2', + handleCodeInApp: true, + iOS: { + bundleId: 'com.example.ios', + }, + android: { + packageName: 'com.example.android', + installApp: true, + minimumVersion: '6', + }, + dynamicLinkDomain: 'custom.page.link', + })).not.to.throw; + }); + + const invalidSettings = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; + invalidSettings.forEach((settings) => { + it('should throw on non-object ActionCodeSettings:' + JSON.stringify(settings), () => { + expect(() => { + return new ActionCodeSettingsBuilder(settings as any); + }).to.throw('"ActionCodeSettings" must be a non-null object.'); + }); + }); + + it('should throw on missing URL', () => { + expect(() => { + return new ActionCodeSettingsBuilder({ + handleCodeInApp: true, + iOS: { + bundleId: 'com.example.ios', + }, + android: { + packageName: 'com.example.android', + installApp: true, + minimumVersion: '6', + }, + dynamicLinkDomain: 'custom.page.link', + } as any); + }).to.throw(AuthClientErrorCode.MISSING_CONTINUE_URI.message); + }); + + const invalidUrls = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidUrls.forEach((url) => { + it('should throw on invalid URL:' + JSON.stringify(url), () => { + expect(() => { + return new ActionCodeSettingsBuilder({ + url, + } as any); + }).to.throw(AuthClientErrorCode.INVALID_CONTINUE_URI.message); + }); + }); + + const invalidHandleCodeInApp = [null, NaN, 0, 1, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidHandleCodeInApp.forEach((handleCodeInApp) => { + it('should throw on invalid handleCodeInApp:' + JSON.stringify(handleCodeInApp), () => { + expect(() => { + return new ActionCodeSettingsBuilder({ + url: 'https://www.example.com/path/file?a=1&b=2', + handleCodeInApp, + } as any); + }).to.throw('"ActionCodeSettings.handleCodeInApp" must be a boolean.'); + }); + }); + + const invalidDomains = [null, NaN, 0, 1, true, false, '', ['custom.page.link'], [], {}, { a: 1 }, _.noop]; + invalidDomains.forEach((domain) => { + it('should throw on invalid dynamicLinkDomain:' + JSON.stringify(domain), () => { + expect(() => { + return new ActionCodeSettingsBuilder({ + url: 'https://www.example.com/path/file?a=1&b=2', + handleCodeInApp: true, + dynamicLinkDomain: domain, + } as any); + }).to.throw(AuthClientErrorCode.INVALID_DYNAMIC_LINK_DOMAIN.message); + }); + }); + + const invalidIOSSettings = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; + invalidIOSSettings.forEach((settings) => { + it('should throw on invalid iOS object:' + JSON.stringify(settings), () => { + expect(() => { + return new ActionCodeSettingsBuilder({ + url: 'https://www.example.com/path/file?a=1&b=2', + handleCodeInApp: true, + iOS: settings, + } as any); + }).to.throw('"ActionCodeSettings.iOS" must be a valid non-null object.'); + }); + }); + + it('should throw on missing iOS bundle ID', () => { + expect(() => { + return new ActionCodeSettingsBuilder({ + url: 'https://www.example.com/path/file?a=1&b=2', + handleCodeInApp: true, + iOS: {}, + } as any); + }).to.throw(AuthClientErrorCode.MISSING_IOS_BUNDLE_ID.message); + }); + + const invalidBundleIds = [null, NaN, 0, 1, true, false, '', ['com.example.ios'], _.noop]; + invalidBundleIds.forEach((bundleId) => { + it('should throw on invalid iOS bundle ID:' + JSON.stringify(bundleId), () => { + expect(() => { + return new ActionCodeSettingsBuilder({ + url: 'https://www.example.com/path/file?a=1&b=2', + handleCodeInApp: true, + iOS: { bundleId }, + } as any); + }).to.throw('"ActionCodeSettings.iOS.bundleId" must be a valid non-empty string.'); + }); + }); + + const invalidAndroidSettings = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; + invalidAndroidSettings.forEach((settings) => { + it('should throw on invalid android object:' + JSON.stringify(settings), () => { + expect(() => { + return new ActionCodeSettingsBuilder({ + url: 'https://www.example.com/path/file?a=1&b=2', + handleCodeInApp: true, + android: settings, + } as any); + }).to.throw('"ActionCodeSettings.android" must be a valid non-null object.'); + }); + }); + + it('should throw on missing android package name', () => { + expect(() => { + return new ActionCodeSettingsBuilder({ + url: 'https://www.example.com/path/file?a=1&b=2', + handleCodeInApp: true, + android: {}, + } as any); + }).to.throw(AuthClientErrorCode.MISSING_ANDROID_PACKAGE_NAME.message); + }); + + const invalidPackageNames = [null, NaN, 0, 1, true, false, '', ['com.example.android'], _.noop]; + invalidPackageNames.forEach((packageName) => { + it('should throw on invalid android package name:' + JSON.stringify(packageName), () => { + expect(() => { + return new ActionCodeSettingsBuilder({ + url: 'https://www.example.com/path/file?a=1&b=2', + handleCodeInApp: true, + android: { packageName }, + } as any); + }).to.throw('"ActionCodeSettings.android.packageName" must be a valid non-empty string.'); + }); + }); + + const invalidMinimumVersions = [null, NaN, 0, 1, true, false, '', [], [1, 'a'], _.noop]; + invalidMinimumVersions.forEach((minimumVersion) => { + it('should throw on invalid android minimum version:' + JSON.stringify(minimumVersion), () => { + expect(() => { + return new ActionCodeSettingsBuilder({ + url: 'https://www.example.com/path/file?a=1&b=2', + handleCodeInApp: true, + android: { + packageName: 'com.example.android', + minimumVersion, + }, + } as any); + }).to.throw('"ActionCodeSettings.android.minimumVersion" must be a valid non-empty string.'); + }); + }); + + const invalidInstallApp = [null, NaN, 0, 1, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidInstallApp.forEach((installApp) => { + it('should throw on invalid android installApp field:' + JSON.stringify(installApp), () => { + expect(() => { + return new ActionCodeSettingsBuilder({ + url: 'https://www.example.com/path/file?a=1&b=2', + handleCodeInApp: true, + android: { + packageName: 'com.example.android', + installApp, + }, + } as any); + }).to.throw('"ActionCodeSettings.android.installApp" must be a valid boolean.'); + }); + }); + }); + + describe('buildRequest()', () => { + it('should return EmailActionCodeRequest with expected fields', () => { + const builder = new ActionCodeSettingsBuilder({ + url: 'https://www.example.com/path/file?a=1&b=2', + handleCodeInApp: true, + iOS: { + bundleId: 'com.example.ios', + }, + android: { + packageName: 'com.example.android', + installApp: true, + minimumVersion: '6', + }, + dynamicLinkDomain: 'custom.page.link', + }); + const expectedRequest = { + continueUrl: 'https://www.example.com/path/file?a=1&b=2', + canHandleCodeInApp: true, + dynamicLinkDomain: 'custom.page.link', + androidPackageName: 'com.example.android', + androidMinimumVersion: '6', + androidInstallApp: true, + iOSBundleId: 'com.example.ios', + }; + expect(builder.buildRequest()).to.deep.equal(expectedRequest); + }); + + it ('should return EmailActionCodeRequest without null or undefined fields', () => { + const builder = new ActionCodeSettingsBuilder({ + url: 'https://www.example.com/path/file?a=1&b=2', + iOS: undefined, + android: { + packageName: 'com.example.android', + installApp: undefined, + }, + }); + const expectedRequest = { + continueUrl: 'https://www.example.com/path/file?a=1&b=2', + canHandleCodeInApp: false, + androidPackageName: 'com.example.android', + androidInstallApp: false, + }; + expect(builder.buildRequest()).to.deep.equal(expectedRequest); + }); + }); +}); diff --git a/test/unit/auth/auth-api-request.spec.ts b/test/unit/auth/auth-api-request.spec.ts index e82ee79bcc..574962df53 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"); @@ -18,7 +19,6 @@ 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'; @@ -26,23 +26,50 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as utils from '../utils'; import * as mocks from '../../resources/mocks'; -import {deepCopy} from '../../../src/utils/deep-copy'; -import {FirebaseApp} from '../../../src/firebase-app'; -import {HttpRequestHandler} from '../../../src/utils/api-request'; +import { deepCopy, deepExtend } from '../../../src/utils/deep-copy'; +import { FirebaseApp } from '../../../src/app/firebase-app'; +import { HttpClient, HttpRequestConfig } from '../../../src/utils/api-request'; import * as validator from '../../../src/utils/validator'; import { - FirebaseAuthRequestHandler, FIREBASE_AUTH_GET_ACCOUNT_INFO, + AuthRequestHandler, FIREBASE_AUTH_GET_ACCOUNT_INFO, FIREBASE_AUTH_GET_ACCOUNTS_INFO, FIREBASE_AUTH_DELETE_ACCOUNT, FIREBASE_AUTH_SET_ACCOUNT_INFO, FIREBASE_AUTH_SIGN_UP_NEW_USER, FIREBASE_AUTH_DOWNLOAD_ACCOUNT, - RESERVED_CLAIMS, + RESERVED_CLAIMS, FIREBASE_AUTH_UPLOAD_ACCOUNT, FIREBASE_AUTH_CREATE_SESSION_COOKIE, + EMAIL_ACTION_REQUEST_TYPES, TenantAwareAuthRequestHandler, AbstractAuthRequestHandler, } from '../../../src/auth/auth-api-request'; -import {AuthClientErrorCode, FirebaseAuthError} from '../../../src/utils/error'; +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 { SAMLConfigServerResponse } from '../../../src/auth/auth-config'; +import { expectUserImportResult } from './user-import-builder.spec'; +import { getSdkVersion } from '../../../src/utils/index'; +import { + UserImportRecord, OIDCAuthProviderConfig, SAMLAuthProviderConfig, OIDCUpdateAuthProviderRequest, + SAMLUpdateAuthProviderRequest, UserIdentifier, UpdateRequest, UpdateMultiFactorInfoRequest, + CreateTenantRequest, UpdateTenantRequest, +} from '../../../src/auth/index'; chai.should(); chai.use(sinonChai); chai.use(chaiAsPromised); const expect = chai.expect; +const host = 'identitytoolkit.googleapis.com'; +const timeout = 25000; + + +interface HandlerTest { + name: string; + supportsTenantManagement: boolean; + init(app: FirebaseApp): AbstractAuthRequestHandler; + path(version: string, api: string, projectId: string): string; +} + +interface InvalidMultiFactorUpdateTest { + name: string; + error: FirebaseAuthError; + secondFactor: any; +} /** @@ -61,6 +88,146 @@ function createRandomString(numOfChars: number): string { } +describe('FIREBASE_AUTH_CREATE_SESSION_COOKIE', () => { + // Spy on all validators. + let isNonEmptyString: sinon.SinonSpy; + let isNumber: sinon.SinonSpy; + + beforeEach(() => { + isNonEmptyString = sinon.spy(validator, 'isNonEmptyString'); + isNumber = sinon.spy(validator, 'isNumber'); + }); + afterEach(() => { + isNonEmptyString.restore(); + isNumber.restore(); + }); + + it('should return the correct endpoint', () => { + expect(FIREBASE_AUTH_CREATE_SESSION_COOKIE.getEndpoint()).to.equal(':createSessionCookie'); + }); + it('should return the correct http method', () => { + expect(FIREBASE_AUTH_CREATE_SESSION_COOKIE.getHttpMethod()).to.equal('POST'); + }); + 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 }; + expect(() => { + return requestValidator(validRequest); + }).not.to.throw(); + expect(isNonEmptyString).to.have.been.calledOnce.and.calledWith('ID_TOKEN'); + expect(isNumber).to.have.been.calledOnce.and.calledWith(60 * 60); + }); + it('should succeed with duration set at minimum allowed', () => { + const validDuration = 60 * 5; + const validRequest = { idToken: 'ID_TOKEN', validDuration }; + expect(() => { + return requestValidator(validRequest); + }).not.to.throw(); + expect(isNonEmptyString).to.have.been.calledOnce.and.calledWith('ID_TOKEN'); + expect(isNumber).to.have.been.calledOnce.and.calledWith(validDuration); + }); + it('should succeed with duration set at maximum allowed', () => { + const validDuration = 60 * 60 * 24 * 14; + const validRequest = { idToken: 'ID_TOKEN', validDuration }; + expect(() => { + return requestValidator(validRequest); + }).not.to.throw(); + expect(isNonEmptyString).to.have.been.calledOnce.and.calledWith('ID_TOKEN'); + expect(isNumber).to.have.been.calledOnce.and.calledWith(validDuration); + }); + it('should fail when idToken not passed', () => { + 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' }; + expect(() => { + return requestValidator(invalidRequest); + }).to.throw(); + expect(isNumber).to.have.been.calledOnce.and.calledWith(undefined); + }); + describe('called with invalid parameters', () => { + it('should fail with invalid idToken', () => { + expect(() => { + 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' }); + }).to.throw(); + expect(isNonEmptyString).to.have.been.calledOnce.and.calledWith('ID_TOKEN'); + expect(isNumber).to.have.been.calledOnce.and.calledWith('invalid'); + }); + it('should fail with validDuration less than minimum allowed', () => { + // Duration less 5 minutes. + const outOfBoundDuration = 60 * 5 - 1; + expect(() => { + 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); + }); + it('should fail with validDuration greater than maximum allowed', () => { + // Duration greater than 14 days. + const outOfBoundDuration = 60 * 60 * 24 * 14 + 1; + expect(() => { + 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); + }); + }); + }); + describe('responseValidator', () => { + const responseValidator = FIREBASE_AUTH_CREATE_SESSION_COOKIE.getResponseValidator(); + it('should succeed with sessionCookie returned', () => { + const validResponse = { sessionCookie: 'SESSION_COOKIE' }; + expect(() => { + return responseValidator(validResponse); + }).not.to.throw(); + }); + it('should fail when no session cookie is returned', () => { + const invalidResponse = {}; + expect(() => { + responseValidator(invalidResponse); + }).to.throw(); + }); + }); +}); + + +describe('FIREBASE_AUTH_UPLOAD_ACCOUNT', () => { + it('should return the correct endpoint', () => { + expect(FIREBASE_AUTH_UPLOAD_ACCOUNT.getEndpoint()).to.equal('/accounts:batchCreate'); + }); + it('should return the correct http method', () => { + expect(FIREBASE_AUTH_UPLOAD_ACCOUNT.getHttpMethod()).to.equal('POST'); + }); + it('should return empty request validator', () => { + expect(FIREBASE_AUTH_UPLOAD_ACCOUNT.getRequestValidator()).to.not.be.null; + expect(() => { + const emptyRequest = {}; + const requestValidator = FIREBASE_AUTH_UPLOAD_ACCOUNT.getRequestValidator(); + requestValidator(emptyRequest); + }).not.to.throw(); + }); + it('should return empty response validator', () => { + expect(FIREBASE_AUTH_UPLOAD_ACCOUNT.getResponseValidator()).to.not.be.null; + expect(() => { + const emptyResponse = {}; + const responseValidator = FIREBASE_AUTH_UPLOAD_ACCOUNT.getResponseValidator(); + responseValidator(emptyResponse); + }).not.to.throw(); + }); +}); + + describe('FIREBASE_AUTH_DOWNLOAD_ACCOUNT', () => { // Spy on all validators. let isNonEmptyString: sinon.SinonSpy; @@ -76,10 +243,10 @@ describe('FIREBASE_AUTH_DOWNLOAD_ACCOUNT', () => { }); it('should return the correct endpoint', () => { - expect(FIREBASE_AUTH_DOWNLOAD_ACCOUNT.getEndpoint()).to.equal('downloadAccount'); + expect(FIREBASE_AUTH_DOWNLOAD_ACCOUNT.getEndpoint()).to.equal('/accounts:batchGet'); }); it('should return the correct http method', () => { - expect(FIREBASE_AUTH_DOWNLOAD_ACCOUNT.getHttpMethod()).to.equal('POST'); + expect(FIREBASE_AUTH_DOWNLOAD_ACCOUNT.getHttpMethod()).to.equal('GET'); }); it('should return empty response validator', () => { expect(FIREBASE_AUTH_DOWNLOAD_ACCOUNT.getResponseValidator()).to.not.be.null; @@ -92,7 +259,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(); @@ -121,31 +288,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']); }); @@ -155,7 +322,7 @@ describe('FIREBASE_AUTH_DOWNLOAD_ACCOUNT', () => { describe('FIREBASE_AUTH_GET_ACCOUNT_INFO', () => { it('should return the correct endpoint', () => { - expect(FIREBASE_AUTH_GET_ACCOUNT_INFO.getEndpoint()).to.equal('getAccountInfo'); + expect(FIREBASE_AUTH_GET_ACCOUNT_INFO.getEndpoint()).to.equal('/accounts:lookup'); }); it('should return the correct http method', () => { expect(FIREBASE_AUTH_GET_ACCOUNT_INFO.getHttpMethod()).to.equal('POST'); @@ -163,25 +330,37 @@ 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' }] }; + expect(() => { + 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']}; + const invalidRequest = { bla: ['1234'] }; expect(() => { return requestValidator(invalidRequest); }).to.throw(); @@ -190,23 +369,99 @@ describe('FIREBASE_AUTH_GET_ACCOUNT_INFO', () => { describe('responseValidator', () => { const responseValidator = FIREBASE_AUTH_GET_ACCOUNT_INFO.getResponseValidator(); it('should succeed with users returned', () => { - const validResponse = {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(); + }); + }); +}); + +describe('FIREBASE_AUTH_GET_ACCOUNTS_INFO', () => { + it('should return the correct endpoint', () => { + expect(FIREBASE_AUTH_GET_ACCOUNTS_INFO.getEndpoint()).to.equal('/accounts:lookup'); + }); + it('should return the correct http method', () => { + expect(FIREBASE_AUTH_GET_ACCOUNTS_INFO.getHttpMethod()).to.equal('POST'); + }); + describe('requestValidator', () => { + const requestValidator = FIREBASE_AUTH_GET_ACCOUNTS_INFO.getRequestValidator(); + it('should succeed with localId passed', () => { + const validRequest = { localId: ['1234'] }; + expect(() => { + return requestValidator(validRequest); + }).not.to.throw(); + }); + it('should succeed with email passed', () => { + const validRequest = { email: ['user@example.com'] }; + expect(() => { + return requestValidator(validRequest); + }).not.to.throw(); + }); + it('should succeed with phoneNumber passed', () => { + 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' }] }; + expect(() => { + return requestValidator(validRequest); + }).not.to.throw(); + }); + it('should fail when neither localId, email or phoneNumber are passed', () => { + const invalidRequest = { bla: ['1234'] }; + expect(() => { + return requestValidator(invalidRequest); + }).to.throw(); + }); + it('should succeed when multiple identifiers passed', () => { + const validRequest = { + localId: ['123', '456'], + email: ['user1@example.com', 'user2@example.com'], + phoneNumber: ['+15555550001', '+15555550002'], + federatedUserId: [ + { providerId: 'google.com', rawId: 'google_uid1' }, + { providerId: 'google.com', rawId: 'google_uid2' } + ] }; + expect(() => { + return requestValidator(validRequest); + }).not.to.throw(); + }); + }); + describe('responseValidator', () => { + const responseValidator = FIREBASE_AUTH_GET_ACCOUNTS_INFO.getResponseValidator(); + it('should succeed with users returned', () => { + const validResponse: object = { users: [] }; + expect(() => { + return responseValidator(validResponse); + }).not.to.throw(); + }); + it('should succeed even if users are not returned', () => { + const invalidResponse = {}; + expect(() => { + responseValidator(invalidResponse); + }).not.to.throw(); + }); }); }); describe('FIREBASE_AUTH_DELETE_ACCOUNT', () => { it('should return the correct endpoint', () => { - expect(FIREBASE_AUTH_DELETE_ACCOUNT.getEndpoint()).to.equal('deleteAccount'); + expect(FIREBASE_AUTH_DELETE_ACCOUNT.getEndpoint()).to.equal('/accounts:delete'); }); it('should return the correct http method', () => { expect(FIREBASE_AUTH_DELETE_ACCOUNT.getHttpMethod()).to.equal('POST'); @@ -222,13 +477,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(); @@ -263,7 +518,7 @@ describe('FIREBASE_AUTH_SET_ACCOUNT_INFO', () => { }); it('should return the correct endpoint', () => { - expect(FIREBASE_AUTH_SET_ACCOUNT_INFO.getEndpoint()).to.equal('setAccountInfo'); + expect(FIREBASE_AUTH_SET_ACCOUNT_INFO.getEndpoint()).to.equal('/accounts:update'); }); it('should return the correct http method', () => { expect(FIREBASE_AUTH_SET_ACCOUNT_INFO.getHttpMethod()).to.equal('POST'); @@ -271,7 +526,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(); @@ -287,7 +542,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', @@ -305,9 +560,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', () => { @@ -320,68 +575,68 @@ 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}); - }).to.throw(`Developer claims payload should not exceed 1000 characters.`); + return requestValidator({ localId: '1234', customAttributes: largeClaims }); + }).to.throw('Developer claims payload should not exceed 1000 characters.'); }); RESERVED_CLAIMS.forEach((invalidClaim) => { it(`should fail with customAttributes containing blacklisted claim: ${invalidClaim}`, () => { expect(() => { // Instantiate custom attributes with invalid claims. - const 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.`); }); }); @@ -391,12 +646,12 @@ describe('FIREBASE_AUTH_SET_ACCOUNT_INFO', () => { sub: 'sub', auth_time: 'time', }; - return requestValidator({localId: '1234', customAttributes: JSON.stringify(claims)}); - }).to.throw(`Developer claims "auth_time", "sub" are reserved and cannot be specified.`); + 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'); }); @@ -405,7 +660,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(); @@ -443,7 +698,7 @@ describe('FIREBASE_AUTH_SIGN_UP_NEW_USER', () => { }); it('should return the correct endpoint', () => { - expect(FIREBASE_AUTH_SIGN_UP_NEW_USER.getEndpoint()).to.equal('signupNewUser'); + expect(FIREBASE_AUTH_SIGN_UP_NEW_USER.getEndpoint()).to.equal('/accounts'); }); it('should return the correct http method', () => { expect(FIREBASE_AUTH_SIGN_UP_NEW_USER.getHttpMethod()).to.equal('POST'); @@ -503,57 +758,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(); }); }); @@ -561,7 +816,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(); @@ -575,859 +830,1213 @@ describe('FIREBASE_AUTH_SIGN_UP_NEW_USER', () => { }); }); -describe('FirebaseAuthRequestHandler', () => { - let mockApp: FirebaseApp; - const mockedRequests: nock.Scope[] = []; - let stubs: sinon.SinonStub[] = []; - const mockAccessToken: string = utils.generateRandomAccessToken(); - let expectedHeaders: object; - - before(() => utils.mockFetchAccessTokenRequests(mockAccessToken)); - after(() => { - stubs = []; - nock.cleanAll(); - }); - - beforeEach(() => { - mockApp = mocks.app(); - - expectedHeaders = { - 'Content-Type': 'application/json', - 'X-Client-Version': 'Node/Admin/', +const AUTH_REQUEST_HANDLER_TESTS: HandlerTest[] = [ + { + name: 'FirebaseAuthRequestHandler', + init: (app: FirebaseApp) => { + return new AuthRequestHandler(app); + }, + path: (version: string, api: string, projectId: string) => { + return `/${version}/projects/${projectId}${api}`; + }, + supportsTenantManagement: true, + }, + { + name: 'FirebaseTenantRequestHandler', + init: (app: FirebaseApp) => { + return new TenantAwareAuthRequestHandler(app, TENANT_ID); + }, + path: (version: string, api: string, projectId: string) => { + return `/${version}/projects/${projectId}/tenants/${TENANT_ID}${api}`; + }, + supportsTenantManagement: false, + }, +]; + +const TENANT_ID = 'tenantId'; +AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { + describe(handler.name, () => { + let mockApp: FirebaseApp; + let stubs: sinon.SinonStub[] = []; + let getTokenStub: sinon.SinonStub; + const mockAccessToken: string = utils.generateRandomAccessToken(); + const expectedHeaders: {[key: string]: string} = { + 'X-Client-Version': `Node/Admin/${getSdkVersion()}`, 'Authorization': 'Bearer ' + mockAccessToken, }; - }); - - afterEach(() => { - _.forEach(stubs, (stub) => stub.restore()); - _.forEach(mockedRequests, (mockedRequest) => mockedRequest.done()); - return mockApp.delete(); - }); + 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, + url: `https://${host}${path}`, + headers: expectedHeaders, + data, + timeout, + }; + }; - describe('Constructor', () => { - it('should succeed with a FirebaseApp instance', () => { - expect(() => { - return new FirebaseAuthRequestHandler(mockApp); - }).not.to.throw(Error); + before(() => { + getTokenStub = utils.stubGetAccessToken(mockAccessToken); }); - }); - - describe('getAccountInfoByEmail', () => { - const httpMethod = 'POST'; - const host = 'www.googleapis.com'; - const port = 443; - const path = '/identitytoolkit/v3/relyingparty/getAccountInfo'; - const timeout = 10000; - it('should be fulfilled given a valid email', () => { - const expectedResult = { - users : [ - {email: 'user@example.com'}, - ], - }; - const data = {email: ['user@example.com']}; - const stub = sinon.stub(HttpRequestHandler.prototype, 'sendRequest') - .returns(Promise.resolve(expectedResult)); - stubs.push(stub); + after(() => { + stubs = []; + getTokenStub.restore(); + }); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.getAccountInfoByEmail('user@example.com') - .then((result) => { - expect(result).to.deep.equal(expectedResult); - expect(stub).to.have.been.calledOnce.and.calledWith( - host, port, path, httpMethod, data, expectedHeaders, timeout); - }); + beforeEach(() => { + mockApp = mocks.app(); + return mockApp.INTERNAL.getToken(); }); - it('should be rejected given an invalid email', () => { - const expectedResult = { - kind: 'identitytoolkit#GetAccountInfoResponse', - }; - const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); - const data = {email: ['user@example.com']}; - const stub = sinon.stub(HttpRequestHandler.prototype, 'sendRequest') - .returns(Promise.resolve(expectedResult)); - stubs.push(stub); + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + return mockApp.delete(); + }); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.getAccountInfoByEmail('user@example.com') - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - expect(stub).to.have.been.calledOnce.and.calledWith( - host, port, path, httpMethod, data, expectedHeaders, timeout); - }); + describe('Constructor', () => { + it('should succeed with a FirebaseApp instance', () => { + expect(() => { + return handler.init(mockApp); + }).not.to.throw(Error); + }); }); - }); - describe('getAccountInfoByUid', () => { - const httpMethod = 'POST'; - const host = 'www.googleapis.com'; - const port = 443; - const path = '/identitytoolkit/v3/relyingparty/getAccountInfo'; - const timeout = 10000; - it('should be fulfilled given a valid localId', () => { - const expectedResult = { + describe('Emulator Support', () => { + const method = 'POST'; + const path = handler.path('v1', '/accounts:lookup', 'project_id'); + const expectedResult = utils.responseFrom({ users : [ - {localId: 'uid'}, + { localId: 'uid' }, ], - }; - const data = {localId: ['uid']}; - - const stub = sinon.stub(HttpRequestHandler.prototype, 'sendRequest') - .returns(Promise.resolve(expectedResult)); - stubs.push(stub); + }); + const data = { localId: ['uid'] }; - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.getAccountInfoByUid('uid') - .then((result) => { - expect(result).to.deep.equal(expectedResult); - expect(stub).to.have.been.calledOnce.and.calledWith( - host, port, path, httpMethod, data, expectedHeaders, timeout); - }); - }); - it('should be rejected given an invalid localId', () => { - const expectedResult = { - kind: 'identitytoolkit#GetAccountInfoResponse', - }; - const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); - const data = {localId: ['uid']}; - - const stub = sinon.stub(HttpRequestHandler.prototype, 'sendRequest') - .returns(Promise.resolve(expectedResult)); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.getAccountInfoByUid('uid') - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - expect(stub).to.have.been.calledOnce.and.calledWith( - host, port, path, httpMethod, data, expectedHeaders, timeout); - }); - }); - it('should be rejected when the backend returns an error', () => { - const expectedResult = { - error: { - message: 'OPERATION_NOT_ALLOWED', - }, - }; - const expectedError = FirebaseAuthError.fromServerError('OPERATION_NOT_ALLOWED'); - const data = {localId: ['uid']}; + after(() => { + delete process.env.FIREBASE_AUTH_EMULATOR_HOST; + }) - const stub = sinon.stub(HttpRequestHandler.prototype, 'sendRequest') - .returns(Promise.resolve(expectedResult)); - stubs.push(stub); + 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 = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.getAccountInfoByUid('uid') - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - expect(stub).to.have.been.calledOnce.and.calledWith( - host, port, path, httpMethod, data, expectedHeaders, timeout); - }); - }); - }); + 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, + }); + }); + }); - describe('getAccountInfoByPhoneNumber', () => { - const httpMethod = 'POST'; - const host = 'www.googleapis.com'; - const port = 443; - const path = '/identitytoolkit/v3/relyingparty/getAccountInfo'; - const timeout = 10000; - it('should be fulfilled given a valid phoneNumber', () => { - const expectedResult = { - users : [ - { - localId: 'uid', - phoneNumber: '+11234567890', - providerUserInfo: [ - { - providerId: 'phone', - rawId: '+11234567890', - phoneNumber: '+11234567890', - }, - ], - }, - ], - }; - const data = { - phoneNumber: ['+11234567890'], - }; + 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(HttpRequestHandler.prototype, 'sendRequest') - .returns(Promise.resolve(expectedResult)); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.getAccountInfoByPhoneNumber('+11234567890') - .then((result) => { - expect(result).to.deep.equal(expectedResult); - expect(stub).to.have.been.calledOnce.and.calledWith( - host, port, path, httpMethod, data, expectedHeaders, timeout); - }); - }); - it('should be rejected given an invalid phoneNumber', () => { - const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_PHONE_NUMBER); - - const stub = sinon.stub(HttpRequestHandler.prototype, 'sendRequest'); - stubs.push(stub); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.getAccountInfoByPhoneNumber('invalid') - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - expect(stub).to.have.not.been.called; - }); + 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, + }); + }); + }); }); - it('should be rejected when the backend returns an error', () => { - const expectedResult = { - kind: 'identitytoolkit#GetAccountInfoResponse', - }; - const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); - const data = { - phoneNumber: ['+11234567890'], - }; - const stub = sinon.stub(HttpRequestHandler.prototype, 'sendRequest') - .returns(Promise.resolve(expectedResult)); - stubs.push(stub); + describe('createSessionCookie', () => { + const durationInMs = 24 * 60 * 60 * 1000; + const path = handler.path('v1', ':createSessionCookie', 'project_id'); + const method = 'POST'; - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.getAccountInfoByPhoneNumber('+11234567890') - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - expect(stub).to.have.been.calledOnce.and.calledWith( - host, port, path, httpMethod, data, expectedHeaders, timeout); + it('should be fulfilled given a valid localId', () => { + const expectedResult = utils.responseFrom({ + sessionCookie: 'SESSION_COOKIE', }); - }); - }); + const data = { idToken: 'ID_TOKEN', validDuration: durationInMs / 1000 }; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); - describe('downloadAccount', () => { - const httpMethod = 'POST'; - const host = 'www.googleapis.com'; - const port = 443; - const path = '/identitytoolkit/v3/relyingparty/downloadAccount'; - const timeout = 10000; - const nextPageToken = 'PAGE_TOKEN'; - const maxResults = 500; - const expectedResult = { - users : [ - {localId: 'uid1'}, - {localId: 'uid2'}, - ], - nextPageToken: 'NEXT_PAGE_TOKEN', - }; - it('should be fulfilled given a valid parameters', () => { - const data = { - maxResults, - nextPageToken, - }; + const requestHandler = handler.init(mockApp); + return requestHandler.createSessionCookie('ID_TOKEN', durationInMs) + .then((result) => { + expect(result).to.deep.equal('SESSION_COOKIE'); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); + }); + }); + it('should be fulfilled given a duration equal to the maximum allowed', () => { + const expectedResult = utils.responseFrom({ + sessionCookie: 'SESSION_COOKIE', + }); + const durationAtLimitInMs = 14 * 24 * 60 * 60 * 1000; + const data = { idToken: 'ID_TOKEN', validDuration: durationAtLimitInMs / 1000 }; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); - const stub = sinon.stub(HttpRequestHandler.prototype, 'sendRequest') - .returns(Promise.resolve(expectedResult)); - stubs.push(stub); + const requestHandler = handler.init(mockApp); + return requestHandler.createSessionCookie('ID_TOKEN', durationAtLimitInMs) + .then((result) => { + expect(result).to.deep.equal('SESSION_COOKIE'); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); + }); + }); + it('should be fulfilled given a duration equal to the minimum allowed', () => { + const expectedResult = utils.responseFrom({ + sessionCookie: 'SESSION_COOKIE', + }); + const durationAtLimitInMs = 5 * 60 * 1000; + const data = { idToken: 'ID_TOKEN', validDuration: durationAtLimitInMs / 1000 }; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.downloadAccount(maxResults, nextPageToken) - .then((result) => { - expect(result).to.deep.equal(expectedResult); - expect(stub).to.have.been.calledOnce.and.calledWith( - host, port, path, httpMethod, data, expectedHeaders, timeout); + const requestHandler = handler.init(mockApp); + return requestHandler.createSessionCookie('ID_TOKEN', durationAtLimitInMs) + .then((result) => { + expect(result).to.deep.equal('SESSION_COOKIE'); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); + }); + }); + it('should be rejected given an invalid ID token', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_ID_TOKEN, + ); + + const requestHandler = handler.init(mockApp); + return requestHandler.createSessionCookie('', durationInMs) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + }); + }); + it('should be rejected given an invalid duration', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_SESSION_COOKIE_DURATION, + ); + + const requestHandler = handler.init(mockApp); + return requestHandler.createSessionCookie('ID_TOKEN', 'invalid' as any) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + }); + }); + it('should be rejected given a duration less than minimum allowed', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_SESSION_COOKIE_DURATION, + ); + const outOfBoundDuration = 60 * 1000 * 5 - 1; + + const requestHandler = handler.init(mockApp); + return requestHandler.createSessionCookie('ID_TOKEN', outOfBoundDuration) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + }); + }); + it('should be rejected given a duration greater than maximum allowed', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_SESSION_COOKIE_DURATION, + ); + const outOfBoundDuration = 60 * 60 * 1000 * 24 * 14 + 1; + + const requestHandler = handler.init(mockApp); + return requestHandler.createSessionCookie('ID_TOKEN', outOfBoundDuration) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + }); + }); + it('should be rejected when the backend returns an error', () => { + const expectedResult = utils.errorFrom({ + error: { + message: 'INVALID_ID_TOKEN', + }, }); - }); - it('should be fulfilled with empty user array when no users exist', () => { - const emptyExpectedResult = { - users: [], - }; - const data = { - maxResults, - nextPageToken, - }; + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_ID_TOKEN); + const data = { idToken: 'invalid-token', validDuration: durationInMs / 1000 }; + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedResult); + stubs.push(stub); - const stub = sinon.stub(HttpRequestHandler.prototype, 'sendRequest') - .returns(Promise.resolve({})); - stubs.push(stub); + const requestHandler = handler.init(mockApp); + return requestHandler.createSessionCookie('invalid-token', durationInMs) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); + }); + }); + }); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.downloadAccount(maxResults, nextPageToken) - .then((result) => { - expect(result).to.deep.equal(emptyExpectedResult); - expect(stub).to.have.been.calledOnce.and.calledWith( - host, port, path, httpMethod, data, expectedHeaders, timeout); + describe('getAccountInfoByEmail', () => { + const path = handler.path('v1', '/accounts:lookup', 'project_id'); + const method = 'POST'; + it('should be fulfilled given a valid email', () => { + const expectedResult = utils.responseFrom({ + users : [ + { email: 'user@example.com' }, + ], }); - }); - it('should be fulfilled given no parameters', () => { - // Default maxResults should be used. - const data = { - maxResults: 1000, - }; + const data = { email: ['user@example.com'] }; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); - const stub = sinon.stub(HttpRequestHandler.prototype, 'sendRequest') - .returns(Promise.resolve(expectedResult)); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.downloadAccount() - .then((result) => { - expect(result).to.deep.equal(expectedResult); - expect(stub).to.have.been.calledOnce.and.calledWith( - host, port, path, httpMethod, data, expectedHeaders, timeout); - }); - }); - 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.`, - ); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.downloadAccount(1001, nextPageToken) - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - }); - }); - it('should be rejected given an invalid next page token', () => { - const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_PAGE_TOKEN, - ); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.downloadAccount(maxResults, '') - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - }); - }); - it('should be rejected when the backend returns an error', () => { - const expectedServerError = { - error: { - message: 'INVALID_PAGE_SELECTION', - }, - }; - const expectedError = FirebaseAuthError.fromServerError('INVALID_PAGE_SELECTION'); - const data = { - maxResults, - nextPageToken, - }; + const requestHandler = handler.init(mockApp); + return requestHandler.getAccountInfoByEmail('user@example.com') + .then((result) => { + expect(result).to.deep.equal(expectedResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith({ + method, + url: `https://${host}${path}`, + data, + headers: expectedHeaders, + timeout, + }); + }); + }); + it('should be rejected given an invalid email', () => { + const expectedResult = utils.responseFrom({ + kind: 'identitytoolkit#GetAccountInfoResponse', + }); + const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); + const data = { email: ['user@example.com'] }; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); - const stub = sinon.stub(HttpRequestHandler.prototype, 'sendRequest') - .returns(Promise.resolve(expectedServerError)); - stubs.push(stub); + const requestHandler = handler.init(mockApp); + return requestHandler.getAccountInfoByEmail('user@example.com') + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); + }); + }); + }); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.downloadAccount(maxResults, nextPageToken) - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - expect(stub).to.have.been.calledOnce.and.calledWith( - host, port, path, httpMethod, data, expectedHeaders, timeout); + describe('getAccountInfoByUid', () => { + const path = handler.path('v1', '/accounts:lookup', 'project_id'); + const method = 'POST'; + it('should be fulfilled given a valid localId', () => { + const expectedResult = utils.responseFrom({ + users : [ + { localId: 'uid' }, + ], }); - }); - }); + const data = { localId: ['uid'] }; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); - describe('deleteAccount', () => { - const httpMethod = 'POST'; - const host = 'www.googleapis.com'; - const port = 443; - const path = '/identitytoolkit/v3/relyingparty/deleteAccount'; - const timeout = 10000; - it('should be fulfilled given a valid localId', () => { - const expectedResult = { - kind: 'identitytoolkit#DeleteAccountResponse', - }; - const data = {localId: 'uid'}; - - const stub = sinon.stub(HttpRequestHandler.prototype, 'sendRequest') - .returns(Promise.resolve(expectedResult)); - stubs.push(stub); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.deleteAccount('uid') - .then((result) => { - expect(result).to.deep.equal(expectedResult); - expect(stub).to.have.been.calledOnce.and.calledWith( - host, port, path, httpMethod, data, expectedHeaders, timeout); - }); - }); - it('should be rejected when the backend returns an error', () => { - const expectedResult = { - error: { - message: 'OPERATION_NOT_ALLOWED', - }, - }; - const expectedError = FirebaseAuthError.fromServerError('OPERATION_NOT_ALLOWED'); - const data = {localId: 'uid'}; - - const stub = sinon.stub(HttpRequestHandler.prototype, 'sendRequest') - .returns(Promise.resolve(expectedResult)); - stubs.push(stub); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.deleteAccount('uid') - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - expect(stub).to.have.been.calledOnce.and.calledWith( - host, port, path, httpMethod, data, expectedHeaders, timeout); + const requestHandler = handler.init(mockApp); + return requestHandler.getAccountInfoByUid('uid') + .then((result) => { + expect(result).to.deep.equal(expectedResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); + }); + }); + it('should be rejected given an invalid localId', () => { + const expectedResult = utils.responseFrom({ + kind: 'identitytoolkit#GetAccountInfoResponse', }); - }); - }); + const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); + const data = { localId: ['uid'] }; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); - describe('updateExistingAccount', () => { - const httpMethod = 'POST'; - const host = 'www.googleapis.com'; - const port = 443; - const path = '/identitytoolkit/v3/relyingparty/setAccountInfo'; - const timeout = 10000; - const uid = '12345678'; - const validData = { - displayName: 'John Doe', - email: 'user@example.com', - emailVerified: true, - disabled: false, - photoURL: 'http://localhost/1234/photo.png', - password: 'password', - phoneNumber: '+11234567890', - ignoredProperty: 'value', - }; - const expectedValidData = { - localId: uid, - displayName: 'John Doe', - email: 'user@example.com', - emailVerified: true, - disableUser: false, - photoUrl: 'http://localhost/1234/photo.png', - password: 'password', - phoneNumber: '+11234567890', - }; - // Valid request to delete photoURL and displayName. - const validDeleteData = deepCopy(validData); - validDeleteData.displayName = null; - validDeleteData.photoURL = null; - const expectedValidDeleteData = { - localId: uid, - email: 'user@example.com', - emailVerified: true, - disableUser: false, - password: 'password', - phoneNumber: '+11234567890', - deleteAttribute: ['DISPLAY_NAME', 'PHOTO_URL'], - }; - // Valid request to delete phoneNumber. - const validDeletePhoneNumberData = deepCopy(validData); - validDeletePhoneNumberData.phoneNumber = null; - const expectedValidDeletePhoneNumberData = { - localId: uid, - displayName: 'John Doe', - email: 'user@example.com', - emailVerified: true, - disableUser: false, - photoUrl: 'http://localhost/1234/photo.png', - password: 'password', - deleteProvider: ['phone'], - }; - const invalidData = { - uid, - email: 'user@invalid@', - }; - const invalidPhoneNumberData = { - uid, - phoneNumber: 'invalid', - }; + const requestHandler = handler.init(mockApp); + return requestHandler.getAccountInfoByUid('uid') + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); + }); + }); + it('should be rejected when the backend returns an error', () => { + const expectedResult = utils.errorFrom({ + error: { + message: 'OPERATION_NOT_ALLOWED', + }, + }); + const expectedError = FirebaseAuthError.fromServerError('OPERATION_NOT_ALLOWED'); + const data = { localId: ['uid'] }; - it('should be fulfilled given a valid localId', () => { - // Successful result server response. - const expectedResult = { - kind: 'identitytoolkit#SetAccountInfoResponse', - localId: uid, - }; + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedResult); + stubs.push(stub); - const stub = sinon.stub(HttpRequestHandler.prototype, 'sendRequest') - .returns(Promise.resolve(expectedResult)); - stubs.push(stub); + const requestHandler = handler.init(mockApp); + return requestHandler.getAccountInfoByUid('uid') + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); + }); + }); + }); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Send empty update request. - return requestHandler.updateExistingAccount(uid, {}) - .then((returnedUid: string) => { - // uid should be returned. - expect(returnedUid).to.be.equal(uid); - // Confirm expected rpc request parameters sent. - expect(stub).to.have.been.calledOnce.and.calledWith( - host, port, path, httpMethod, {localId: uid}, expectedHeaders, timeout); + describe('getAccountInfoByPhoneNumber', () => { + const path = handler.path('v1', '/accounts:lookup', 'project_id'); + const method = 'POST'; + it('should be fulfilled given a valid phoneNumber', () => { + const expectedResult = utils.responseFrom({ + users : [ + { + localId: 'uid', + phoneNumber: '+11234567890', + providerUserInfo: [ + { + providerId: 'phone', + rawId: '+11234567890', + phoneNumber: '+11234567890', + }, + ], + }, + ], }); - }); + const data = { + phoneNumber: ['+11234567890'], + }; - it('should be fulfilled given valid parameters', () => { - // Successful result server response. - const expectedResult = { - kind: 'identitytoolkit#SetAccountInfoResponse', - localId: uid, - }; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.getAccountInfoByPhoneNumber('+11234567890') + .then((result) => { + expect(result).to.deep.equal(expectedResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); + }); + }); + it('should be rejected given an invalid phoneNumber', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_PHONE_NUMBER); - const stub = sinon.stub(HttpRequestHandler.prototype, 'sendRequest') - .returns(Promise.resolve(expectedResult)); - stubs.push(stub); + const stub = sinon.stub(HttpClient.prototype, 'send'); + stubs.push(stub); + const requestHandler = handler.init(mockApp); + return requestHandler.getAccountInfoByPhoneNumber('invalid') + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + expect(stub).to.have.not.been.called; + }); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Send update request with all possible valid parameters. - return requestHandler.updateExistingAccount(uid, validData) - .then((returnedUid: string) => { - // uid should be returned. - expect(returnedUid).to.be.equal(uid); - // Confirm expected rpc request parameters sent. - expect(stub).to.have.been.calledOnce.and.calledWith( - host, port, path, httpMethod, expectedValidData, expectedHeaders, timeout); + }); + it('should be rejected when the backend returns an error', () => { + const expectedResult = utils.responseFrom({ + kind: 'identitytoolkit#GetAccountInfoResponse', }); + const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); + const data = { + phoneNumber: ['+11234567890'], + }; + + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.getAccountInfoByPhoneNumber('+11234567890') + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); + }); + }); }); - it('should be fulfilled given valid profile parameters to delete', () => { - // Successful result server response. - const expectedResult = { - kind: 'identitytoolkit#SetAccountInfoResponse', - localId: uid, - }; + describe('getAccountInfoByIdentifiers', () => { + it('should throw when given more than 100 identifiers', () => { + const identifiers: UserIdentifier[] = []; + for (let i = 0; i < 101; i++) { + identifiers.push({ uid: 'id' + i }); + } + + const requestHandler = handler.init(mockApp); + expect(() => requestHandler.getAccountInfoByIdentifiers(identifiers)) + .to.throw(FirebaseAuthError) + .with.property('code', 'auth/maximum-user-count-exceeded'); + }); + + it('should return no results when given no identifiers', () => { + const requestHandler = handler.init(mockApp); + return requestHandler.getAccountInfoByIdentifiers([]) + .then((getUsersResult) => { + expect(getUsersResult).to.deep.equal({ users: [] }); + }); + }); - const stub = sinon.stub(HttpRequestHandler.prototype, 'sendRequest') - .returns(Promise.resolve(expectedResult)); - stubs.push(stub); + it('should return no users when given identifiers that do not exist', () => { + const expectedResult = utils.responseFrom({ users: [] }); + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Send update request to delete display name and photo URL. - return requestHandler.updateExistingAccount(uid, validDeleteData) - .then((returnedUid: string) => { - // uid should be returned. - expect(returnedUid).to.be.equal(uid); - // Confirm expected rpc request parameters sent. In this case, displayName - // and photoURL removed from request and deleteAttribute added. - expect(stub).to.have.been.calledOnce.and.calledWith( - host, port, path, httpMethod, expectedValidDeleteData, expectedHeaders, timeout); - }); - }); + const requestHandler = handler.init(mockApp); + const notFoundIds = [{ uid: 'id that doesnt exist' }]; + return requestHandler.getAccountInfoByIdentifiers(notFoundIds) + .then((getUsersResult) => { + expect(getUsersResult).to.deep.equal({ users: [] }); + }); + }); - it('should be fulfilled given phone number to delete', () => { - // Successful result server response. - const expectedResult = { - kind: 'identitytoolkit#SetAccountInfoResponse', - localId: uid, - }; + it('should throw when given an invalid uid', () => { + const requestHandler = handler.init(mockApp); + 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' }])) + .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' }])) + .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: '' }])) + .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' }, + ]; + + const requestHandler = handler.init(mockApp); + expect(() => requestHandler.getAccountInfoByIdentifiers(identifiers)) + .to.throw(FirebaseAuthError) + .with.property('code', 'auth/invalid-uid'); + }); + + it('returns users by various identifier types in a single call', async () => { + const mockUsers = [{ + localId: 'uid1', + email: 'user1@example.com', + phoneNumber: '+15555550001', + }, { + localId: 'uid2', + email: 'user2@example.com', + phoneNumber: '+15555550002', + }, { + localId: 'uid3', + email: 'user3@example.com', + phoneNumber: '+15555550003', + }, { + localId: 'uid4', + email: 'user4@example.com', + phoneNumber: '+15555550004', + providerUserInfo: [{ + providerId: 'google.com', + rawId: 'google_uid4', + }], + }]; + const expectedResult = utils.responseFrom({ users: mockUsers }) + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + const users = await requestHandler.getAccountInfoByIdentifiers([ + { uid: 'uid1' }, + { email: 'user2@example.com' }, + { phoneNumber: '+15555550003' }, + { providerId: 'google.com', providerUid: 'google_uid4' }, + { uid: 'this-user-doesnt-exist' }, + ]); - const stub = sinon.stub(HttpRequestHandler.prototype, 'sendRequest') - .returns(Promise.resolve(expectedResult)); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Send update request to delete phone number. - return requestHandler.updateExistingAccount(uid, validDeletePhoneNumberData) - .then((returnedUid: string) => { - // uid should be returned. - expect(returnedUid).to.be.equal(uid); - // Confirm expected rpc request parameters sent. In this case, phoneNumber - // removed from request and deleteProvider added. - expect(stub).to.have.been.calledOnce.and.calledWith( - host, port, path, httpMethod, expectedValidDeletePhoneNumberData, expectedHeaders, - timeout); - }); - }); - - it('should be rejected given invalid parameters such as email', () => { - // Expected error when an invalid email is provided. - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Send update request with invalid email. - return requestHandler.updateExistingAccount(uid, invalidData) - .then((returnedUid: string) => { - throw new Error('Unexpected success'); - }, (error) => { - // Invalid email error should be thrown. - expect(error).to.deep.equal(expectedError); - }); - }); - - it('should be rejected given invalid parameters such as phoneNumber', () => { - // Expected error when an invalid phone number is provided. - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Send update request with invalid phone number. - return requestHandler.updateExistingAccount(uid, invalidPhoneNumberData) - .then((returnedUid: string) => { - throw new Error('Unexpected success'); - }, (error) => { - // Invalid phone number error should be thrown. - expect(error).to.deep.equal(expectedError); - }); - }); - - it('should be rejected when the backend returns an error', () => { - // Backend returned error. - const expectedError = FirebaseAuthError.fromServerError('OPERATION_NOT_ALLOWED'); - const expectedResult = { - error: { - message: 'OPERATION_NOT_ALLOWED', + expect(users).to.deep.equal({ users: mockUsers }) + }); + }); + + describe('uploadAccount', () => { + const path = handler.path('v1', '/accounts:batchCreate', 'project_id'); + const tenantId = handler.supportsTenantManagement ? undefined : TENANT_ID; + const method = 'POST'; + const nowString = new Date().toUTCString(); + const users = [ + { + uid: '1234', + email: 'user@example.com', + passwordHash: Buffer.from('password'), + passwordSalt: Buffer.from('salt'), + displayName: 'Test User', + photoURL: 'https://www.example.com/1234/photo.png', + disabled: true, + metadata: { + lastSignInTime: nowString, + creationTime: nowString, + }, + providerData: [ + { + uid: 'google1234', + email: 'user@example.com', + photoURL: 'https://www.google.com/1234/photo.png', + displayName: 'Google User', + providerId: 'google.com', + }, + ], + multiFactor: { + enrolledFactors: [ + { + uid: 'mfaUid1', + phoneNumber: '+16505550001', + displayName: 'Corp phone number', + factorId: 'phone', + enrollmentTime: new Date().toUTCString(), + }, + ], + }, + customClaims: { admin: true }, + // Tenant ID accepted on user batch upload. + tenantId, + }, + { + uid: '9012', + email: 'johndoe@example.com', + passwordHash: Buffer.from('userpass'), + passwordSalt: Buffer.from('NaCl'), + }, + { uid: '5678', phoneNumber: '+16505550101' }, + ]; + const options = { + hash: { + algorithm: 'BCRYPT' as any, }, }; - const stub = sinon.stub(HttpRequestHandler.prototype, 'sendRequest') - .returns(Promise.resolve(expectedResult)); - stubs.push(stub); + 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".', + ); + const invalidOptions = { + hash: { + algorithm: 'invalid', + }, + } as any; + const stub = sinon.stub(HttpClient.prototype, 'send'); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + expect(() => { + requestHandler.uploadAccount(users, invalidOptions); + }).to.throw(expectedError.message); + expect(stub).to.have.not.been.called; + }); + + 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.', + ); + const stub = sinon.stub(HttpClient.prototype, 'send'); + stubs.push(stub); + + const testUsers: UserImportRecord[] = []; + for (let i = 0; i < 1001; i++) { + testUsers.push({ + uid: 'USER' + i.toString(), + email: 'user' + i.toString() + '@example.com', + passwordHash: Buffer.from('password'), + }); + } + + const requestHandler = handler.init(mockApp); + expect(() => { + requestHandler.uploadAccount(testUsers, options); + }).to.throw(expectedError.message); + expect(stub).to.have.not.been.called; + }); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.updateExistingAccount(uid, validData) - .then((returnedUid: string) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - expect(stub).to.have.been.calledOnce.and.calledWith( - host, port, path, httpMethod, expectedValidData, expectedHeaders, timeout); + if (handler.name === 'FirebaseTenantRequestHandler') { + it('should throw when a user record with mismatching tenant ID is provided', () => { + const mismatchIndex = 34; + const mismatchTenantId = 'MISMATCHING-TENANT-ID'; + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.MISMATCHING_TENANT_ID, + `UserRecord of index "${mismatchIndex}" has mismatching tenant ID "${mismatchTenantId}"`, + ); + const stub = sinon.stub(HttpClient.prototype, 'send'); + stubs.push(stub); + + const testUsers: UserImportRecord[] = []; + for (let i = 0; i < 100; i++) { + testUsers.push({ + uid: 'USER' + i.toString(), + email: 'user' + i.toString() + '@example.com', + passwordHash: Buffer.from('password'), + tenantId: i === mismatchIndex ? mismatchTenantId : undefined, + }); + } + + const requestHandler = handler.init(mockApp); + expect(() => { + requestHandler.uploadAccount(testUsers, options); + }).to.throw(expectedError.message); + expect(stub).to.have.not.been.called; }); - }); - }); + } - describe('setCustomUserClaims', () => { - const httpMethod = 'POST'; - const host = 'www.googleapis.com'; - const port = 443; - const path = '/identitytoolkit/v3/relyingparty/setAccountInfo'; - const timeout = 10000; - const uid = '12345678'; - const claims = {admin: true, groupId: '1234'}; - const expectedValidData = { - localId: uid, - customAttributes: JSON.stringify(claims), - }; - const expectedEmptyClaimsData = { - localId: uid, - customAttributes: JSON.stringify({}), - }; - const expectedResult = { - localId: uid, - }; + it('should resolve successfully when 1000 UserImportRecords are provided', () => { + const expectedResult = utils.responseFrom({}); + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); - it('should be fulfilled given a valid localId and customAttributes', () => { - // Successful result server response. - const stub = sinon.stub(HttpRequestHandler.prototype, 'sendRequest') - .returns(Promise.resolve(expectedResult)); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Send empty request. - return requestHandler.setCustomUserClaims(uid, claims) - .then((returnedUid: string) => { - // uid should be returned. - expect(returnedUid).to.be.equal(uid); - // Confirm expected rpc request parameters sent. - expect(stub).to.have.been.calledOnce.and.calledWith( - host, port, path, httpMethod, expectedValidData, - expectedHeaders, timeout); - }); - }); - - it('should be fulfilled given valid localId and null claims', () => { - // Successful result server response. - const stub = sinon.stub(HttpRequestHandler.prototype, 'sendRequest') - .returns(Promise.resolve(expectedResult)); - stubs.push(stub); - - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Send request to delete custom claims. - return requestHandler.setCustomUserClaims(uid, null) - .then((returnedUid: string) => { - // uid should be returned. - expect(returnedUid).to.be.equal(uid); - // Confirm expected rpc request parameters sent. - expect(stub).to.have.been.calledOnce.and.calledWith( - host, port, path, httpMethod, expectedEmptyClaimsData, - expectedHeaders, timeout); - }); - }); - - it('should be rejected given invalid parameters such as uid', () => { - // Expected error when an invalid uid is provided. - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_UID); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Send request with invalid uid. - return requestHandler.setCustomUserClaims('', claims) - .then((returnedUid: string) => { - throw new Error('Unexpected success'); - }, (error) => { - // Invalid uid error should be thrown. - expect(error).to.deep.equal(expectedError); - }); - }); - - it('should be rejected given invalid parameters such as customClaims', () => { - // Expected error when invalid claims are provided. - const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, - 'CustomUserClaims argument must be an object or null.', - ); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Send request with invalid claims. - return requestHandler.setCustomUserClaims(uid, 'invalid' as any) - .then((returnedUid: string) => { - throw new Error('Unexpected success'); - }, (error) => { - // Invalid argument error should be thrown. - expect(error).to.deep.equal(expectedError); - }); - }); - - it('should be rejected given customClaims with blacklisted claims', () => { - // Expected error when invalid claims are provided. - const expectedError = new FirebaseAuthError( - AuthClientErrorCode.FORBIDDEN_CLAIM, - `Developer claim "aud" is reserved and cannot be specified.`, - ); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - const blacklistedClaims = {admin: true, aud: 'bla'}; - // Send request with blacklisted claims. - return requestHandler.setCustomUserClaims(uid, blacklistedClaims) - .then((returnedUid: string) => { - throw new Error('Unexpected success'); - }, (error) => { - // Forbidden claims error should be thrown. - expect(error).to.deep.equal(expectedError); - }); - }); - - it('should be rejected when the backend returns an error', () => { - // Backend returned error. - const expectedError = FirebaseAuthError.fromServerError('USER_NOT_FOUND'); - const expectedServerError = { - error: { - message: 'USER_NOT_FOUND', - }, - }; + const testUsers = []; + for (let i = 0; i < 1000; i++) { + testUsers.push({ + uid: 'USER' + i.toString(), + email: 'user' + i.toString() + '@example.com', + passwordHash: Buffer.from('password'), + }); + } + + const requestHandler = handler.init(mockApp); + const userImportBuilder = new UserImportBuilder(testUsers, options); + return requestHandler.uploadAccount(testUsers, options) + .then((result) => { + expect(result).to.deep.equal(userImportBuilder.buildResponse([])); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, method, userImportBuilder.buildRequest())); + }); - const stub = sinon.stub(HttpRequestHandler.prototype, 'sendRequest') - .returns(Promise.resolve(expectedServerError)); - stubs.push(stub); + }); + + it('should resolve with expected result on underlying API success', () => { + const expectedResult = utils.responseFrom({}); + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + const userImportBuilder = new UserImportBuilder(users, options); + return requestHandler.uploadAccount(users, options) + .then((result) => { + expect(result).to.deep.equal(userImportBuilder.buildResponse([])); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, method, userImportBuilder.buildRequest())); + }); + }); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.setCustomUserClaims(uid, claims) - .then((returnedUid: string) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - expect(stub).to.have.been.calledOnce.and.calledWith( - host, port, path, httpMethod, expectedValidData, expectedHeaders, timeout); + 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' }, + ], }); - }); - }); + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); - describe('revokeRefreshTokens', () => { - const httpMethod = 'POST'; - const host = 'www.googleapis.com'; - const port = 443; - const path = '/identitytoolkit/v3/relyingparty/setAccountInfo'; - const timeout = 10000; - const uid = '12345678'; - const now = new Date(); - const expectedResult = { - localId: uid, - }; - let clock; + const requestHandler = handler.init(mockApp); + const userImportBuilder = new UserImportBuilder(users, options); + return requestHandler.uploadAccount(users, options) + .then((result) => { + expectUserImportResult( + result, userImportBuilder.buildResponse(expectedResult.data.error)); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, method, userImportBuilder.buildRequest())); + }); + }); - beforeEach(() => { - clock = sinon.useFakeTimers(now.getTime()); - }); + 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' }, + ] 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) }, + ], + }; + const stub = sinon.stub(HttpClient.prototype, 'send'); + stubs.push(stub); - afterEach(() => { - clock.restore(); + const requestHandler = handler.init(mockApp); + return requestHandler.uploadAccount(testUsers) + .then((result) => { + expectUserImportResult(result, expectedResult); + expect(stub).to.have.not.been.called; + }); + }); + + 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: 'user13', + providerData: [{ providerId: 'google.com', uid: 'RAW_ID', displayName: false }], + }, + { + uid: 'user14', + providerData: [{ providerId: 'google.com', uid: 'RAW_ID', email: 'invalid' }], + }, + { + uid: 'user15', + providerData: [{ providerId: 'google.com', uid: 'RAW_ID', photoURL: 'invalid' }], + }, + { uid: 'user16', providerData: [{}] }, + { email: 'user17@example.com' }, + { + uid: 'user18', + email: 'user18@example.com', + multiFactor: { + enrolledFactors: [ + { + // Invalid mfa enrollment ID. + uid: '', + factorId: 'phone', + phoneNumber: '+16505550001', + }, + ], + }, + }, + { + uid: 'user19', + email: 'user19@example.com', + multiFactor: { + enrolledFactors: [ + { + uid: 'mfaUid1', + factorId: 'phone', + // Invalid display name. + displayName: false, + phoneNumber: '+16505550002', + }, + ], + }, + }, + { + uid: 'user20', + email: 'user20@example.com', + multiFactor: { + enrolledFactors: [ + { + uid: 'mfaUid2', + factorId: 'phone', + // Invalid enrollment time. + enrollmentTime: 'invalid', + phoneNumber: '+16505550003', + }, + ], + }, + }, + { + uid: 'user21', + email: 'user21@example.com', + multiFactor: { + enrolledFactors: [ + { + uid: 'mfaUid3', + factorId: 'phone', + // Invalid phone number + phoneNumber: 'invalid', + enrollmentTime: new Date().toUTCString(), + }, + ], + }, + }, + { + uid: 'user22', + email: 'user22@example.com', + multiFactor: { + enrolledFactors: [ + { + uid: 'mfaUid3', + // Invalid factor ID. + factorId: 'invalid', + phoneNumber: '+16505550003', + enrollmentTime: new Date().toUTCString(), + }, + ], + }, + }, + ] as any; + const validOptions = { + hash: { + algorithm: 'BCRYPT', + }, + } as any; + const expectedResult = { + 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: 9, + error: new FirebaseAuthError( + AuthClientErrorCode.FORBIDDEN_CLAIM, + '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: 12, + error: new FirebaseAuthError( + AuthClientErrorCode.INVALID_UID, + '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.', + ), + }, + { + index: 14, + error: new FirebaseAuthError( + AuthClientErrorCode.INVALID_EMAIL, + '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.', + ), + }, + { index: 16, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID) }, + { index: 17, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_UID) }, + { + index: 18, + error: new FirebaseAuthError( + AuthClientErrorCode.INVALID_UID, + '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.', + ), + }, + { + index: 20, + error: new FirebaseAuthError( + AuthClientErrorCode.INVALID_ENROLLMENT_TIME, + '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.', + ), + }, + { + index: 22, + error: new FirebaseAuthError( + AuthClientErrorCode.UNSUPPORTED_SECOND_FACTOR, + `Unsupported second factor "${JSON.stringify(testUsers[22].multiFactor.enrolledFactors[0])}" provided.`, + ), + }, + ], + }; + const stub = sinon.stub(HttpClient.prototype, 'send'); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.uploadAccount(testUsers, validOptions) + .then((result) => { + expectUserImportResult(result, expectedResult); + expect(stub).to.have.not.been.called; + }); + }); + + it('should be rejected when the backend returns an error', () => { + const expectedServerError = utils.errorFrom({ + error: { + message: 'INTERNAL_ERROR', + }, + }); + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'An internal error has occurred. Raw server response: ' + + `"${JSON.stringify(expectedServerError.response.data)}"`, + ); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + const userImportBuilder = new UserImportBuilder(users, options); + return requestHandler.uploadAccount(users, options) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, method, userImportBuilder.buildRequest())); + }); + }); }); - it('should be fulfilled given a valid uid', () => { - const requestData = { - localId: uid, - // Current time should be passed, rounded up. - validSince: Math.ceil((now.getTime() + 5000) / 1000), - }; + describe('downloadAccount', () => { + const path = handler.path('v1', '/accounts:batchGet', 'project_id'); + const method = 'GET'; + const nextPageToken = 'PAGE_TOKEN'; + const maxResults = 500; + const expectedResult = utils.responseFrom({ + users : [ + { localId: 'uid1' }, + { localId: 'uid2' }, + ], + nextPageToken: 'NEXT_PAGE_TOKEN', + }); + it('should be fulfilled given a valid parameters', () => { + const data = { + maxResults, + nextPageToken, + }; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); - const stub = sinon.stub(HttpRequestHandler.prototype, 'sendRequest') - .returns(Promise.resolve(expectedResult)); - stubs.push(stub); + const requestHandler = handler.init(mockApp); + return requestHandler.downloadAccount(maxResults, nextPageToken) + .then((result) => { + expect(result).to.deep.equal(expectedResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); + }); + }); + it('should be fulfilled with empty user array when no users exist', () => { + const data = { + maxResults, + nextPageToken, + }; + + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({})); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.downloadAccount(maxResults, nextPageToken) + .then((result) => { + expect(result).to.deep.equal({ users: [] }); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); + }); + }); + it('should be fulfilled given no parameters', () => { + // Default maxResults should be used. + const data = { + maxResults: 1000, + }; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Simulate 5 seconds passed. - clock.tick(5000); - return requestHandler.revokeRefreshTokens(uid) - .then((returnedUid: string) => { - expect(returnedUid).to.be.equal(uid); - expect(stub).to.have.been.calledOnce.and.calledWith( - host, port, path, httpMethod, requestData, expectedHeaders, timeout); + const requestHandler = handler.init(mockApp); + return requestHandler.downloadAccount() + .then((result) => { + expect(result).to.deep.equal(expectedResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); + }); + }); + 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.', + ); + + const requestHandler = handler.init(mockApp); + return requestHandler.downloadAccount(1001, nextPageToken) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + }); + }); + it('should be rejected given an invalid next page token', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_PAGE_TOKEN, + ); + + const requestHandler = handler.init(mockApp); + return requestHandler.downloadAccount(maxResults, '') + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + }); + }); + it('should be rejected when the backend returns an error', () => { + const expectedServerError = utils.errorFrom({ + error: { + message: 'INVALID_PAGE_SELECTION', + }, }); + const expectedError = FirebaseAuthError.fromServerError('INVALID_PAGE_SELECTION'); + const data = { + maxResults, + nextPageToken, + }; + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.downloadAccount(maxResults, nextPageToken) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); + }); + }); }); - it('should be rejected given an invalid uid', () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_UID); - const invalidUid: any = {localId: uid}; + describe('deleteAccount', () => { + const path = handler.path('v1', '/accounts:delete', 'project_id'); + const method = 'POST'; + it('should be fulfilled given a valid localId', () => { + const expectedResult = utils.responseFrom({ + kind: 'identitytoolkit#DeleteAccountResponse', + }); + const data = { localId: 'uid' }; - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.revokeRefreshTokens(invalidUid as any) - .then((resp) => { - throw new Error('Unexpected success'); - }, (error) => { - // Invalid uid error should be thrown. - expect(error).to.deep.equal(expectedError); + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + const requestHandler = handler.init(mockApp); + return requestHandler.deleteAccount('uid') + .then((result) => { + expect(result).to.deep.equal(expectedResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); + }); + }); + it('should be rejected when the backend returns an error', () => { + const expectedResult = utils.errorFrom({ + error: { + message: 'OPERATION_NOT_ALLOWED', + }, }); + const expectedError = FirebaseAuthError.fromServerError('OPERATION_NOT_ALLOWED'); + const data = { localId: 'uid' }; + + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedResult); + stubs.push(stub); + const requestHandler = handler.init(mockApp); + return requestHandler.deleteAccount('uid') + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); + }); + }); }); - it('should be rejected when the backend returns an error', () => { - // Backend returned error. - const expectedError = FirebaseAuthError.fromServerError('USER_NOT_FOUND'); - const expectedServerError = { - error: { - message: 'USER_NOT_FOUND', - }, - }; - const requestData = { - localId: uid, - validSince: Math.ceil((now.getTime() + 5000) / 1000), - }; + describe('deleteAccounts', () => { + const path = handler.path('v1', '/accounts:batchDelete', 'project_id'); + const method = 'POST'; - const stub = sinon.stub(HttpRequestHandler.prototype, 'sendRequest') - .returns(Promise.resolve(expectedServerError)); - stubs.push(stub); + it('should succeed given an empty list', () => { + const requestHandler = handler.init(mockApp); + return requestHandler.deleteAccounts([], /*force=*/true) + .then((deleteUsersResult) => { + expect(deleteUsersResult).to.deep.equal({}); + }); + }); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Simulate 5 seconds passed. - clock.tick(5000); - return requestHandler.revokeRefreshTokens(uid) - .then((returnedUid: string) => { - throw new Error('Unexpected success'); - }, (error) => { - expect(error).to.deep.equal(expectedError); - expect(stub).to.have.been.calledOnce.and.calledWith( - host, port, path, httpMethod, requestData, expectedHeaders, timeout); - }); + it('should be rejected when given more than 1000 identifiers', () => { + const ids: string[] = []; + for (let i = 0; i < 1001; i++) { + ids.push('id' + i); + } + + const requestHandler = handler.init(mockApp); + expect(() => requestHandler.deleteAccounts(ids, /*force=*/true)) + .to.throw(FirebaseAuthError) + .with.property('code', 'auth/maximum-user-count-exceeded'); + }); + + it('should immediately fail given an invalid id', () => { + const requestHandler = handler.init(mockApp); + expect(() => requestHandler.deleteAccounts(['too long ' + ('.' as any).repeat(128)], /*force=*/true)) + .to.throw(FirebaseAuthError) + .with.property('code', 'auth/invalid-uid'); + }); + + it('should be fulfilled given valid uids', async () => { + const expectedResult = utils.responseFrom({}); + const data = { localIds: ['uid1', 'uid2', 'uid3'], force: true }; + + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + const requestHandler = handler.init(mockApp); + return requestHandler.deleteAccounts(['uid1', 'uid2', 'uid3'], /*force=*/true) + .then((result) => { + expect(result).to.deep.equal({}) + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); + }); + }); }); - }); - describe('createNewAccount', () => { - describe('with uid specified', () => { - const httpMethod = 'POST'; - const host = 'www.googleapis.com'; - const port = 443; - const path = '/identitytoolkit/v3/relyingparty/signupNewUser'; - const timeout = 10000; + describe('updateExistingAccount', () => { + const now = new Date('2019-10-25T04:30:52.000Z'); + const path = handler.path('v1', '/accounts:update', 'project_id'); + const method = 'POST'; const uid = '12345678'; - const validData = { - uid, + const validData: UpdateRequest = { displayName: 'John Doe', email: 'user@example.com', emailVerified: true, @@ -1435,17 +2044,88 @@ describe('FirebaseAuthRequestHandler', () => { photoURL: 'http://localhost/1234/photo.png', password: 'password', phoneNumber: '+11234567890', - ignoredProperty: 'value', + multiFactor: { + enrolledFactors: [ + { + uid: 'enrolledSecondFactor1', + phoneNumber: '+16505557348', + displayName: 'Spouse\'s phone number', + factorId: 'phone', + enrollmentTime: now.toUTCString(), + } as UpdateMultiFactorInfoRequest, + { + uid: 'enrolledSecondFactor2', + phoneNumber: '+16505551000', + factorId: 'phone', + } as UpdateMultiFactorInfoRequest, + { + // No error should be thrown when no uid is specified. + phoneNumber: '+16505551234', + factorId: 'phone', + } as UpdateMultiFactorInfoRequest, + ], + }, }; + (validData as any).ignoredProperty = 'value'; const expectedValidData = { localId: uid, displayName: 'John Doe', email: 'user@example.com', emailVerified: true, - disabled: false, + disableUser: false, photoUrl: 'http://localhost/1234/photo.png', password: 'password', phoneNumber: '+11234567890', + mfa: { + enrollments: [ + { + mfaEnrollmentId: 'enrolledSecondFactor1', + phoneInfo: '+16505557348', + displayName: 'Spouse\'s phone number', + enrolledAt: now.toISOString(), + }, + { + mfaEnrollmentId: 'enrolledSecondFactor2', + phoneInfo: '+16505551000', + }, + { + phoneInfo: '+16505551234', + }, + ], + }, + }; + // Valid request to delete photoURL and displayName. + const validDeleteData = deepCopy(validData); + validDeleteData.displayName = null; + validDeleteData.photoURL = null; + delete validDeleteData.multiFactor; + const expectedValidDeleteData = { + localId: uid, + email: 'user@example.com', + emailVerified: true, + disableUser: false, + password: 'password', + phoneNumber: '+11234567890', + deleteAttribute: ['DISPLAY_NAME', 'PHOTO_URL'], + }; + // Valid request to delete phoneNumber. + const validDeletePhoneNumberData = deepCopy(validData); + validDeletePhoneNumberData.phoneNumber = null; + delete validDeletePhoneNumberData.multiFactor; + const expectedValidDeletePhoneNumberData = { + localId: uid, + displayName: 'John Doe', + email: 'user@example.com', + emailVerified: true, + disableUser: false, + photoUrl: 'http://localhost/1234/photo.png', + password: 'password', + deleteProvider: ['phone'], + }; + // Valid request to delete all second factors. + const expectedValidDeleteMfaData = { + localId: uid, + mfa: {}, }; const invalidData = { uid, @@ -1455,326 +2135,2875 @@ describe('FirebaseAuthRequestHandler', () => { uid, phoneNumber: 'invalid', }; - const emptyRequest = { + + it('should be fulfilled given a valid localId', () => { + // Successful result server response. + const expectedResult = utils.responseFrom({ + kind: 'identitytoolkit#SetAccountInfoResponse', + localId: uid, + }); + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + // Send empty update request. + return requestHandler.updateExistingAccount(uid, {}) + .then((returnedUid: string) => { + // uid should be returned. + 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 })); + }); + }); + + it('should be fulfilled given valid parameters', () => { + // Successful result server response. + const expectedResult = utils.responseFrom({ + kind: 'identitytoolkit#SetAccountInfoResponse', + localId: uid, + }); + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + // Send update request with all possible valid parameters. + return requestHandler.updateExistingAccount(uid, validData) + .then((returnedUid: string) => { + // uid should be returned. + expect(returnedUid).to.be.equal(uid); + // Confirm expected rpc request parameters sent. + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, method, expectedValidData)); + }); + }); + + it('should be fulfilled given valid profile parameters to delete', () => { + // Successful result server response. + const expectedResult = utils.responseFrom({ + kind: 'identitytoolkit#SetAccountInfoResponse', + localId: uid, + }); + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + // Send update request to delete display name and photo URL. + return requestHandler.updateExistingAccount(uid, validDeleteData) + .then((returnedUid: string) => { + // uid should be returned. + expect(returnedUid).to.be.equal(uid); + // Confirm expected rpc request parameters sent. In this case, displayName + // and photoURL removed from request and deleteAttribute added. + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, method, expectedValidDeleteData)); + }); + }); + + it('should be fulfilled given phone number to delete', () => { + // Successful result server response. + const expectedResult = utils.responseFrom({ + kind: 'identitytoolkit#SetAccountInfoResponse', + localId: uid, + }); + + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + // Send update request to delete phone number. + return requestHandler.updateExistingAccount(uid, validDeletePhoneNumberData) + .then((returnedUid: string) => { + // uid should be returned. + expect(returnedUid).to.be.equal(uid); + // Confirm expected rpc request parameters sent. In this case, phoneNumber + // removed from request and deleteProvider added. + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, method, expectedValidDeletePhoneNumberData)); + }); + }); + + it('should be fulfilled given null enrolled factors', () => { + // Successful result server response. + const expectedResult = utils.responseFrom({ + localId: uid, + }); + + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + // Send update request to delete enrolled factors. + return requestHandler.updateExistingAccount(uid, { multiFactor: { enrolledFactors: null } }) + .then((returnedUid: string) => { + // uid should be returned. + expect(returnedUid).to.be.equal(uid); + // Confirm expected rpc request parameters sent. In this case, mfa is set to + // an empty object. + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, method, expectedValidDeleteMfaData)); + }); + }); + + it('should be fulfilled given empty enrolled factors array', () => { + // Successful result server response. + const expectedResult = utils.responseFrom({ + localId: uid, + }); + + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + // Send update request to delete enrolled factors. + return requestHandler.updateExistingAccount(uid, { multiFactor: { enrolledFactors: [] } }) + .then((returnedUid: string) => { + // uid should be returned. + expect(returnedUid).to.be.equal(uid); + // Confirm expected rpc request parameters sent. In this case, mfa is set to + // an empty object. + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, method, expectedValidDeleteMfaData)); + }); + }); + + it('should be rejected given invalid parameters such as email', () => { + // Expected error when an invalid email is provided. + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL); + const requestHandler = handler.init(mockApp); + // Send update request with invalid email. + return requestHandler.updateExistingAccount(uid, invalidData) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Invalid email error should be thrown. + expect(error).to.deep.include(expectedError); + }); + }); + + const unsupportedSecondFactor = { + uid: 'enrolledSecondFactor1', + secret: 'SECRET', + displayName: 'Google Authenticator on personal phone', + factorId: 'totp', + }; + const invalidSecondFactorTests: InvalidMultiFactorUpdateTest[] = [ + { + name: 'invalid second factor uid', + error: new FirebaseAuthError( + AuthClientErrorCode.INVALID_UID, + 'The second factor "uid" must be a valid non-empty string.', + ), + secondFactor: { + uid: ['enrollmentId'], + phoneNumber: '+16505557348', + displayName: 'Spouse\'s phone number', + factorId: 'phone', + }, + }, + { + name: 'invalid second factor display name', + error: new FirebaseAuthError( + AuthClientErrorCode.INVALID_DISPLAY_NAME, + 'The second factor "displayName" for "enrolledSecondFactor1" must be a valid string.', + ), + secondFactor: { + uid: 'enrolledSecondFactor1', + phoneNumber: '+16505557348', + displayName: ['Corp phone number'], + factorId: 'phone', + }, + }, + { + 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.'), + secondFactor: { + uid: 'enrolledSecondFactor1', + phoneNumber: 'invalid', + displayName: 'Spouse\'s phone number', + factorId: 'phone', + }, + }, + { + 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.'), + secondFactor: { + uid: 'enrolledSecondFactor1', + phoneNumber: '+16505557348', + displayName: 'Spouse\'s phone number', + factorId: 'phone', + enrollmentTime: 'invalid', + }, + }, + { + name: 'invalid second factor type', + error: new FirebaseAuthError( + AuthClientErrorCode.UNSUPPORTED_SECOND_FACTOR, + `Unsupported second factor "${JSON.stringify(unsupportedSecondFactor)}" provided.`), + secondFactor: unsupportedSecondFactor, + }, + ]; + invalidSecondFactorTests.forEach((invalidSecondFactorTest) => { + it(`should be rejected given an ${invalidSecondFactorTest.name}`, () => { + const invalidSecondFactorData = { + multiFactor: { + enrolledFactors: [invalidSecondFactorTest.secondFactor], + }, + }; + const requestHandler = handler.init(mockApp); + return requestHandler.updateExistingAccount(uid, invalidSecondFactorData as any) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Expected error should be thrown. + expect(error).to.deep.include(invalidSecondFactorTest.error); + }); + }); + }); + + it('should be rejected given a tenant ID to modify', () => { + const dataWithModifiedTenantId = deepCopy(validData); + (dataWithModifiedTenantId as any).tenantId = 'MODIFIED-TENANT-ID'; + // Expected error when a tenant ID is provided. + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"tenantId" is an invalid "UpdateRequest" property.', + ); + const requestHandler = handler.init(mockApp); + // Send update request with tenant ID. + return requestHandler.updateExistingAccount(uid, dataWithModifiedTenantId) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Invalid argument error should be thrown. + expect(error).to.deep.include(expectedError); + }); + }); + + it('should be rejected given invalid parameters such as phoneNumber', () => { + // Expected error when an invalid phone number is provided. + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER); + const requestHandler = handler.init(mockApp); + // Send update request with invalid phone number. + return requestHandler.updateExistingAccount(uid, invalidPhoneNumberData) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Invalid phone number error should be thrown. + expect(error).to.deep.include(expectedError); + }); + }); + + it('should be rejected when the backend returns an error', () => { + // Backend returned error. + const expectedError = FirebaseAuthError.fromServerError('OPERATION_NOT_ALLOWED'); + const expectedResult = utils.errorFrom({ + error: { + message: 'OPERATION_NOT_ALLOWED', + }, + }); + + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.updateExistingAccount(uid, validData) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, method, expectedValidData)); + }); + }); + }); + + describe('setCustomUserClaims', () => { + const path = handler.path('v1', '/accounts:update', 'project_id'); + const method = 'POST'; + const uid = '12345678'; + const claims = { admin: true, groupId: '1234' }; + const expectedValidData = { + localId: uid, + customAttributes: JSON.stringify(claims), + }; + const expectedEmptyClaimsData = { localId: uid, + customAttributes: JSON.stringify({}), }; - it('should be fulfilled given a valid localId', () => { - // Successful uploadAccount response. - const expectedResult = { - kind: 'identitytoolkit#SignupNewUserResponse', + const expectedResult = utils.responseFrom({ + localId: uid, + }); + + it('should be fulfilled given a valid localId and customAttributes', () => { + // Successful result server response. + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + // Send empty request. + return requestHandler.setCustomUserClaims(uid, claims) + .then((returnedUid: string) => { + // uid should be returned. + expect(returnedUid).to.be.equal(uid); + // Confirm expected rpc request parameters sent. + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, method, expectedValidData)); + }); + }); + + it('should be fulfilled given valid localId and null claims', () => { + // Successful result server response. + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + // Send request to delete custom claims. + return requestHandler.setCustomUserClaims(uid, null) + .then((returnedUid: string) => { + // uid should be returned. + expect(returnedUid).to.be.equal(uid); + // Confirm expected rpc request parameters sent. + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, method, expectedEmptyClaimsData)); + }); + }); + + it('should be rejected given invalid parameters such as uid', () => { + // Expected error when an invalid uid is provided. + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_UID); + const requestHandler = handler.init(mockApp); + // Send request with invalid uid. + return requestHandler.setCustomUserClaims('', claims) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Invalid uid error should be thrown. + expect(error).to.deep.include(expectedError); + }); + }); + + it('should be rejected given invalid parameters such as customClaims', () => { + // Expected error when invalid claims are provided. + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + 'CustomUserClaims argument must be an object or null.', + ); + const requestHandler = handler.init(mockApp); + // Send request with invalid claims. + return requestHandler.setCustomUserClaims(uid, 'invalid' as any) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Invalid argument error should be thrown. + expect(error).to.deep.include(expectedError); + }); + }); + + it('should be rejected given customClaims with blacklisted claims', () => { + // Expected error when invalid claims are provided. + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.FORBIDDEN_CLAIM, + 'Developer claim "aud" is reserved and cannot be specified.', + ); + const requestHandler = handler.init(mockApp); + const blacklistedClaims = { admin: true, aud: 'bla' }; + // Send request with blacklisted claims. + return requestHandler.setCustomUserClaims(uid, blacklistedClaims) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Forbidden claims error should be thrown. + expect(error).to.deep.include(expectedError); + }); + }); + + it('should be rejected when the backend returns an error', () => { + // Backend returned error. + const expectedError = FirebaseAuthError.fromServerError('USER_NOT_FOUND'); + const expectedServerError = utils.errorFrom({ + error: { + message: 'USER_NOT_FOUND', + }, + }); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.setCustomUserClaims(uid, claims) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, method, expectedValidData)); + }); + }); + }); + + describe('revokeRefreshTokens', () => { + const path = handler.path('v1', '/accounts:update', 'project_id'); + const method = 'POST'; + const uid = '12345678'; + const now = new Date(); + const expectedResult = utils.responseFrom({ + localId: uid, + }); + let clock: sinon.SinonFakeTimers; + + beforeEach(() => { + clock = sinon.useFakeTimers(now.getTime()); + }); + + afterEach(() => { + clock.restore(); + }); + + it('should be fulfilled given a valid uid', () => { + const requestData = { + localId: uid, + // Current time should be passed, rounded down. + validSince: Math.floor((now.getTime() + 5000) / 1000), + }; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + // Simulate 5 seconds passed. + clock.tick(5000); + return requestHandler.revokeRefreshTokens(uid) + .then((returnedUid: string) => { + expect(returnedUid).to.be.equal(uid); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, requestData)); + }); + }); + + it('should be rejected given an invalid uid', () => { + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_UID); + const invalidUid: any = { localId: uid }; + + const requestHandler = handler.init(mockApp); + return requestHandler.revokeRefreshTokens(invalidUid as any) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Invalid uid error should be thrown. + expect(error).to.deep.include(expectedError); + }); + }); + + it('should be rejected when the backend returns an error', () => { + // Backend returned error. + const expectedError = FirebaseAuthError.fromServerError('USER_NOT_FOUND'); + const expectedServerError = utils.errorFrom({ + error: { + message: 'USER_NOT_FOUND', + }, + }); + const requestData = { + localId: uid, + validSince: Math.floor((now.getTime() + 5000) / 1000), + }; + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + // Simulate 5 seconds passed. + clock.tick(5000); + return requestHandler.revokeRefreshTokens(uid) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, requestData)); + }); + }); + }); + + describe('createNewAccount', () => { + describe('with uid specified', () => { + const path = handler.path('v1', '/accounts', 'project_id'); + const method = 'POST'; + const uid = '12345678'; + const validData = { + uid, + displayName: 'John Doe', + email: 'user@example.com', + emailVerified: true, + disabled: false, + photoURL: 'http://localhost/1234/photo.png', + password: 'password', + phoneNumber: '+11234567890', + ignoredProperty: 'value', + multiFactor: { + enrolledFactors: [ + { + phoneNumber: '+16505557348', + displayName: 'Spouse\'s phone number', + factorId: 'phone', + }, + { + phoneNumber: '+16505551000', + factorId: 'phone', + }, + ], + }, + }; + const expectedValidData = { + localId: uid, + displayName: 'John Doe', + email: 'user@example.com', + emailVerified: true, + disabled: false, + photoUrl: 'http://localhost/1234/photo.png', + password: 'password', + phoneNumber: '+11234567890', + mfaInfo: [ + { + phoneInfo: '+16505557348', + displayName: 'Spouse\'s phone number', + }, + { + phoneInfo: '+16505551000', + }, + ], + }; + const invalidData = { + uid, + email: 'user@invalid@', + }; + const invalidPhoneNumberData = { + uid, + phoneNumber: 'invalid', + }; + const emptyRequest = { localId: uid, }; + it('should be fulfilled given a valid localId', () => { + // Successful uploadAccount response. + const expectedResult = utils.responseFrom({ + kind: 'identitytoolkit#SignupNewUserResponse', + localId: uid, + }); + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + // Send empty create new account request with only a uid provided. + return requestHandler.createNewAccount({ uid }) + .then((returnedUid: string) => { + // uid should be returned. + expect(returnedUid).to.be.equal(uid); + // Confirm expected rpc request parameters sent. + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, emptyRequest)); + }); + }); + + it('should be fulfilled given valid parameters', () => { + const expectedResult = utils.responseFrom({ + kind: 'identitytoolkit#SignupNewUserResponse', + localId: uid, + }); + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + // Create a new account with all possible valid data. + return requestHandler.createNewAccount(validData) + .then((returnedUid: string) => { + // uid should be returned. + expect(returnedUid).to.be.equal(uid); + // Confirm expected rpc request parameters sent. + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, method, expectedValidData)); + }); + }); + + it('should be rejected given invalid parameters such as email', () => { + // Expected error when an invalid email is provided. + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL); + const requestHandler = handler.init(mockApp); + // Create new account with invalid email. + return requestHandler.createNewAccount(invalidData) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Expected invalid email error should be thrown. + expect(error).to.deep.include(expectedError); + }); + }); - const stub = sinon.stub(HttpRequestHandler.prototype, 'sendRequest') - .returns(Promise.resolve(expectedResult)); + const noEnrolledFactors: any[] = [[], null]; + noEnrolledFactors.forEach((arg) => { + it(`should be fulfilled given "${JSON.stringify(arg)}" enrolled factors`, () => { + // Successful result server response. + const expectedResult = utils.responseFrom({ + localId: uid, + }); + + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + // Send create new account request with no enrolled factors. + const request: any = { uid, multiFactor: { enrolledFactors: null } }; + return requestHandler.createNewAccount(request) + .then((returnedUid: string) => { + // uid should be returned. + expect(returnedUid).to.be.equal(uid); + // Confirm expected rpc request parameters sent. In this case, no mfa info should + // be sent. + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, method, emptyRequest)); + }); + }); + }); + + const unsupportedSecondFactor = { + secret: 'SECRET', + displayName: 'Google Authenticator on personal phone', + // TOTP is not yet supported. + factorId: 'totp', + }; + const invalidSecondFactorTests: InvalidMultiFactorUpdateTest[] = [ + { + name: 'unsupported second factor uid', + error: new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"uid" is not supported when adding second factors via "createUser()"', + ), + secondFactor: { + uid: 'enrollmentId', + phoneNumber: '+16505557348', + displayName: 'Spouse\'s phone number', + factorId: 'phone', + }, + }, + { + name: 'invalid second factor display name', + error: new FirebaseAuthError( + AuthClientErrorCode.INVALID_DISPLAY_NAME, + 'The second factor "displayName" for "+16505557348" must be a valid string.', + ), + secondFactor: { + phoneNumber: '+16505557348', + displayName: ['Corp phone number'], + factorId: 'phone', + }, + }, + { + 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.'), + secondFactor: { + phoneNumber: 'invalid', + displayName: 'Spouse\'s phone number', + factorId: 'phone', + }, + }, + { + name: 'unsupported second factor enrollment time', + error: new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"enrollmentTime" is not supported when adding second factors via "createUser()"'), + secondFactor: { + phoneNumber: '+16505557348', + displayName: 'Spouse\'s phone number', + factorId: 'phone', + enrollmentTime: new Date().toUTCString(), + }, + }, + { + name: 'invalid second factor type', + error: new FirebaseAuthError( + AuthClientErrorCode.UNSUPPORTED_SECOND_FACTOR, + `Unsupported second factor "${JSON.stringify(unsupportedSecondFactor)}" provided.`), + secondFactor: unsupportedSecondFactor, + }, + ]; + invalidSecondFactorTests.forEach((invalidSecondFactorTest) => { + it(`should be rejected given an ${invalidSecondFactorTest.name}`, () => { + const invalidSecondFactorData = { + uid, + email: 'user@example.com', + emailVerified: true, + password: 'secretpassword', + multiFactor: { + enrolledFactors: [invalidSecondFactorTest.secondFactor], + }, + }; + const requestHandler = handler.init(mockApp); + return requestHandler.createNewAccount(invalidSecondFactorData as any) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Expected error should be thrown. + expect(error).to.deep.include(invalidSecondFactorTest.error); + }); + }); + }); + + it('should be rejected given tenantId in CreateRequest', () => { + // Expected error when a tenantId is provided. + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"tenantId" is an invalid "CreateRequest" property.'); + const validDataWithTenantId = deepCopy(validData); + (validDataWithTenantId as any).tenantId = TENANT_ID; + + const requestHandler = handler.init(mockApp); + // Create new account with tenantId. + return requestHandler.createNewAccount(validDataWithTenantId) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Expected invalid argument error should be thrown. + expect(error).to.deep.include(expectedError); + }); + }); + + it('should be rejected given invalid parameters such as phoneNumber', () => { + // Expected error when an invalid phone number is provided. + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER); + const requestHandler = handler.init(mockApp); + // Create new account with invalid phone number. + return requestHandler.createNewAccount(invalidPhoneNumberData) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Expected invalid phone number error should be thrown. + expect(error).to.deep.include(expectedError); + }); + }); + + it('should be rejected when the backend returns a user exists error', () => { + // Expected error when the uid already exists. + const expectedError = new FirebaseAuthError(AuthClientErrorCode.UID_ALREADY_EXISTS); + const expectedResult = utils.errorFrom({ + error: { + message: 'DUPLICATE_LOCAL_ID', + }, + }); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + // Send create new account request and simulate a backend error that the user + // already exists. + return requestHandler.createNewAccount(validData) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, method, expectedValidData)); + }); + }); + + it('should be rejected when the backend returns an email exists error', () => { + // Expected error when the email already exists. + const expectedError = new FirebaseAuthError(AuthClientErrorCode.EMAIL_ALREADY_EXISTS); + const expectedResult = utils.errorFrom({ + error: { + message: 'EMAIL_EXISTS', + }, + }); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + // Send create new account request and simulate a backend error that the email + // already exists. + return requestHandler.createNewAccount(validData) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, method, expectedValidData)); + }); + }); + + it('should be rejected when the backend returns a generic error', () => { + // Some generic backend error. + const expectedError = FirebaseAuthError.fromServerError('OPERATION_NOT_ALLOWED'); + const expectedResult = utils.errorFrom({ + error: { + message: 'OPERATION_NOT_ALLOWED', + }, + }); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + // Send create new account request with valid data but simulate backend error. + return requestHandler.createNewAccount(validData) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, method, expectedValidData)); + }); + }); + }); + + describe('with no uid specified', () => { + const path = handler.path('v1', '/accounts', 'project_id'); + const method = 'POST'; + const uid = '12345678'; + const validData = { + displayName: 'John Doe', + email: 'user@example.com', + emailVerified: true, + disabled: false, + photoURL: 'http://localhost/1234/photo.png', + password: 'password', + phoneNumber: '+11234567890', + ignoredProperty: 'value', + }; + const expectedValidData = { + displayName: 'John Doe', + email: 'user@example.com', + emailVerified: true, + disabled: false, + photoUrl: 'http://localhost/1234/photo.png', + password: 'password', + phoneNumber: '+11234567890', + }; + const invalidData = { + email: 'user@invalid@', + }; + const invalidPhoneNumberData = { + uid, + phoneNumber: 'invalid', + }; + + it('should be fulfilled given valid parameters', () => { + // signupNewUser successful response. + const expectedResult = utils.responseFrom({ + kind: 'identitytoolkit#SignupNewUserResponse', + localId: uid, + }); + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + // Send request with valid data. + return requestHandler.createNewAccount(validData) + .then((returnedUid: string) => { + // uid should be returned. + expect(returnedUid).to.be.equal(uid); + // Confirm expected rpc request parameters sent. + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, method, expectedValidData)); + }); + }); + + it('should be rejected given invalid parameters such as email', () => { + // Expected error when an invalid email is provided. + const expectedError = + new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL); + const requestHandler = handler.init(mockApp); + // Send create new account request with invalid data. + return requestHandler.createNewAccount(invalidData) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + }); + }); + + it('should be rejected given invalid parameters such as phone number', () => { + // Expected error when an invalid phone number is provided. + const expectedError = + new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER); + const requestHandler = handler.init(mockApp); + // Send create new account request with invalid data. + return requestHandler.createNewAccount(invalidPhoneNumberData) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + }); + }); + + it('should be rejected when the backend returns a generic error', () => { + // Some generic backend error. + const expectedError = FirebaseAuthError.fromServerError('OPERATION_NOT_ALLOWED'); + const expectedResult = utils.errorFrom({ + error: { + message: 'OPERATION_NOT_ALLOWED', + }, + }); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + // Send valid create new account request and simulate backend error. + return requestHandler.createNewAccount(validData) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, method, expectedValidData)); + }); + }); + }); + }); + + describe('getEmailActionLink', () => { + const path = handler.path('v1', '/accounts:sendOobCode', 'project_id'); + const method = 'POST'; + const email = 'user@example.com'; + const newEmail = 'usernew@example.com'; + const actionCodeSettings = { + url: 'https://www.example.com/path/file?a=1&b=2', + handleCodeInApp: true, + iOS: { + bundleId: 'com.example.ios', + }, + android: { + packageName: 'com.example.android', + installApp: true, + minimumVersion: '6', + }, + dynamicLinkDomain: 'custom.page.link', + }; + const expectedActionCodeSettingsRequest = new ActionCodeSettingsBuilder(actionCodeSettings).buildRequest(); + const expectedLink = 'https://custom.page.link?link=' + + encodeURIComponent('https://projectId.firebaseapp.com/__/auth/action?oobCode=CODE') + + '&apn=com.example.android&ibi=com.example.ios'; + const expectedResult = utils.responseFrom({ + email, + oobLink: expectedLink, + }); + + it('should be fulfilled given a valid email', () => { + const requestData = deepExtend({ + requestType: 'PASSWORD_RESET', + email, + returnOobLink: true, + }, expectedActionCodeSettingsRequest); + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.getEmailActionLink('PASSWORD_RESET', email, actionCodeSettings) + .then((oobLink: string) => { + expect(oobLink).to.be.equal(expectedLink); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, requestData)); + }); + }); + + EMAIL_ACTION_REQUEST_TYPES.forEach((requestType) => { + it('should be fulfilled given a valid requestType:' + requestType + ' and ActionCodeSettings', () => { + const requestData = deepExtend({ + requestType, + email, + returnOobLink: true, + ...(requestType === 'VERIFY_AND_CHANGE_EMAIL') && { newEmail }, + }, expectedActionCodeSettingsRequest); + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.getEmailActionLink(requestType, email, actionCodeSettings, + (requestType === 'VERIFY_AND_CHANGE_EMAIL') ? newEmail: undefined) + .then((oobLink: string) => { + expect(oobLink).to.be.equal(expectedLink); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, requestData)); + }); + }); + }); + + EMAIL_ACTION_REQUEST_TYPES.forEach((requestType) => { + if (requestType === 'EMAIL_SIGNIN' || requestType === 'VERIFY_AND_CHANGE_EMAIL') { + return; + } + it('should be fulfilled given requestType:' + requestType + ' and no ActionCodeSettings', () => { + const requestData = { + requestType, + email, + returnOobLink: true, + }; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.getEmailActionLink(requestType, email) + .then((oobLink: string) => { + expect(oobLink).to.be.equal(expectedLink); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, requestData)); + }); + }); + }); + + it('should be fulfilled given a valid requestType: VERIFY_AND_CHANGE_EMAIL and no ActionCodeSettings', () => { + const VERIFY_AND_CHANGE_EMAIL = 'VERIFY_AND_CHANGE_EMAIL'; + const requestData = { + requestType: VERIFY_AND_CHANGE_EMAIL, + email, + returnOobLink: true, + newEmail, + }; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.getEmailActionLink(VERIFY_AND_CHANGE_EMAIL, email, undefined, newEmail) + .then((oobLink: string) => { + expect(oobLink).to.be.equal(expectedLink); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, requestData)); + }); + }); + + it('should be rejected given requestType:EMAIL_SIGNIN and no ActionCodeSettings', () => { + const invalidRequestType = 'EMAIL_SIGNIN'; + const requestHandler = handler.init(mockApp); + + return requestHandler.getEmailActionLink(invalidRequestType, email) + .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); + }); + + it('should be rejected given requestType: VERIFY_AND_CHANGE and no new Email address', () => { + const requestHandler = handler.init(mockApp); + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '`newEmail` is required when `requestType` === \'VERIFY_AND_CHANGE_EMAIL\'', + ) + + return requestHandler.getEmailActionLink('VERIFY_AND_CHANGE_EMAIL', email) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Invalid argument error should be thrown. + expect(error).to.deep.include(expectedError); + }); + }); + + it('should be rejected given an invalid email', () => { + const invalidEmail = 'invalid'; + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL); + + const requestHandler = handler.init(mockApp); + return requestHandler.getEmailActionLink('PASSWORD_RESET', invalidEmail, actionCodeSettings) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Invalid email error should be thrown. + expect(error).to.deep.include(expectedError); + }); + }); + + it('should be rejected given an invalid new email', () => { + const invalidNewEmail = 'invalid'; + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_NEW_EMAIL); + + const requestHandler = handler.init(mockApp); + return requestHandler.getEmailActionLink('VERIFY_AND_CHANGE_EMAIL', email, actionCodeSettings, invalidNewEmail) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Invalid new email error should be thrown. + expect(error).to.deep.include(expectedError); + }); + }); + + it('should be rejected given an invalid request type', () => { + const invalidRequestType = 'invalid'; + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"invalid" is not a supported email action request type.', + ); + + const requestHandler = handler.init(mockApp); + return requestHandler.getEmailActionLink(invalidRequestType, email, actionCodeSettings) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Invalid argument error should be thrown. + expect(error).to.deep.include(expectedError); + }); + }); + + it('should be rejected given an invalid ActionCodeSettings object', () => { + const invalidActionCodeSettings = 'invalid' as any; + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"ActionCodeSettings" must be a non-null object.', + ); + + const requestHandler = handler.init(mockApp); + return requestHandler.getEmailActionLink('EMAIL_SIGNIN', email, invalidActionCodeSettings) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Invalid argument error should be thrown. + expect(error).to.deep.include(expectedError); + }); + }); + + it('should be rejected when the response does not contain a link', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Unable to create the email action link'); + const requestData = deepExtend({ + requestType: 'VERIFY_EMAIL', + email, + returnOobLink: true, + }, expectedActionCodeSettingsRequest); + // Simulate response missing link. + const stub = sinon.stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({ email })); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.getEmailActionLink('VERIFY_EMAIL', email, actionCodeSettings) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, requestData)); + }); + }); + + it('should be rejected when the backend returns an error', () => { + // Backend returned error. + const expectedError = FirebaseAuthError.fromServerError('USER_NOT_FOUND'); + const expectedServerError = utils.errorFrom({ + error: { + message: 'USER_NOT_FOUND', + }, + }); + const requestData = deepExtend({ + requestType: 'VERIFY_EMAIL', + email, + returnOobLink: true, + }, expectedActionCodeSettingsRequest); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.getEmailActionLink('VERIFY_EMAIL', email, actionCodeSettings) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, requestData)); + }); + }); + }); + + describe('getOAuthIdpConfig()', () => { + const providerId = 'oidc.provider'; + const path = handler.path('v2', `/oauthIdpConfigs/${providerId}`, 'project_id'); + const expectedHttpMethod = 'GET'; + const expectedResult = utils.responseFrom({ + name: `projects/project1/oauthIdpConfigs/${providerId}`, + }); + + it('should be fulfilled given a valid provider ID', () => { + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.getOAuthIdpConfig(providerId) + .then((result) => { + expect(result).to.deep.equal(expectedResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, expectedHttpMethod, {})); + }); + }); + + const invalidProviderIds = [ + null, NaN, 0, 1, true, false, '', 'saml.provider', ['oidc.provider'], [], {}, { a: 1 }, _.noop]; + invalidProviderIds.forEach((invalidProviderId) => { + it('should be rejected given an invalid provider ID:' + JSON.stringify(invalidProviderId), () => { + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID); + + const requestHandler = handler.init(mockApp); + return requestHandler.getOAuthIdpConfig(invalidProviderId as any) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + }); + }); + }); + + it('should be rejected given a backend error', () => { + const expectedError = new FirebaseAuthError(AuthClientErrorCode.CONFIGURATION_NOT_FOUND); + const expectedServerError = utils.errorFrom({ + error: { + message: 'CONFIGURATION_NOT_FOUND', + }, + }); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.getOAuthIdpConfig(providerId) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, expectedHttpMethod, {})); + }); + }); + }); + + describe('listOAuthIdpConfigs()', () => { + const path = handler.path('v2', '/oauthIdpConfigs', 'project_id'); + const expectedHttpMethod = 'GET'; + const nextPageToken = 'PAGE_TOKEN'; + const maxResults = 50; + const expectedResult = utils.responseFrom({ + oauthIdpConfigs : [ + { name: 'projects/project1/oauthIdpConfigs/oidc.provider1' }, + { name: 'projects/project1/oauthIdpConfigs/oidc.provider2' }, + ], + nextPageToken: 'NEXT_PAGE_TOKEN', + }); + + it('should be fulfilled given a valid parameters', () => { + const data = { + pageSize: maxResults, + pageToken: nextPageToken, + }; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.listOAuthIdpConfigs(maxResults, nextPageToken) + .then((result) => { + expect(result).to.deep.equal(expectedResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, expectedHttpMethod, data)); + }); + }); + + it('should be fulfilled with empty configuration array when no configurations exist', () => { + const data = { + pageSize: maxResults, + pageToken: nextPageToken, + }; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({})); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.listOAuthIdpConfigs(maxResults, nextPageToken) + .then((result) => { + expect(result).to.deep.equal({ oauthIdpConfigs: [] }); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, expectedHttpMethod, data)); + }); + }); + + it('should be fulfilled given no parameters', () => { + // Default maxResults should be used. + const data = { + pageSize: 100, + }; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.listOAuthIdpConfigs() + .then((result) => { + expect(result).to.deep.equal(expectedResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, expectedHttpMethod, data)); + }); + }); + + 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.', + ); + + const requestHandler = handler.init(mockApp); + return requestHandler.listOAuthIdpConfigs(101, nextPageToken) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + }); + }); + + it('should be rejected given an invalid next page token', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_PAGE_TOKEN, + ); + + const requestHandler = handler.init(mockApp); + return requestHandler.listOAuthIdpConfigs(maxResults, '') + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + }); + }); + + it('should be rejected when the backend returns an error', () => { + const expectedServerError = utils.errorFrom({ + error: { + message: 'INVALID_PAGE_SELECTION', + }, + }); + const expectedError = FirebaseAuthError.fromServerError('INVALID_PAGE_SELECTION'); + const data = { + pageSize: maxResults, + pageToken: nextPageToken, + }; + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.listOAuthIdpConfigs(maxResults, nextPageToken) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, expectedHttpMethod, data)); + }); + }); + }); + + describe('deleteOAuthIdpConfig()', () => { + const providerId = 'oidc.provider'; + const path = handler.path('v2', `/oauthIdpConfigs/${providerId}`, 'project_id'); + const expectedHttpMethod = 'DELETE'; + const expectedResult = utils.responseFrom({}); + + it('should be fulfilled given a valid provider ID', () => { + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.deleteOAuthIdpConfig(providerId) + .then((result) => { + expect(result).to.be.undefined; + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, expectedHttpMethod, {})); + }); + }); + + const invalidProviderIds = [ + null, NaN, 0, 1, true, false, '', 'saml.provider', ['oidc.provider'], [], {}, { a: 1 }, _.noop]; + invalidProviderIds.forEach((invalidProviderId) => { + it('should be rejected given an invalid provider ID:' + JSON.stringify(invalidProviderId), () => { + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID); + + const requestHandler = handler.init(mockApp); + return requestHandler.deleteOAuthIdpConfig(invalidProviderId as any) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + }); + }); + }); + + it('should be rejected given a backend error', () => { + const expectedError = new FirebaseAuthError(AuthClientErrorCode.CONFIGURATION_NOT_FOUND); + const expectedServerError = utils.errorFrom({ + error: { + message: 'CONFIGURATION_NOT_FOUND', + }, + }); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.deleteOAuthIdpConfig(providerId) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, expectedHttpMethod, {})); + }); + }); + }); + + describe('createOAuthIdpConfig', () => { + 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', + enabled: true, + clientId: 'CLIENT_ID', + issuer: 'https://oidc.com/issuer', + }; + const expectedRequest = { + displayName: 'OIDC_DISPLAY_NAME', + enabled: true, + clientId: 'CLIENT_ID', + issuer: 'https://oidc.com/issuer', + }; + 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); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.createOAuthIdpConfig(configOptions) + .then((response) => { + expect(response).to.deep.equal(expectedResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, expectedHttpMethod, expectedRequest)); + }); + }); + + 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, + '"OIDCAuthProviderConfig.issuer" must be a valid URL string.', + ); + const invalidOptions: OIDCAuthProviderConfig = deepCopy(configOptions); + invalidOptions.issuer = 'invalid'; + + const requestHandler = handler.init(mockApp); + return requestHandler.createOAuthIdpConfig(invalidOptions) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + }); + }); + + it('should be rejected when the backend returns a response missing name', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Unable to create new OIDC configuration', + ); + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({})); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.createOAuthIdpConfig(configOptions) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, expectedHttpMethod, expectedRequest)); + }); + }); + + it('should be rejected when the backend returns an error', () => { + const expectedServerError = utils.errorFrom({ + error: { + message: 'INVALID_CONFIG', + }, + }); + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_CONFIG); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.createOAuthIdpConfig(configOptions) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, expectedHttpMethod, expectedRequest)); + }); + }); + }); + + describe('updateOAuthIdpConfig()', () => { + 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, + clientId: 'CLIENT_ID', + issuer: 'https://oidc.com/issuer', + }; + const expectedRequest = { + displayName: 'OIDC_DISPLAY_NAME', + enabled: true, + clientId: 'CLIENT_ID', + issuer: 'https://oidc.com/issuer', + }; + const expectedResult = utils.responseFrom(deepExtend({ + name: `projects/project_id/oauthIdpConfigs/${providerId}`, + }, expectedRequest)); + const expectedPartialResult = utils.responseFrom(deepExtend({ + name: `projects/project_id/oauthIdpConfigs/${providerId}`, + }, { + displayName: 'OIDC_DISPLAY_NAME', + enabled: false, + 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'; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.updateOAuthIdpConfig(providerId, configOptions) + .then((response) => { + expect(response).to.deep.equal(expectedResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(expectedPath, expectedHttpMethod, expectedRequest)); + }); + }); + + 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); + const partialConfigOptions = { + enabled: false, + clientId: 'NEW_CLIENT_ID', + }; + const partialRequest: OIDCUpdateAuthProviderRequest = { + enabled: false, + displayName: undefined, + issuer: undefined, + clientId: 'NEW_CLIENT_ID', + }; + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.updateOAuthIdpConfig(providerId, partialConfigOptions) + .then((response) => { + expect(response).to.deep.equal(expectedPartialResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(expectedPath, expectedHttpMethod, partialRequest)); + }); + }); + + it('should be fulfilled given single parameter to change', () => { + const expectedPath = path + '?updateMask=issuer'; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedPartialResult); + const partialConfigOptions = { + issuer: 'https://oidc.com/issuer2', + }; + const partialRequest: OIDCUpdateAuthProviderRequest = { + clientId: undefined, + displayName: undefined, + enabled: undefined, + issuer: 'https://oidc.com/issuer2', + }; + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.updateOAuthIdpConfig(providerId, partialConfigOptions) + .then((response) => { + expect(response).to.deep.equal(expectedPartialResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(expectedPath, expectedHttpMethod, partialRequest)); + }); + }); + + const invalidProviderIds = [ + null, NaN, 0, 1, true, false, '', 'saml.provider', ['oidc.provider'], [], {}, { a: 1 }, _.noop]; + invalidProviderIds.forEach((invalidProviderId) => { + it('should be rejected given an invalid provider ID:' + JSON.stringify(invalidProviderId), () => { + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID); + + const requestHandler = handler.init(mockApp); + return requestHandler.updateOAuthIdpConfig(invalidProviderId as any, configOptions) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + }); + }); + }); + + it('should be rejected given invalid parameters', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"OIDCAuthProviderConfig.issuer" must be a valid URL string.', + ); + const invalidOptions: OIDCUpdateAuthProviderRequest = deepCopy(configOptions); + invalidOptions.issuer = 'invalid'; + + const requestHandler = handler.init(mockApp); + return requestHandler.updateOAuthIdpConfig(providerId, invalidOptions) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + }); + }); + + it('should be rejected when the backend returns a response missing name', () => { + const expectedPath = path + '?updateMask=enabled,displayName,issuer,clientId'; + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Unable to update OIDC configuration', + ); + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({})); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.updateOAuthIdpConfig(providerId, configOptions) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(expectedPath, expectedHttpMethod, expectedRequest)); + }); + }); + + it('should be rejected when the backend returns an error', () => { + const expectedPath = path + '?updateMask=enabled,displayName,issuer,clientId'; + const expectedServerError = utils.errorFrom({ + error: { + message: 'INVALID_CONFIG', + }, + }); + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_CONFIG); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.updateOAuthIdpConfig(providerId, configOptions) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(expectedPath, expectedHttpMethod, expectedRequest)); + }); + }); + }); + + describe('getInboundSamlConfig()', () => { + const providerId = 'saml.provider'; + const path = handler.path('v2', `/inboundSamlConfigs/${providerId}`, 'project_id'); + + const expectedHttpMethod = 'GET'; + const expectedResult = utils.responseFrom({ + name: `projects/project1/inboundSamlConfigs/${providerId}`, + }); + + it('should be fulfilled given a valid provider ID', () => { + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.getInboundSamlConfig(providerId) + .then((result) => { + expect(result).to.deep.equal(expectedResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, expectedHttpMethod, {})); + }); + }); + + const invalidProviderIds = [ + null, NaN, 0, 1, true, false, '', 'oidc.provider', ['saml.provider'], [], {}, { a: 1 }, _.noop]; + invalidProviderIds.forEach((invalidProviderId) => { + it('should be rejected given an invalid provider ID:' + JSON.stringify(invalidProviderId), () => { + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID); + + const requestHandler = handler.init(mockApp); + return requestHandler.getInboundSamlConfig(invalidProviderId as any) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + }); + }); + }); + + it('should be rejected given a backend error', () => { + const expectedError = new FirebaseAuthError(AuthClientErrorCode.CONFIGURATION_NOT_FOUND); + const expectedServerError = utils.errorFrom({ + error: { + message: 'CONFIGURATION_NOT_FOUND', + }, + }); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.getInboundSamlConfig(providerId) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, expectedHttpMethod, {})); + }); + }); + }); + + describe('listInboundSamlConfigs()', () => { + const path = handler.path('v2', '/inboundSamlConfigs', 'project_id'); + const expectedHttpMethod = 'GET'; + const nextPageToken = 'PAGE_TOKEN'; + const maxResults = 50; + const expectedResult = utils.responseFrom({ + inboundSamlConfigs : [ + { name: 'projects/project1/inboundSamlConfigs/saml.provider1' }, + { name: 'projects/project1/inboundSamlConfigs/saml.provider2' }, + ], + nextPageToken: 'NEXT_PAGE_TOKEN', + }); + + it('should be fulfilled given a valid parameters', () => { + const data = { + pageSize: maxResults, + pageToken: nextPageToken, + }; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); stubs.push(stub); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Send empty create new account request with only a uid provided. - return requestHandler.createNewAccount({uid}) - .then((returnedUid: string) => { - // uid should be returned. - expect(returnedUid).to.be.equal(uid); - // Confirm expected rpc request parameters sent. - expect(stub).to.have.been.calledOnce.and.calledWith( - host, port, path, httpMethod, emptyRequest, expectedHeaders, timeout); + const requestHandler = handler.init(mockApp); + return requestHandler.listInboundSamlConfigs(maxResults, nextPageToken) + .then((result) => { + expect(result).to.deep.equal(expectedResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, expectedHttpMethod, data)); }); }); - it('should be fulfilled given valid parameters', () => { - const expectedResult = { - kind: 'identitytoolkit#SignupNewUserResponse', - localId: uid, + it('should be fulfilled with empty configuration array when no configurations exist', () => { + const data = { + pageSize: maxResults, + pageToken: nextPageToken, }; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({})); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.listInboundSamlConfigs(maxResults, nextPageToken) + .then((result) => { + expect(result).to.deep.equal({ inboundSamlConfigs: [] }); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, expectedHttpMethod, data)); + }); + }); - const stub = sinon.stub(HttpRequestHandler.prototype, 'sendRequest') - .returns(Promise.resolve(expectedResult)); + it('should be fulfilled given no parameters', () => { + // Default maxResults should be used. + const data = { + pageSize: 100, + }; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); stubs.push(stub); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Create a new account with all possible valid data. - return requestHandler.createNewAccount(validData) - .then((returnedUid: string) => { - // uid should be returned. - expect(returnedUid).to.be.equal(uid); - // Confirm expected rpc request parameters sent. - expect(stub).to.have.been.calledOnce.and.calledWith( - host, port, path, httpMethod, expectedValidData, expectedHeaders, timeout); + const requestHandler = handler.init(mockApp); + return requestHandler.listInboundSamlConfigs() + .then((result) => { + expect(result).to.deep.equal(expectedResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, expectedHttpMethod, data)); }); }); - it('should be rejected given invalid parameters such as email', () => { - // Expected error when an invalid email is provided. - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Create new account with invalid email. - return requestHandler.createNewAccount(invalidData) - .then((returnedUid: string) => { + 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.', + ); + + const requestHandler = handler.init(mockApp); + return requestHandler.listInboundSamlConfigs(101, nextPageToken) + .then(() => { 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); }); }); - it('should be rejected given invalid parameters such as phoneNumber', () => { - // Expected error when an invalid phone number is provided. - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Create new account with invalid phone number. - return requestHandler.createNewAccount(invalidPhoneNumberData) - .then((returnedUid: string) => { + it('should be rejected given an invalid next page token', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_PAGE_TOKEN, + ); + + const requestHandler = handler.init(mockApp); + return requestHandler.listInboundSamlConfigs(maxResults, '') + .then(() => { 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); }); }); - it('should be rejected when the backend returns a user exists error', () => { - // Expected error when the uid already exists. - const expectedError = new FirebaseAuthError(AuthClientErrorCode.UID_ALREADY_EXISTS); - const expectedResult = { + it('should be rejected when the backend returns an error', () => { + const expectedServerError = utils.errorFrom({ error: { - message: 'DUPLICATE_LOCAL_ID', + message: 'INVALID_PAGE_SELECTION', }, + }); + const expectedError = FirebaseAuthError.fromServerError('INVALID_PAGE_SELECTION'); + const data = { + pageSize: maxResults, + pageToken: nextPageToken, }; - - const stub = sinon.stub(HttpRequestHandler.prototype, 'sendRequest') - .returns(Promise.resolve(expectedResult)); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); stubs.push(stub); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Send create new account request and simulate a backend error that the user - // already exists. - return requestHandler.createNewAccount(validData) - .then((returnedUid: string) => { + const requestHandler = handler.init(mockApp); + return requestHandler.listInboundSamlConfigs(maxResults, nextPageToken) + .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); - expect(stub).to.have.been.calledOnce.and.calledWith( - host, port, path, httpMethod, expectedValidData, expectedHeaders, timeout); + expect(error).to.deep.include(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, expectedHttpMethod, data)); }); }); + }); - it('should be rejected when the backend returns an email exists error', () => { - // Expected error when the email already exists. - const expectedError = new FirebaseAuthError(AuthClientErrorCode.EMAIL_ALREADY_EXISTS); - const expectedResult = { + describe('deleteInboundSamlConfig()', () => { + const providerId = 'saml.provider'; + const path = handler.path('v2', `/inboundSamlConfigs/${providerId}`, 'project_id'); + const expectedHttpMethod = 'DELETE'; + const expectedResult = utils.responseFrom({}); + + it('should be fulfilled given a valid provider ID', () => { + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.deleteInboundSamlConfig(providerId) + .then((result) => { + expect(result).to.be.undefined; + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, expectedHttpMethod, {})); + }); + }); + + const invalidProviderIds = [ + null, NaN, 0, 1, true, false, '', 'oidc.provider', ['saml.provider'], [], {}, { a: 1 }, _.noop]; + invalidProviderIds.forEach((invalidProviderId) => { + it('should be rejected given an invalid provider ID:' + JSON.stringify(invalidProviderId), () => { + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID); + + const requestHandler = handler.init(mockApp); + return requestHandler.deleteInboundSamlConfig(invalidProviderId as any) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + }); + }); + }); + + it('should be rejected given a backend error', () => { + const expectedError = new FirebaseAuthError(AuthClientErrorCode.CONFIGURATION_NOT_FOUND); + const expectedServerError = utils.errorFrom({ error: { - message: 'EMAIL_EXISTS', + message: 'CONFIGURATION_NOT_FOUND', }, - }; + }); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.deleteInboundSamlConfig(providerId) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, expectedHttpMethod, {})); + }); + }); + }); + + describe('createInboundSamlConfig', () => { + const providerId = 'saml.provider'; + const path = handler.path('v2', `/inboundSamlConfigs?inboundSamlConfigId=${providerId}`, 'project_id'); + const expectedHttpMethod = 'POST'; + const configOptions = { + providerId, + displayName: 'SAML_DISPLAY_NAME', + enabled: true, + idpEntityId: 'IDP_ENTITY_ID', + ssoURL: 'https://example.com/login', + x509Certificates: ['CERT1', 'CERT2'], + rpEntityId: 'RP_ENTITY_ID', + callbackURL: 'https://projectId.firebaseapp.com/__/auth/handler', + enableRequestSigning: true, + }; + const expectedRequest = { + idpConfig: { + idpEntityId: 'IDP_ENTITY_ID', + ssoUrl: 'https://example.com/login', + signRequest: true, + idpCertificates: [ + { x509Certificate: 'CERT1' }, + { x509Certificate: 'CERT2' }, + ], + }, + spConfig: { + spEntityId: 'RP_ENTITY_ID', + callbackUri: 'https://projectId.firebaseapp.com/__/auth/handler', + }, + displayName: 'SAML_DISPLAY_NAME', + enabled: true, + }; + const expectedResult = utils.responseFrom(deepExtend({ + name: 'projects/project1/inboundSamlConfigs/saml.provider', + }, expectedRequest)); - const stub = sinon.stub(HttpRequestHandler.prototype, 'sendRequest') - .returns(Promise.resolve(expectedResult)); + it('should be fulfilled given valid parameters', () => { + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); stubs.push(stub); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Send create new account request and simulate a backend error that the email - // already exists. - return requestHandler.createNewAccount(validData) - .then((returnedUid: string) => { + const requestHandler = handler.init(mockApp); + return requestHandler.createInboundSamlConfig(configOptions) + .then((response) => { + expect(response).to.deep.equal(expectedResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, expectedHttpMethod, expectedRequest)); + }); + }); + + it('should be rejected given invalid parameters', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"SAMLAuthProviderConfig.callbackURL" must be a valid URL string.', + ); + const invalidOptions: SAMLAuthProviderConfig = deepCopy(configOptions); + invalidOptions.callbackURL = 'invalid'; + + const requestHandler = handler.init(mockApp); + return requestHandler.createInboundSamlConfig(invalidOptions) + .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 a response missing name', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Unable to create new SAML configuration', + ); + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({})); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.createInboundSamlConfig(configOptions) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( - host, port, path, httpMethod, expectedValidData, expectedHeaders, timeout); + callParams(path, expectedHttpMethod, expectedRequest)); }); }); - it('should be rejected when the backend returns a generic error', () => { - // Some generic backend error. - const expectedError = FirebaseAuthError.fromServerError('OPERATION_NOT_ALLOWED'); - const expectedResult = { + it('should be rejected when the backend returns an error', () => { + const expectedServerError = utils.errorFrom({ error: { - message: 'OPERATION_NOT_ALLOWED', + message: 'INVALID_CONFIG', }, - }; - - const stub = sinon.stub(HttpRequestHandler.prototype, 'sendRequest') - .returns(Promise.resolve(expectedResult)); + }); + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_CONFIG); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); stubs.push(stub); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Send create new account request with valid data but simulate backend error. - return requestHandler.createNewAccount(validData) - .then((returnedUid: string) => { + const requestHandler = handler.init(mockApp); + return requestHandler.createInboundSamlConfig(configOptions) + .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( - host, port, path, httpMethod, expectedValidData, expectedHeaders, timeout); + callParams(path, expectedHttpMethod, expectedRequest)); }); }); }); - describe('with no uid specified', () => { - const httpMethod = 'POST'; - const host = 'www.googleapis.com'; - const port = 443; - const path = '/identitytoolkit/v3/relyingparty/signupNewUser'; - const timeout = 10000; - const uid = '12345678'; - const validData = { - displayName: 'John Doe', - email: 'user@example.com', - emailVerified: true, - disabled: false, - photoURL: 'http://localhost/1234/photo.png', - password: 'password', - phoneNumber: '+11234567890', - ignoredProperty: 'value', - }; - const expectedValidData = { - displayName: 'John Doe', - email: 'user@example.com', - emailVerified: true, - disabled: false, - photoUrl: 'http://localhost/1234/photo.png', - password: 'password', - phoneNumber: '+11234567890', - }; - const invalidData = { - email: 'user@invalid@', + describe('updateInboundSamlConfig()', () => { + const providerId = 'saml.provider'; + const path = handler.path('v2', `/inboundSamlConfigs/${providerId}`, 'project_id'); + + const expectedHttpMethod = 'PATCH'; + const configOptions = { + idpEntityId: 'IDP_ENTITY_ID', + ssoURL: 'https://example.com/login', + x509Certificates: ['CERT1', 'CERT2'], + rpEntityId: 'RP_ENTITY_ID', + callbackURL: 'https://projectId.firebaseapp.com/__/auth/handler', + enableRequestSigning: true, + enabled: true, + displayName: 'samlProviderName', }; - const invalidPhoneNumberData = { - uid, - phoneNumber: 'invalid', + const expectedRequest = { + idpConfig: { + idpEntityId: 'IDP_ENTITY_ID', + ssoUrl: 'https://example.com/login', + signRequest: true, + idpCertificates: [ + { x509Certificate: 'CERT1' }, + { x509Certificate: 'CERT2' }, + ], + }, + spConfig: { + spEntityId: 'RP_ENTITY_ID', + callbackUri: 'https://projectId.firebaseapp.com/__/auth/handler', + }, + displayName: 'samlProviderName', + enabled: true, }; + const expectedResult = utils.responseFrom(deepExtend({ + name: `projects/project_id/inboundSamlConfigs/${providerId}`, + }, expectedRequest)); + const expectedPartialResult = utils.responseFrom(deepExtend({ + name: `projects/project_id/inboundSamlConfigs/${providerId}`, + }, { + idpConfig: { + idpEntityId: 'IDP_ENTITY_ID', + ssoUrl: 'https://example.com/login2', + signRequest: true, + idpCertificates: [ + { x509Certificate: 'CERT1' }, + { x509Certificate: 'CERT2' }, + ], + }, + spConfig: { + spEntityId: 'RP_ENTITY_ID2', + callbackUri: 'https://projectId.firebaseapp.com/__/auth/handler', + }, + displayName: 'samlProviderName', + enabled: false, + })); + const fullUpadateMask = + 'enabled,displayName,idpConfig.idpEntityId,idpConfig.ssoUrl,' + + 'idpConfig.signRequest,idpConfig.idpCertificates,spConfig.spEntityId,spConfig.callbackUri'; + + it('should be fulfilled given full parameters', () => { + const expectedPath = path + `?updateMask=${fullUpadateMask}`; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); - it('should be fulfilled given valid parameters', () => { - // signupNewUser successful response. - const expectedResult = { - kind: 'identitytoolkit#SignupNewUserResponse', - localId: uid, + const requestHandler = handler.init(mockApp); + return requestHandler.updateInboundSamlConfig(providerId, configOptions) + .then((response) => { + expect(response).to.deep.equal(expectedResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(expectedPath, expectedHttpMethod, expectedRequest)); + }); + }); + + it('should be fulfilled given partial parameters', () => { + const expectedPath = path + '?updateMask=enabled,idpConfig.ssoUrl,spConfig.spEntityId'; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedPartialResult); + const partialConfigOptions = { + ssoURL: 'https://example.com/login2', + rpEntityId: 'RP_ENTITY_ID2', + enabled: false, + }; + const partialRequest: SAMLConfigServerResponse = { + idpConfig: { + idpEntityId: undefined, + ssoUrl: 'https://example.com/login2', + signRequest: undefined, + idpCertificates: undefined, + }, + spConfig: { + spEntityId: 'RP_ENTITY_ID2', + callbackUri: undefined, + }, + displayName: undefined, + enabled: false, }; + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.updateInboundSamlConfig(providerId, partialConfigOptions) + .then((response) => { + expect(response).to.deep.equal(expectedPartialResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(expectedPath, expectedHttpMethod, partialRequest)); + }); + }); - const stub = sinon.stub(HttpRequestHandler.prototype, 'sendRequest') - .returns(Promise.resolve(expectedResult)); + it('should be fulfilled given single parameter to change', () => { + const expectedPath = path + '?updateMask=idpConfig.ssoUrl'; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedPartialResult); + const partialConfigOptions = { + ssoURL: 'https://example.com/login2', + }; + const partialRequest: SAMLConfigServerResponse = { + idpConfig: { + idpEntityId: undefined, + ssoUrl: 'https://example.com/login2', + signRequest: undefined, + idpCertificates: undefined, + }, + displayName: undefined, + enabled: undefined, + }; stubs.push(stub); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Send request with valid data. - return requestHandler.createNewAccount(validData) - .then((returnedUid: string) => { - // uid should be returned. - expect(returnedUid).to.be.equal(uid); - // Confirm expected rpc request parameters sent. + const requestHandler = handler.init(mockApp); + return requestHandler.updateInboundSamlConfig(providerId, partialConfigOptions) + .then((response) => { + expect(response).to.deep.equal(expectedPartialResult.data); expect(stub).to.have.been.calledOnce.and.calledWith( - host, port, path, httpMethod, expectedValidData, expectedHeaders, timeout); + callParams(expectedPath, expectedHttpMethod, partialRequest)); }); }); - it('should be rejected given invalid parameters such as email', () => { - // Expected error when an invalid email is provided. - const expectedError = - new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Send create new account request with invalid data. - return requestHandler.createNewAccount(invalidData) - .then((returnedUid: string) => { + const invalidProviderIds = [ + null, NaN, 0, 1, true, false, '', 'oidc.provider', ['saml.provider'], [], {}, { a: 1 }, _.noop]; + invalidProviderIds.forEach((invalidProviderId) => { + it('should be rejected given an invalid provider ID:' + JSON.stringify(invalidProviderId), () => { + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID); + + const requestHandler = handler.init(mockApp); + return requestHandler.updateInboundSamlConfig(invalidProviderId as any, configOptions) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + }); + }); + }); + + it('should be rejected given invalid parameters', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"SAMLAuthProviderConfig.ssoURL" must be a valid URL string.', + ); + const invalidOptions: SAMLUpdateAuthProviderRequest = deepCopy(configOptions); + invalidOptions.ssoURL = 'invalid'; + + const requestHandler = handler.init(mockApp); + return requestHandler.updateInboundSamlConfig(providerId, invalidOptions) + .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); - it('should be rejected given invalid parameters such as phone number', () => { - // Expected error when an invalid phone number is provided. - const expectedError = - new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Send create new account request with invalid data. - return requestHandler.createNewAccount(invalidPhoneNumberData) - .then((returnedUid: string) => { + it('should be rejected when the backend returns a response missing name', () => { + const expectedPath = path + `?updateMask=${fullUpadateMask}`; + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Unable to update SAML configuration', + ); + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({})); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.updateInboundSamlConfig(providerId, configOptions) + .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)); }); }); - it('should be rejected when the backend returns a generic error', () => { - // Some generic backend error. - const expectedError = FirebaseAuthError.fromServerError('OPERATION_NOT_ALLOWED'); - const expectedResult = { + it('should be rejected when the backend returns an error', () => { + const expectedPath = path + `?updateMask=${fullUpadateMask}`; + const expectedServerError = utils.errorFrom({ error: { - message: 'OPERATION_NOT_ALLOWED', + message: 'INVALID_CONFIG', }, - }; - - const stub = sinon.stub(HttpRequestHandler.prototype, 'sendRequest') - .returns(Promise.resolve(expectedResult)); + }); + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_CONFIG); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); stubs.push(stub); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - // Send valid create new account request and simulate backend error. - return requestHandler.createNewAccount(validData) - .then((returnedUid: string) => { + const requestHandler = handler.init(mockApp); + return requestHandler.updateInboundSamlConfig(providerId, configOptions) + .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( - host, port, path, httpMethod, expectedValidData, expectedHeaders, timeout); + callParams(expectedPath, expectedHttpMethod, expectedRequest)); }); }); }); - }); - describe('non-2xx responses', () => { - it('should be rejected given a simulated non-2xx response with a known error code', () => { - const mockErrorResponse = { - error: { - error: { - message: 'USER_NOT_FOUND', + if (handler.supportsTenantManagement) { + describe('getTenant', () => { + const path = '/v2/projects/project_id/tenants/tenant-id'; + const method = 'GET'; + const tenantId = 'tenant-id'; + const expectedResult = utils.responseFrom({ + name: 'projects/project_id/tenants/tenant-id', + }); + + it('should be fulfilled given a valid tenant ID', () => { + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp) as AuthRequestHandler; + return requestHandler.getTenant(tenantId) + .then((result) => { + expect(result).to.deep.include(expectedResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, {})); + }); + }); + + const invalidTenantIds = [null, NaN, 0, 1, true, false, '', ['tenant-id'], [], {}, { a: 1 }, _.noop]; + invalidTenantIds.forEach((invalidTenantId) => { + it('should be rejected given an invalid tenant ID:' + JSON.stringify(invalidTenantId), () => { + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_TENANT_ID); + + const requestHandler = handler.init(mockApp) as AuthRequestHandler; + return requestHandler.getTenant(invalidTenantId as any) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + }); + }); + }); + + it('should be rejected given a backend error', () => { + const expectedError = new FirebaseAuthError(AuthClientErrorCode.TENANT_NOT_FOUND); + const expectedServerError = utils.errorFrom({ + error: { + message: 'TENANT_NOT_FOUND', + }, + }); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); + stubs.push(stub); + + const requestHandler = handler.init(mockApp) as AuthRequestHandler; + return requestHandler.getTenant(tenantId) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, {})); + }); + }); + }); + + describe('listTenants', () => { + const path = '/v2/projects/project_id/tenants'; + const method = 'GET'; + const nextPageToken = 'PAGE_TOKEN'; + const maxResults = 500; + const expectedResult = utils.responseFrom({ + tenants : [ + { name: 'projects/project_id/tenants/tenant-id1' }, + { name: 'projects/project_id/tenants/tenant-id2' }, + ], + nextPageToken: 'NEXT_PAGE_TOKEN', + }); + + it('should be fulfilled given valid parameters', () => { + const data = { + pageSize: maxResults, + pageToken: nextPageToken, + }; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp) as AuthRequestHandler; + return requestHandler.listTenants(maxResults, nextPageToken) + .then((result) => { + expect(result).to.deep.equal(expectedResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); + }); + }); + + it('should be fulfilled with empty tenant array when no tenants exist', () => { + const data = { + pageSize: maxResults, + pageToken: nextPageToken, + }; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({})); + stubs.push(stub); + + const requestHandler = handler.init(mockApp) as AuthRequestHandler; + return requestHandler.listTenants(maxResults, nextPageToken) + .then((result) => { + expect(result).to.deep.equal({ tenants: [] }); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); + }); + }); + + it('should be fulfilled given no parameters', () => { + // Default maxResults should be used. + const data = { + pageSize: 1000, + }; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp) as AuthRequestHandler; + return requestHandler.listTenants() + .then((result) => { + expect(result).to.deep.equal(expectedResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); + }); + }); + + 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.', + ); + + const requestHandler = handler.init(mockApp) as AuthRequestHandler; + return requestHandler.listTenants(1001, nextPageToken) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + }); + }); + + it('should be rejected given an invalid next page token', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_PAGE_TOKEN, + ); + + const requestHandler = handler.init(mockApp) as AuthRequestHandler; + return requestHandler.listTenants(maxResults, '') + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + }); + }); + + it('should be rejected when the backend returns an error', () => { + const expectedServerError = utils.errorFrom({ + error: { + message: 'INVALID_PAGE_SELECTION', + }, + }); + const expectedError = FirebaseAuthError.fromServerError('INVALID_PAGE_SELECTION'); + const data = { + pageSize: maxResults, + pageToken: nextPageToken, + }; + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); + stubs.push(stub); + + const requestHandler = handler.init(mockApp) as AuthRequestHandler; + return requestHandler.listTenants(maxResults, nextPageToken) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); + }); + }); + }); + + describe('deleteTenant', () => { + const path = '/v2/projects/project_id/tenants/tenant-id'; + const method = 'DELETE'; + const tenantId = 'tenant-id'; + const expectedResult = utils.responseFrom({}); + + it('should be fulfilled given a valid tenant ID', () => { + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp) as AuthRequestHandler; + return requestHandler.deleteTenant(tenantId) + .then((result) => { + expect(result).to.be.undefined; + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, undefined)); + }); + }); + + const invalidTenantIds = [null, NaN, 0, 1, true, false, '', ['tenant-id'], [], {}, { a: 1 }, _.noop]; + invalidTenantIds.forEach((invalidTenantId) => { + it('should be rejected given an invalid tenant ID:' + JSON.stringify(invalidTenantId), () => { + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_TENANT_ID); + + const requestHandler = handler.init(mockApp) as AuthRequestHandler; + return requestHandler.deleteTenant(invalidTenantId as any) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + }); + }); + }); + + it('should be rejected given a backend error', () => { + const expectedError = new FirebaseAuthError(AuthClientErrorCode.TENANT_NOT_FOUND); + const expectedServerError = utils.errorFrom({ + error: { + message: 'TENANT_NOT_FOUND', + }, + }); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); + stubs.push(stub); + + const requestHandler = handler.init(mockApp) as AuthRequestHandler; + return requestHandler.deleteTenant(tenantId) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, undefined)); + }); + }); + }); + + describe('createTenant', () => { + const path = '/v2/projects/project_id/tenants'; + const postMethod = 'POST'; + const tenantOptions: CreateTenantRequest = { + displayName: 'TENANT-DISPLAY-NAME', + emailSignInConfig: { + enabled: true, + passwordRequired: true, }, - }, - statusCode: 400, - }; + 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 valid parameters', () => { + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp) as AuthRequestHandler; + return requestHandler.createTenant(tenantOptions) + .then((actualResult) => { + expect(actualResult).to.be.deep.equal(expectedResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, postMethod, expectedRequest)); + }); + }); + + it('should be rejected given invalid parameters', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"EmailSignInConfig" must be a non-null object.', + ); + const invalidOptions = deepCopy(tenantOptions); + invalidOptions.emailSignInConfig = 'invalid' as any; + + const requestHandler = handler.init(mockApp) as AuthRequestHandler; + return requestHandler.createTenant(invalidOptions) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + }); + }); - const stub = sinon.stub(HttpRequestHandler.prototype, 'sendRequest') - .rejects(mockErrorResponse); - stubs.push(stub); + it('should be rejected when the backend returns a response missing name', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Unable to create new tenant', + ); + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({})); + stubs.push(stub); + + const requestHandler = handler.init(mockApp) as AuthRequestHandler; + return requestHandler.createTenant(tenantOptions) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, postMethod, expectedRequest)); + }); + }); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.getAccountInfoByEmail('user@example.com') - .should.eventually.be.rejected.and.have.property('code', 'auth/user-not-found'); - }); + it('should be rejected when the backend returns a response missing tenant ID in response name', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Unable to create new tenant', + ); + // 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' })); + stubs.push(stub); + + const requestHandler = handler.init(mockApp) as AuthRequestHandler; + return requestHandler.createTenant(tenantOptions) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, postMethod, expectedRequest)); + }); + }); + + it('should be rejected when the backend returns an error', () => { + const expectedServerError = utils.errorFrom({ + error: { + message: 'INTERNAL_ERROR', + }, + }); + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'An internal error has occurred. Raw server response: ' + + `"${JSON.stringify(expectedServerError.response.data)}"`, + ); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); + stubs.push(stub); + + const requestHandler = handler.init(mockApp) as AuthRequestHandler; + return requestHandler.createTenant(tenantOptions) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, postMethod, expectedRequest)); + }); + }); + }); + + describe('updateTenant', () => { + const path = '/v2/projects/project_id/tenants/tenant-id'; + const patchMethod = 'PATCH'; + const tenantId = 'tenant-id'; + const tenantOptions: UpdateTenantRequest = { + 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,' + + 'mfaConfig.state,mfaConfig.enabledProviders,testPhoneNumbers'; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp) as AuthRequestHandler; + return requestHandler.updateTenant(tenantId, tenantOptions) + .then((actualResult) => { + expect(actualResult).to.deep.equal(expectedResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(expectedPath, patchMethod, expectedRequest)); + }); + }); + + it('should be fulfilled given partial parameters', () => { + const expectedPath = path + '?updateMask=allowPasswordSignup'; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + const partialRequest = { + allowPasswordSignup: true, + }; + const partialTenantOptions = { + emailSignInConfig: { enabled: true }, + }; + stubs.push(stub); + + const requestHandler = handler.init(mockApp) as AuthRequestHandler; + return requestHandler.updateTenant(tenantId, partialTenantOptions) + .then((actualResult) => { + expect(actualResult).to.deep.equal(expectedResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(expectedPath, patchMethod, partialRequest)); + }); + }); + + it('should be fulfilled given a single parameter to change', () => { + const expectedPath = path + '?updateMask=displayName'; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + const partialRequest = { + displayName: 'TENANT_DISPLAY_NAME', + }; + const partialTenantOptions = { + displayName: 'TENANT_DISPLAY_NAME', + }; + stubs.push(stub); + + const requestHandler = handler.init(mockApp) as AuthRequestHandler; + return requestHandler.updateTenant(tenantId, partialTenantOptions) + .then((actualResult) => { + expect(actualResult).to.deep.equal(expectedResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(expectedPath, patchMethod, partialRequest)); + }); + }); + + const invalidTenantIds = [null, NaN, 0, 1, true, false, '', ['tenant-id'], [], {}, { a: 1 }, _.noop]; + invalidTenantIds.forEach((invalidTenantId) => { + it('should be rejected given an invalid tenant ID:' + JSON.stringify(invalidTenantId), () => { + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_TENANT_ID); + + const requestHandler = handler.init(mockApp) as AuthRequestHandler; + return requestHandler.updateTenant(invalidTenantId as any, tenantOptions) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + }); + }); + }); + + it('should be rejected given invalid parameters', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"EmailSignInConfig" must be a non-null object.', + ); + const invalidOptions = deepCopy(tenantOptions); + invalidOptions.emailSignInConfig = 'invalid' as any; + + const requestHandler = handler.init(mockApp) as AuthRequestHandler; + return requestHandler.updateTenant(tenantId, invalidOptions) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + }); + }); + + it('should be rejected when the backend returns a response missing name', () => { + 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', + ); + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({})); + stubs.push(stub); + + const requestHandler = handler.init(mockApp) as AuthRequestHandler; + return requestHandler.updateTenant(tenantId, tenantOptions) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(expectedPath, patchMethod, expectedRequest)); + }); + }); - it('should be rejected given a simulated non-2xx response with an unknown error code', () => { - const mockErrorResponse = { - error: { + it('should be rejected when the backend returns a response missing tenant ID in response name', () => { + 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', + ); + // 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' })); + stubs.push(stub); + + const requestHandler = handler.init(mockApp) as AuthRequestHandler; + return requestHandler.updateTenant(tenantId, tenantOptions) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(expectedPath, patchMethod, expectedRequest)); + }); + }); + + it('should be rejected when the backend returns an error', () => { + const expectedPath = path + '?updateMask=allowPasswordSignup,enableEmailLinkSignin,displayName,' + + 'mfaConfig.state,mfaConfig.enabledProviders,testPhoneNumbers'; + const expectedServerError = utils.errorFrom({ + error: { + message: 'INTERNAL_ERROR', + }, + }); + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'An internal error has occurred. Raw server response: ' + + `"${JSON.stringify(expectedServerError.response.data)}"`, + ); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); + stubs.push(stub); + + const requestHandler = handler.init(mockApp) as AuthRequestHandler; + return requestHandler.updateTenant(tenantId, tenantOptions) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + expect(error).to.deep.include(expectedError); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(expectedPath, patchMethod, expectedRequest)); + }); + }); + }); + } + + describe('non-2xx responses', () => { + it('should be rejected given a simulated non-2xx response with a known error code', () => { + const mockErrorResponse = utils.errorFrom({ error: { - message: 'UNKNOWN_ERROR_CODE', + message: 'USER_NOT_FOUND', }, - }, - statusCode: 400, - }; + }, 400); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(mockErrorResponse); + stubs.push(stub); - const stub = sinon.stub(HttpRequestHandler.prototype, 'sendRequest') - .rejects(mockErrorResponse); - stubs.push(stub); + const requestHandler = handler.init(mockApp); + return requestHandler.getAccountInfoByEmail('user@example.com') + .should.eventually.be.rejected.and.have.property('code', 'auth/user-not-found'); + }); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.getAccountInfoByEmail('user@example.com') - .should.eventually.be.rejected.and.have.property('code', 'auth/internal-error'); - }); + it('should be rejected given a simulated non-2xx response with an unknown error code', () => { + const mockErrorResponse = utils.errorFrom({ + error: { + message: 'UNKNOWN_ERROR_CODE', + }, + }, 400); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(mockErrorResponse); + stubs.push(stub); - it('should be rejected given a simulated non-2xx response with no error code', () => { - const mockErrorResponse = { - error: { - foo: 'bar', - }, - statusCode: 400, - }; + const requestHandler = handler.init(mockApp); + return requestHandler.getAccountInfoByEmail('user@example.com') + .should.eventually.be.rejected.and.have.property('code', 'auth/internal-error'); + }); - const stub = sinon.stub(HttpRequestHandler.prototype, 'sendRequest') - .rejects(mockErrorResponse); - stubs.push(stub); + it('should be rejected given a simulated non-2xx response with no error code', () => { + const mockErrorResponse = utils.errorFrom({ + error: { + foo: 'bar', + }, + }, 400); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(mockErrorResponse); + stubs.push(stub); - const requestHandler = new FirebaseAuthRequestHandler(mockApp); - return requestHandler.getAccountInfoByEmail('user@example.com') - .should.eventually.be.rejected.and.have.property('code', 'auth/internal-error'); + const requestHandler = handler.init(mockApp); + return requestHandler.getAccountInfoByEmail('user@example.com') + .should.eventually.be.rejected.and.have.property('code', 'auth/internal-error'); + }); }); }); }); diff --git a/test/unit/auth/auth-config.spec.ts b/test/unit/auth/auth-config.spec.ts new file mode 100644 index 0000000000..8894bdc5ff --- /dev/null +++ b/test/unit/auth/auth-config.spec.ts @@ -0,0 +1,1324 @@ +/*! + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as _ from 'lodash'; +import * as chai from 'chai'; +import * as sinonChai from 'sinon-chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +import { deepCopy } from '../../../src/utils/deep-copy'; +import { + OIDCConfig, SAMLConfig, SAMLConfigServerRequest, + SAMLConfigServerResponse, OIDCConfigServerRequest, + OIDCConfigServerResponse, + EmailSignInConfig, MultiFactorAuthConfig, validateTestPhoneNumbers, + MAXIMUM_TEST_PHONE_NUMBERS, + PasswordPolicyAuthConfig, + CustomStrengthOptionsConfig, +} from '../../../src/auth/auth-config'; +import { + SAMLUpdateAuthProviderRequest, OIDCUpdateAuthProviderRequest, + SAMLAuthProviderConfig, OIDCAuthProviderConfig, +} from '../../../src/auth/index'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +describe('EmailSignInConfig', () => { + describe('constructor', () => { + const validConfig = new EmailSignInConfig({ + allowPasswordSignup: true, + enableEmailLinkSignin: true, + }); + + it('should throw on missing allowPasswordSignup', () => { + expect(() => new EmailSignInConfig({ + enableEmailLinkSignin: false, + })).to.throw('INTERNAL ASSERT FAILED: Invalid email sign-in configuration response'); + }); + + it('should set readonly property "enabled" to true on allowPasswordSignup enabled', () => { + expect(validConfig.enabled).to.be.true; + }); + + it('should set readonly property "enabled" to false on allowPasswordSignup disabled', () => { + const passwordSignupDisabledConfig = new EmailSignInConfig({ + allowPasswordSignup: false, + enableEmailLinkSignin: false, + }); + expect(passwordSignupDisabledConfig.enabled).to.be.false; + }); + + it('should set readonly property "passwordRequired" to false on email link sign in enabled', () => { + expect(validConfig.passwordRequired).to.be.false; + }); + + it('should set readonly property "passwordRequired" to true on email link sign in disabled', () => { + const passwordSignupEnabledConfig = new EmailSignInConfig({ + allowPasswordSignup: true, + enableEmailLinkSignin: false, + }); + expect(passwordSignupEnabledConfig.passwordRequired).to.be.true; + }); + }); + + describe('toJSON()', () => { + it('should return expected JSON representation', () => { + const config = new EmailSignInConfig({ + allowPasswordSignup: true, + enableEmailLinkSignin: true, + }); + expect(config.toJSON()).to.deep.equal({ + enabled: true, + passwordRequired: false, + }); + }); + }); + + describe('buildServerRequest()', () => { + it('should return expected server request on valid input with email link sign-in', () => { + expect(EmailSignInConfig.buildServerRequest({ + enabled: true, + passwordRequired: false, + })).to.deep.equal({ + allowPasswordSignup: true, + enableEmailLinkSignin: true, + }); + }); + + it('should return expected server request on valid input without email link sign-in', () => { + expect(EmailSignInConfig.buildServerRequest({ + enabled: true, + passwordRequired: true, + })).to.deep.equal({ + allowPasswordSignup: true, + enableEmailLinkSignin: false, + }); + }); + + const invalidOptions = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; + invalidOptions.forEach((options) => { + it('should throw on invalid EmailSignInConfig:' + JSON.stringify(options), () => { + expect(() => { + EmailSignInConfig.buildServerRequest(options as any); + }).to.throw('"EmailSignInConfig" must be a non-null object.'); + }); + }); + + it('should throw on EmailSignInConfig with unsupported attribute', () => { + expect(() => { + EmailSignInConfig.buildServerRequest({ + unsupported: true, + enabled: true, + passwordRequired: false, + } as any); + }).to.throw('"unsupported" is not a valid EmailSignInConfig parameter.'); + }); + + const invalidEnabled = [null, NaN, 0, 1, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidEnabled.forEach((enabled) => { + it('should throw on invalid EmailSignInConfig.enabled:' + JSON.stringify(enabled), () => { + expect(() => { + EmailSignInConfig.buildServerRequest({ + enabled, + passwordRequired: false, + } as any); + }).to.throw('"EmailSignInConfig.enabled" must be a boolean.'); + }); + }); + + const invalidPasswordRequired = invalidEnabled; + invalidPasswordRequired.forEach((passwordRequired) => { + it('should throw on invalid EmailSignInConfig.passwordRequired:' + JSON.stringify(passwordRequired), () => { + expect(() => { + EmailSignInConfig.buildServerRequest({ + enabled: true, + passwordRequired, + } as any); + }).to.throw('"EmailSignInConfig.passwordRequired" must be a boolean.'); + }); + }); + }); +}); + +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'], + providerConfigs: [ + { + state: 'ENABLED', + totpProviderConfig: { + adjacentIntervals: 5, + }, + }, + ], + }); + expect(config.toJSON()).to.deep.equal({ + state: 'ENABLED', + factorIds: ['phone'], + providerConfigs: [ + { + state: 'ENABLED', + totpProviderConfig: { + adjacentIntervals: 5, + }, + }, + ], + }); + }); + }); + + 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".`); + }); + }); + + const totpBaseConfig = { + state: 'DISABLED', + providerConfigs: [ + { + state: 'ENABLED', + totpProviderConfig: {}, + }, + ], + } as any; + const expectedTotpConfig = deepCopy(totpBaseConfig); + it('should build server request with TOTP enabled', () => { + expect(MultiFactorAuthConfig.buildServerRequest(totpBaseConfig)).to.deep.equal(expectedTotpConfig); + }); + + it('should build server request with TOTP disabled', () => { + totpBaseConfig.providerConfigs[0].state = 'DISABLED'; + const expectedTotpConfig = deepCopy(totpBaseConfig); + expect(MultiFactorAuthConfig.buildServerRequest(totpBaseConfig)).to.deep.equal(expectedTotpConfig); + }); + + it('should return expected server request on valid TOTP provider config', () => { + totpBaseConfig.providerConfigs[0].totpProviderConfig.adjacentIntervals = 5; + const expectedTotpConfig = deepCopy(totpBaseConfig); + expect(MultiFactorAuthConfig.buildServerRequest(totpBaseConfig)).to.deep.equal(expectedTotpConfig); + }); + + 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 invalidProviderConfigs = [NaN, 0, 1, '', 'a', {}, { a: 1 }, _.noop, true, false,] + invalidProviderConfigs.forEach((config) => { + it('should throw an error for invalid providerConfigs type:' + JSON.stringify(config), () => { + expect(() => { + MultiFactorAuthConfig.buildServerRequest({ + state: 'DISABLED', + providerConfigs: config, + } as any); + }).to.throw('"MultiFactorConfig.providerConfigs" must be an array of valid "MultiFactorProviderConfig."') + }); + }); + it('should throw on providerConfig with unsupported attribute', () => { + expect(() => { + MultiFactorAuthConfig.buildServerRequest({ + state: 'DISABLED', + providerConfigs: [ + { + unsupported: true, + state: 'ENABLED', + totpProviderConfig: {}, + } + ], + } as any); + }).to.throw('"unsupported" is not a valid ProviderConfig parameter.'); + }); + const invalidProviderConfigObjects = [undefined, NaN, 0, 1, 'a', [], true, false,] + invalidProviderConfigObjects.forEach((object) => { + it('should throw an error for invalid providerConfig type:' + JSON.stringify(object), () => { + expect(() => { + MultiFactorAuthConfig.buildServerRequest({ + state: 'DISABLED', + providerConfigs: [ + object + ], + } as any); + }).to.throw(`"${object}" is not a valid "MultiFactorProviderConfig" type.`) + }); + }); + invalidState.forEach((state) => { + it('should throw an error for invalid providerConfig.state type', () => { + expect(() => { + MultiFactorAuthConfig.buildServerRequest({ + state: 'DISABLED', + providerConfigs: [ + { + state, + totpProviderConfig: {}, + }, + ], + } as any); + }).to.throw('"MultiFactorConfig.providerConfigs.state" must be either "ENABLED" or "DISABLED".') + }); + }); + it('should throw on totpProviderConfig with unsupported attribute', () => { + expect(() => { + MultiFactorAuthConfig.buildServerRequest({ + state: 'DISABLED', + providerConfigs: [ + { + state: 'ENABLED', + totpProviderConfig: { + unsupported: true, + }, + } + ], + } as any); + }).to.throw('"unsupported" is not a valid TotpProviderConfig parameter.'); + }); + it('should throw an error for undefined totpProviderConfig', () => { + expect(() => { + MultiFactorAuthConfig.buildServerRequest({ + state: 'DISABLED', + providerConfigs: [ + { + state: 'ENABLED', + }, + ], + } as any); + }).to.throw('"MultiFactorConfig.providerConfigs.totpProviderConfig" must be defined.') + }); + const invalidAdjacentIntervals = [null, NaN, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop, true, false, -1, 11, 1.1] + invalidAdjacentIntervals.forEach((interval) => { + it('should throw an error for invalid adjacentIntervals type: ' + JSON.stringify(interval), () => { + expect(() => { + MultiFactorAuthConfig.buildServerRequest({ + state: 'DISABLED', + providerConfigs: [ + { + state: 'ENABLED', + totpProviderConfig: { + adjacentIntervals: interval, + }, + }, + ], + } as any); + }).to.throw('"MultiFactorConfig.providerConfigs.totpProviderConfig.adjacentIntervals" must' + + ' be a valid number between 0 and 10 (both inclusive).') + }); + }); + }); +}); + +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: { + idpEntityId: 'IDP_ENTITY_ID', + ssoUrl: 'https://example.com/login', + signRequest: true, + idpCertificates: [ + { x509Certificate: 'CERT1' }, + { x509Certificate: 'CERT2' }, + ], + }, + spConfig: { + spEntityId: 'RP_ENTITY_ID', + callbackUri: 'https://projectId.firebaseapp.com/__/auth/handler', + }, + displayName: 'samlProviderName', + enabled: true, + }; + const serverResponse: SAMLConfigServerResponse = { + name: 'projects/project_id/inboundSamlConfigs/saml.provider', + idpConfig: { + idpEntityId: 'IDP_ENTITY_ID', + ssoUrl: 'https://example.com/login', + signRequest: true, + idpCertificates: [ + { x509Certificate: 'CERT1' }, + { x509Certificate: 'CERT2' }, + ], + }, + spConfig: { + spEntityId: 'RP_ENTITY_ID', + callbackUri: 'https://projectId.firebaseapp.com/__/auth/handler', + }, + displayName: 'samlProviderName', + enabled: true, + }; + const clientRequest: SAMLAuthProviderConfig = { + providerId: 'saml.provider', + idpEntityId: 'IDP_ENTITY_ID', + ssoURL: 'https://example.com/login', + x509Certificates: ['CERT1', 'CERT2'], + rpEntityId: 'RP_ENTITY_ID', + callbackURL: 'https://projectId.firebaseapp.com/__/auth/handler', + enabled: true, + displayName: 'samlProviderName', + }; + const config = new SAMLConfig(serverResponse); + + describe('constructor', () => { + it('should not throw on valid response', () => { + expect(() => new SAMLConfig(serverResponse)).not.to.throw(); + }); + + it('should set readonly property providerId', () => { + expect(config.providerId).to.equal('saml.provider'); + }); + + it('should set readonly property rpEntityId', () => { + expect(config.rpEntityId).to.equal('RP_ENTITY_ID'); + }); + + it('should set readonly property callbackURL', () => { + expect(config.callbackURL).to.equal('https://projectId.firebaseapp.com/__/auth/handler'); + }); + + it('should set readonly property idpEntityId', () => { + expect(config.idpEntityId).to.equal('IDP_ENTITY_ID'); + }); + + it('should set readonly property ssoURL', () => { + expect(config.ssoURL).to.equal('https://example.com/login'); + }); + + it('should set readonly property enableRequestSigning', () => { + expect(config.enableRequestSigning).to.be.true; + }); + + it('should set readonly property x509Certificates', () => { + expect(config.x509Certificates).to.deep.equal(['CERT1', 'CERT2']); + }); + + it('should set readonly property displayName', () => { + expect(config.displayName).to.equal('samlProviderName'); + }); + + it('should set readonly property enabled', () => { + expect(config.enabled).to.be.true; + }); + + it('should throw on missing idpConfig', () => { + const invalidResponse = deepCopy(serverResponse); + delete invalidResponse.idpConfig; + expect(() => new SAMLConfig(invalidResponse)) + .to.throw('INTERNAL ASSERT FAILED: Invalid SAML configuration response'); + }); + + it('should throw on missing rpConfig', () => { + const invalidResponse = deepCopy(serverResponse); + delete invalidResponse.spConfig; + expect(() => new SAMLConfig(invalidResponse)) + .to.throw('INTERNAL ASSERT FAILED: Invalid SAML configuration response'); + }); + + it('should throw on invalid provider ID', () => { + const invalidResponse = deepCopy(serverResponse); + invalidResponse.name = 'projects/project_id/inboundSamlConfigs/oidc.provider'; + expect(() => new SAMLConfig(invalidResponse)) + .to.throw('INTERNAL ASSERT FAILED: Invalid SAML configuration response'); + }); + }); + + describe('getProviderIdFromResourceName()', () => { + it('should return the expected provider ID for valid resource', () => { + expect(SAMLConfig.getProviderIdFromResourceName('projects/project1/inboundSamlConfigs/saml.provider')) + .to.be.equal('saml.provider'); + }); + + const invalidResourceNames: string[] = [ + '', 'incorrectsaml.', 'saml.provider', 'saml', 'oidc.provider', + 'projects/project1/prefixinboundSamlConfigs/saml.provider', + 'projects/project1/oauthIdpConfigs/saml.provider']; + invalidResourceNames.forEach((invalidResourceName) => { + it(`should return null for invalid resource name "${invalidResourceName}"`, () => { + expect(SAMLConfig.getProviderIdFromResourceName(invalidResourceName)).to.be.null; + }); + }); + }); + + describe('isProviderId()', () => { + it('should return true on valid SAML provider ID', () => { + expect(SAMLConfig.isProviderId('saml.provider')).to.be.true; + }); + + const invalidProviderIds = [ + null, NaN, 0, 1, true, false, '', 'incorrectsaml.', 'saml', 'oidc.provider', 'other', [], [1, 'a'], + {}, { a: 1 }, _.noop]; + invalidProviderIds.forEach((invalidProviderId) => { + it(`should return false on invalid SAML provider ID "${JSON.stringify(invalidProviderId)}"`, () => { + expect(SAMLConfig.isProviderId(invalidProviderId)).to.be.false; + }); + }); + }); + + describe('toJSON()', () => { + it('should return expected JSON', () => { + expect(config.toJSON()).to.deep.equal({ + enabled: true, + displayName: 'samlProviderName', + providerId: 'saml.provider', + idpEntityId: 'IDP_ENTITY_ID', + ssoURL: 'https://example.com/login', + x509Certificates: ['CERT1', 'CERT2'], + rpEntityId: 'RP_ENTITY_ID', + callbackURL: 'https://projectId.firebaseapp.com/__/auth/handler', + enableRequestSigning: true, + }); + }); + }); + + describe('buildServerRequest()', () => { + it('should return expected server request on valid input', () => { + 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', () => { + const updateRequest: SAMLUpdateAuthProviderRequest = { + idpEntityId: 'IDP_ENTITY_ID', + callbackURL: 'https://projectId.firebaseapp.com/__/auth/handler', + enabled: false, + }; + const updateServerRequest: SAMLConfigServerRequest = { + idpConfig: { + idpEntityId: 'IDP_ENTITY_ID', + ssoUrl: undefined, + idpCertificates: undefined, + signRequest: undefined, + }, + spConfig: { + spEntityId: undefined, + callbackUri: 'https://projectId.firebaseapp.com/__/auth/handler', + }, + displayName: undefined, + enabled: false, + }; + expect(SAMLConfig.buildServerRequest(updateRequest, true)).to.deep.equal(updateServerRequest); + }); + + it('should throw on invalid input', () => { + const invalidClientRequest = deepCopy(clientRequest); + invalidClientRequest.providerId = 'oidc.provider'; + expect(() => SAMLConfig.buildServerRequest(invalidClientRequest)) + .to.throw('"SAMLAuthProviderConfig.providerId" must be a valid non-empty string prefixed with "saml.".'); + }); + + 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)) + .to.be.null; + }); + }); + }); + + describe('validate()', () => { + it('should not throw on valid client request object', () => { + expect(() => SAMLConfig.validate(clientRequest)).not.to.throw(); + }); + + it('should not throw when providerId is missing and not required', () => { + const partialRequest = deepCopy(clientRequest) as any; + delete partialRequest.providerId; + expect(() => SAMLConfig.validate(partialRequest, true)).not.to.throw(); + }); + + it('should not throw when idpEntityId is missing and not required', () => { + const partialRequest = deepCopy(clientRequest) as any; + delete partialRequest.idpEntityId; + expect(() => SAMLConfig.validate(partialRequest, true)).not.to.throw(); + }); + + it('should not throw when ssoURL is missing and not required', () => { + const partialRequest = deepCopy(clientRequest) as any; + delete partialRequest.ssoURL; + expect(() => SAMLConfig.validate(partialRequest, true)).not.to.throw(); + }); + + it('should not throw when rpEntityId is missing and not required', () => { + const partialRequest = deepCopy(clientRequest) as any; + delete partialRequest.rpEntityId; + expect(() => SAMLConfig.validate(partialRequest, true)).not.to.throw(); + }); + + it('should not throw when callbackURL is missing and not required', () => { + const partialRequest = deepCopy(clientRequest) as any; + delete partialRequest.callbackURL; + expect(() => SAMLConfig.validate(partialRequest, true)).not.to.throw(); + }); + + it('should not throw when x509Certificates is missing and not required', () => { + const partialRequest = deepCopy(clientRequest) as any; + delete partialRequest.x509Certificates; + expect(() => SAMLConfig.validate(partialRequest, true)).not.to.throw(); + }); + + const nonObjects = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; + nonObjects.forEach((request) => { + it('should throw on non-null SAMLAuthProviderConfig object:' + JSON.stringify(request), () => { + expect(() => SAMLConfig.validate(request as any)) + .to.throw('"SAMLAuthProviderConfig" must be a valid non-null object.'); + }); + }); + + it('should throw on unsupported attribute', () => { + const invalidClientRequest = deepCopy(clientRequest) as any; + invalidClientRequest.unsupported = 'value'; + expect(() => SAMLConfig.validate(invalidClientRequest)) + .to.throw('"unsupported" is not a valid SAML config parameter.'); + }); + + const invalidProviderIds = [ + null, NaN, 0, 1, true, false, '', 'oidc.provider', 'other', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidProviderIds.forEach((providerId) => { + it('should throw on invalid providerId:' + JSON.stringify(providerId), () => { + const invalidClientRequest = deepCopy(clientRequest) as any; + invalidClientRequest.providerId = providerId; + expect(() => SAMLConfig.validate(invalidClientRequest)) + .to.throw('"SAMLAuthProviderConfig.providerId" must be a valid non-empty string prefixed with "saml.".'); + }); + }); + + const invalidIdpEntityIds = [null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidIdpEntityIds.forEach((idpEntityId) => { + it('should throw on invalid idpEntityId:' + JSON.stringify(idpEntityId), () => { + const invalidClientRequest = deepCopy(clientRequest) as any; + invalidClientRequest.idpEntityId = idpEntityId; + expect(() => SAMLConfig.validate(invalidClientRequest)) + .to.throw('"SAMLAuthProviderConfig.idpEntityId" must be a valid non-empty string.'); + }); + }); + + const ssoURLs = [null, NaN, 0, 1, true, false, '', 'invalid', [], [1, 'a'], {}, { a: 1 }, _.noop]; + ssoURLs.forEach((ssoURL) => { + it('should throw on invalid ssoURL:' + JSON.stringify(ssoURL), () => { + const invalidClientRequest = deepCopy(clientRequest) as any; + invalidClientRequest.ssoURL = ssoURL; + expect(() => SAMLConfig.validate(invalidClientRequest)) + .to.throw('"SAMLAuthProviderConfig.ssoURL" must be a valid URL string.'); + }); + }); + + const invalidRpEntityIds = [null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidRpEntityIds.forEach((rpEntityId) => { + it('should throw on invalid rpEntityId:' + JSON.stringify(rpEntityId), () => { + const invalidClientRequest = deepCopy(clientRequest) as any; + invalidClientRequest.rpEntityId = rpEntityId; + expect(() => SAMLConfig.validate(invalidClientRequest)) + .to.throw('"SAMLAuthProviderConfig.rpEntityId" must be a valid non-empty string.'); + }); + }); + + const callbackURLs = [null, NaN, 0, 1, true, false, '', 'invalid', [], [1, 'a'], {}, { a: 1 }, _.noop]; + callbackURLs.forEach((callbackURL) => { + it('should throw on invalid callbackURL:' + JSON.stringify(callbackURL), () => { + const invalidClientRequest = deepCopy(clientRequest) as any; + invalidClientRequest.callbackURL = callbackURL; + expect(() => SAMLConfig.validate(invalidClientRequest)) + .to.throw('"SAMLAuthProviderConfig.callbackURL" must be a valid URL string.'); + }); + }); + + const x509Certs = [null, NaN, 0, 1, true, false, '', [1, 'a'], [''], 'CERT', {}, { a: 1 }, _.noop]; + x509Certs.forEach((x509Cert) => { + it('should throw on invalid x509Certificates:' + JSON.stringify(x509Cert), () => { + const invalidClientRequest = deepCopy(clientRequest) as any; + invalidClientRequest.x509Certificates = x509Cert; + expect(() => SAMLConfig.validate(invalidClientRequest)) + .to.throw('"SAMLAuthProviderConfig.x509Certificates" must be a valid array of X509 certificate strings.'); + }); + }); + + const invalidRequestSigning = [null, NaN, 0, 1, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidRequestSigning.forEach((enableRequestSigning) => { + it('should throw on invalid enableRequestSigning:' + JSON.stringify(enableRequestSigning), () => { + const invalidClientRequest = deepCopy(clientRequest) as any; + invalidClientRequest.enableRequestSigning = enableRequestSigning; + expect(() => SAMLConfig.validate(invalidClientRequest)) + .to.throw('"SAMLAuthProviderConfig.enableRequestSigning" must be a boolean.'); + }); + }); + + const invalidEnabledOptions = [null, NaN, 0, 1, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidEnabledOptions.forEach((invalidEnabled) => { + it('should throw on invalid enabled:' + JSON.stringify(invalidEnabled), () => { + const invalidClientRequest = deepCopy(clientRequest) as any; + invalidClientRequest.enabled = invalidEnabled; + expect(() => SAMLConfig.validate(invalidClientRequest)) + .to.throw('"SAMLAuthProviderConfig.enabled" must be a boolean.'); + }); + }); + + const invalidDisplayNames = [null, NaN, 0, 1, true, false, [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidDisplayNames.forEach((invalidDisplayName) => { + it('should throw on invalid displayName:' + JSON.stringify(invalidDisplayName), () => { + const invalidClientRequest = deepCopy(clientRequest) as any; + invalidClientRequest.displayName = invalidDisplayName; + expect(() => SAMLConfig.validate(invalidClientRequest)) + .to.throw('"SAMLAuthProviderConfig.displayName" must be a valid string.'); + }); + }); + }); +}); + +describe('OIDCConfig', () => { + const serverRequest: OIDCConfigServerRequest = { + clientId: 'CLIENT_ID', + 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', + clientId: 'CLIENT_ID', + issuer: 'https://oidc.com/issuer', + displayName: 'oidcProviderName', + enabled: true, + clientSecret: 'CLIENT_SECRET', + responseType: { + code: true, + }, + }; + const clientRequest: OIDCAuthProviderConfig = { + providerId: 'oidc.provider', + clientId: 'CLIENT_ID', + issuer: 'https://oidc.com/issuer', + displayName: 'oidcProviderName', + enabled: true, + clientSecret: 'CLIENT_SECRET', + responseType: { + idToken: false, + code: true, + }, + }; + const config = new OIDCConfig(serverResponse); + + describe('constructor', () => { + it('should not throw on valid response', () => { + expect(() => new OIDCConfig(serverResponse)).not.to.throw(); + }); + + it('should set readonly property providerId', () => { + expect(config.providerId).to.equal('oidc.provider'); + }); + + it('should set readonly property clientId', () => { + expect(config.clientId).to.equal('CLIENT_ID'); + }); + + it('should set readonly property issuer', () => { + expect(config.issuer).to.equal('https://oidc.com/issuer'); + }); + + it('should set readonly property displayName', () => { + expect(config.displayName).to.equal('oidcProviderName'); + }); + + it('should set readonly property enabled', () => { + 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; + expect(() => new OIDCConfig(invalidResponse)) + .to.throw('INTERNAL ASSERT FAILED: Invalid OIDC configuration response'); + }); + + it('should throw on missing clientId', () => { + const invalidResponse = deepCopy(serverResponse); + delete invalidResponse.clientId; + expect(() => new OIDCConfig(invalidResponse)) + .to.throw('INTERNAL ASSERT FAILED: Invalid OIDC configuration response'); + }); + + it('should throw on invalid provider ID', () => { + const invalidResponse = deepCopy(serverResponse); + invalidResponse.name = 'projects/project_id/oauthIdpConfigs/saml.provider'; + expect(() => new OIDCConfig(invalidResponse)) + .to.throw('INTERNAL ASSERT FAILED: Invalid OIDC configuration response'); + }); + }); + + describe('getProviderIdFromResourceName()', () => { + it('should return the expected provider ID for valid resource', () => { + expect(OIDCConfig.getProviderIdFromResourceName('projects/project1/oauthIdpConfigs/oidc.provider')) + .to.be.equal('oidc.provider'); + }); + + const invalidResourceNames: string[] = [ + '', 'incorrectsaml.', 'oidc.provider', 'oidc', 'saml.provider', + 'projects/project1/prefixoauthIdpConfigs/oidc.provider', + 'projects/project1/inboundSamlConfigs/oidc.provider']; + invalidResourceNames.forEach((invalidResourceName) => { + it(`should return null for invalid resource name "${invalidResourceName}"`, () => { + expect(OIDCConfig.getProviderIdFromResourceName(invalidResourceName)).to.be.null; + }); + }); + }); + + describe('isProviderId()', () => { + it('should return true on valid OIDC provider ID', () => { + expect(OIDCConfig.isProviderId('oidc.provider')).to.be.true; + }); + + const invalidProviderIds = [ + null, NaN, 0, 1, true, false, '', 'incorrectoidc.', 'oidc', 'saml.provider', 'other', [], [1, 'a'], + {}, { a: 1 }, _.noop]; + invalidProviderIds.forEach((invalidProviderId) => { + it(`should return false on invalid OIDC provider ID "${JSON.stringify(invalidProviderId)}"`, () => { + expect(OIDCConfig.isProviderId(invalidProviderId)).to.be.false; + }); + }); + }); + + describe('toJSON()', () => { + it('should return expected JSON', () => { + expect(config.toJSON()).to.deep.equal({ + enabled: true, + displayName: 'oidcProviderName', + providerId: 'oidc.provider', + issuer: 'https://oidc.com/issuer', + clientId: 'CLIENT_ID', + clientSecret: 'CLIENT_SECRET', + responseType: { + code: true, + }, + }); + }); + }); + + describe('buildServerRequest()', () => { + it('should return expected server request on valid input', () => { + expect(OIDCConfig.buildServerRequest(clientRequest)).to.deep.equal(serverRequest); + }); + + it('should ignore missing fields if not required', () => { + 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); + }); + + it('should throw on invalid input', () => { + const invalidClientRequest = deepCopy(clientRequest); + invalidClientRequest.providerId = 'saml.provider'; + expect(() => OIDCConfig.buildServerRequest(invalidClientRequest)) + .to.throw('"OIDCAuthProviderConfig.providerId" must be a valid non-empty string prefixed with "oidc.".'); + }); + + 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; + }); + }); + }); + + describe('validate()', () => { + it('should not throw on valid client request object', () => { + expect(() => OIDCConfig.validate(clientRequest)).not.to.throw(); + }); + + it('should not throw when providerId is missing and not required', () => { + const partialRequest = deepCopy(clientRequest) as any; + delete partialRequest.providerId; + expect(() => OIDCConfig.validate(partialRequest, true)).not.to.throw(); + }); + + it('should not throw when clientId is missing and not required', () => { + const partialRequest = deepCopy(clientRequest) as any; + delete partialRequest.clientId; + expect(() => OIDCConfig.validate(partialRequest, true)).not.to.throw(); + }); + + it('should not throw when issuer is missing and not required', () => { + const partialRequest = deepCopy(clientRequest) as any; + delete partialRequest.issuer; + 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), () => { + expect(() => OIDCConfig.validate(request as any)) + .to.throw('"OIDCAuthProviderConfig" must be a valid non-null object.'); + }); + }); + + it('should throw on unsupported attribute', () => { + const invalidClientRequest = deepCopy(clientRequest) as any; + invalidClientRequest.unsupported = 'value'; + expect(() => OIDCConfig.validate(invalidClientRequest)) + .to.throw('"unsupported" is not a valid OIDC config parameter.'); + }); + + const invalidProviderIds = [ + null, NaN, 0, 1, true, false, '', 'other', 'saml.provider', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidProviderIds.forEach((providerId) => { + it('should throw on invalid providerId:' + JSON.stringify(providerId), () => { + const invalidClientRequest = deepCopy(clientRequest) as any; + invalidClientRequest.providerId = providerId; + expect(() => OIDCConfig.validate(invalidClientRequest)) + .to.throw('"OIDCAuthProviderConfig.providerId" must be a valid non-empty string prefixed with "oidc.".'); + }); + }); + + const invalicClientIds = [null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalicClientIds.forEach((invalicClientId) => { + it('should throw on invalid clientId:' + JSON.stringify(invalicClientId), () => { + const invalidClientRequest = deepCopy(clientRequest) as any; + invalidClientRequest.clientId = invalicClientId; + expect(() => OIDCConfig.validate(invalidClientRequest)) + .to.throw('"OIDCAuthProviderConfig.clientId" must be a valid non-empty string.'); + }); + }); + + const invalidIssuers = [null, NaN, 0, 1, true, false, '', 'invalid', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidIssuers.forEach((invalidIssuer) => { + it('should throw on invalid issuer:' + JSON.stringify(invalidIssuer), () => { + const invalidClientRequest = deepCopy(clientRequest) as any; + invalidClientRequest.issuer = invalidIssuer; + expect(() => OIDCConfig.validate(invalidClientRequest)) + .to.throw('"OIDCAuthProviderConfig.issuer" must be a valid URL string.'); + }); + }); + + const invalidEnabledOptions = [null, NaN, 0, 1, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidEnabledOptions.forEach((invalidEnabled) => { + it('should throw on invalid enabled:' + JSON.stringify(invalidEnabled), () => { + const invalidClientRequest = deepCopy(clientRequest) as any; + invalidClientRequest.enabled = invalidEnabled; + expect(() => OIDCConfig.validate(invalidClientRequest)) + .to.throw('"OIDCAuthProviderConfig.enabled" must be a boolean.'); + }); + }); + + const invalidDisplayNames = [null, NaN, 0, 1, true, false, [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidDisplayNames.forEach((invalidDisplayName) => { + it('should throw on invalid displayName:' + JSON.stringify(invalidDisplayName), () => { + const invalidClientRequest = deepCopy(clientRequest) as any; + invalidClientRequest.displayName = invalidDisplayName; + expect(() => OIDCConfig.validate(invalidClientRequest)) + .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.'); + }); + }); + }); +}); + +describe('PasswordPolicyAuthConfig',() => { + describe('constructor',() => { + const validConfig = new PasswordPolicyAuthConfig({ + passwordPolicyEnforcementState: 'ENFORCE', + passwordPolicyVersions: [ + { + customStrengthOptions: { + containsNumericCharacter: true, + containsLowercaseCharacter: true, + containsNonAlphanumericCharacter: true, + containsUppercaseCharacter: true, + minPasswordLength: 8, + maxPasswordLength: 30, + }, + }, + ], + forceUpgradeOnSignin: true, + }); + + it('should throw an error on missing state',() => { + expect(() => new PasswordPolicyAuthConfig({ + passwordPolicyVersions: [ + { + customStrengthOptions: {}, + } + ], + } as any)).to.throw('INTERNAL ASSERT FAILED: Invalid password policy configuration response'); + }); + + it('should set readonly property "enforcementState" to ENFORCE on state enforced',() => { + expect(validConfig.enforcementState).to.equal('ENFORCE'); + }); + + it('should set readonly property "enforcementState" to OFF on state disabling',() => { + const offStateConfig=new PasswordPolicyAuthConfig({ + passwordPolicyEnforcementState: 'OFF', + }); + expect(offStateConfig.enforcementState).to.equal('OFF'); + }); + + it('should set readonly property "constraints"',() => { + const expectedConstraints: CustomStrengthOptionsConfig = { + requireUppercase: true, + requireLowercase: true, + requireNonAlphanumeric: true, + requireNumeric: true, + minLength: 8, + maxLength: 30, + } + expect(validConfig.constraints).to.deep.equal(expectedConstraints); + }); + + it('should set readonly property "forceUpgradeOnSignin"',() => { + expect(validConfig.forceUpgradeOnSignin).to.deep.equal(true); + }); + }); + + describe('buildServerRequest()', () => { + it('should return server request with default constraints', () => { + expect(PasswordPolicyAuthConfig.buildServerRequest({ + enforcementState: 'ENFORCE', + constraints: {}, + })).to.deep.equal({ + passwordPolicyEnforcementState: 'ENFORCE', + forceUpgradeOnSignin: false, + passwordPolicyVersions: [ + { + customStrengthOptions: { + containsLowercaseCharacter: false, + containsUppercaseCharacter: false, + containsNumericCharacter: false, + containsNonAlphanumericCharacter: false, + minPasswordLength: 6, + maxPasswordLength: 4096, + } + } + ] + }); + }); + }); +}); diff --git a/test/unit/auth/auth.spec.ts b/test/unit/auth/auth.spec.ts index 3ae442fb13..4924cb5b12 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"); @@ -16,9 +17,9 @@ 'use strict'; +import * as jwt from 'jsonwebtoken'; 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'; @@ -26,14 +27,24 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as utils from '../utils'; import * as mocks from '../../resources/mocks'; -import {Auth, DecodedIdToken} from '../../../src/auth/auth'; -import {UserRecord} from '../../../src/auth/user-record'; -import {FirebaseApp} from '../../../src/firebase-app'; -import {FirebaseTokenGenerator} from '../../../src/auth/token-generator'; -import {FirebaseAuthRequestHandler} from '../../../src/auth/auth-api-request'; -import {AuthClientErrorCode, FirebaseAuthError} from '../../../src/utils/error'; +import { FirebaseApp } from '../../../src/app/firebase-app'; +import { + AuthRequestHandler, TenantAwareAuthRequestHandler, AbstractAuthRequestHandler, +} from '../../../src/auth/auth-api-request'; +import { AuthClientErrorCode, FirebaseAuthError } from '../../../src/utils/error'; import * as validator from '../../../src/utils/validator'; +import { DecodedAuthBlockingToken, FirebaseTokenVerifier } from '../../../src/auth/token-verifier'; +import { + OIDCConfig, SAMLConfig, OIDCConfigServerResponse, SAMLConfigServerResponse, +} from '../../../src/auth/auth-config'; +import { deepCopy } from '../../../src/utils/deep-copy'; +import { ServiceAccountCredential } from '../../../src/app/credential-internal'; +import { HttpClient } from '../../../src/utils/api-request'; +import { + Auth, TenantAwareAuth, BaseAuth, UserRecord, DecodedIdToken, + UpdateRequest, AuthProviderConfigFilter, TenantManager, +} from '../../../src/auth/index'; chai.should(); chai.use(sinonChai); @@ -42,12 +53,29 @@ chai.use(chaiAsPromised); const expect = chai.expect; +interface AuthTest { + name: string; + supportsTenantManagement: boolean; + Auth: new (...args: any[]) => BaseAuth; + RequestHandler: new (...args: any[]) => AbstractAuthRequestHandler; + init(app: FirebaseApp): BaseAuth; +} + + +interface EmailActionTest { + api: string; + requestType: string; + requiresSettings: boolean; +} + + /** + * @param {string=} tenantId The optional tenant Id. * @return {object} A sample valid server response as returned from getAccountInfo * endpoint. */ -function getValidGetAccountInfoResponse() { - const userResponse: object = { +function getValidGetAccountInfoResponse(tenantId?: string): {kind: string; users: any[]} { + const userResponse: any = { localId: 'abcdefghijklmnopqrstuvwxyz', email: 'user@gmail.com', emailVerified: true, @@ -76,11 +104,26 @@ function getValidGetAccountInfoResponse() { rawId: '+11234567890', }, ], + mfaInfo: [ + { + mfaEnrollmentId: 'enrolledSecondFactor1', + phoneInfo: '+16505557348', + displayName: 'Spouse\'s phone number', + enrolledAt: new Date().toISOString(), + }, + { + mfaEnrollmentId: 'enrolledSecondFactor2', + phoneInfo: '+16505551000', + }, + ], photoUrl: 'https://lh3.googleusercontent.com/1234567890/photo.jpg', validSince: '1476136676', lastLoginAt: '1476235905000', createdAt: '1476136676000', }; + if (typeof tenantId !== 'undefined') { + userResponse.tenantId = tenantId; + } return { kind: 'identitytoolkit#GetAccountInfoResponse', users: [userResponse], @@ -93,7 +136,7 @@ function getValidGetAccountInfoResponse() { * @param {any} serverResponse Raw getAccountInfo response. * @return {Object} The corresponding user record. */ -function getValidUserRecord(serverResponse: any) { +function getValidUserRecord(serverResponse: any): UserRecord { return new UserRecord(serverResponse.users[0]); } @@ -103,9 +146,10 @@ function getValidUserRecord(serverResponse: any) { * * @param {string} uid The uid corresponding to the ID token. * @param {Date} authTime The authentication time of the ID token. + * @param {string=} tenantId The optional tenant ID. * @return {DecodedIdToken} The generated decoded ID token. */ -function getDecodedIdToken(uid: string, authTime: Date): DecodedIdToken { +function getDecodedIdToken(uid: string, authTime: Date, tenantId?: string): DecodedIdToken { return { iss: 'https://securetoken.google.com/project123456789', aud: 'project123456789', @@ -116,1222 +160,3818 @@ function getDecodedIdToken(uid: string, authTime: Date): DecodedIdToken { firebase: { identities: {}, sign_in_provider: 'custom', + tenant: tenantId, }, + uid, }; } +/** + * Generates a mock decoded Auth Blocking token with the provided parameters. + * + * @param uid The uid corresponding to the Auth Blocking token. + * @param authTime The authentication time of the Auth Blocking token. + * @return The generated decoded Auth Blocking token. + */ +function getDecodedAuthBlockingToken(uid: string, authTime: Date): DecodedAuthBlockingToken { + return { + iss: 'https://securetoken.google.com/project123456789', + aud: 'https://us-central1-project123456789.cloudfunctions.net/functionName', + auth_time: Math.floor(authTime.getTime() / 1000), + sub: uid, + iat: Math.floor(authTime.getTime() / 1000), + exp: Math.floor(authTime.getTime() / 1000 + 3600), + uid, + event_id: 'abcdefgh', + event_type: 'beforeCreate', + ip_address: '1234556', + }; +} -describe('Auth', () => { - let auth: Auth; - let mockApp: FirebaseApp; - let oldProcessEnv: NodeJS.ProcessEnv; - let nullAccessTokenAuth: Auth; - let malformedAccessTokenAuth: Auth; - let rejectedPromiseAccessTokenAuth: Auth; - - before(() => utils.mockFetchAccessTokenRequests()); - after(() => nock.cleanAll()); +/** + * Generates a mock decoded session cookie with the provided parameters. + * + * @param {string} uid The uid corresponding to the session cookie. + * @param {Date} authTime The authentication time of the session cookie. + * @param {string=} tenantId The optional tenant ID. + * @return {DecodedIdToken} The generated decoded session cookie. + */ +function getDecodedSessionCookie(uid: string, authTime: Date, tenantId?: string): DecodedIdToken { + return { + iss: 'https://session.firebase.google.com/project123456789', + aud: 'project123456789', + auth_time: Math.floor(authTime.getTime() / 1000), + sub: uid, + iat: Math.floor(authTime.getTime() / 1000), + exp: Math.floor(authTime.getTime() / 1000 + 3600), + firebase: { + identities: {}, + sign_in_provider: 'custom', + tenant: tenantId, + }, + uid, + }; +} - beforeEach(() => { - mockApp = mocks.app(); - auth = new Auth(mockApp); - nullAccessTokenAuth = new Auth(mocks.appReturningNullAccessToken()); - malformedAccessTokenAuth = new Auth(mocks.appReturningMalformedAccessToken()); - rejectedPromiseAccessTokenAuth = new Auth(mocks.appRejectedWhileFetchingAccessToken()); +/** + * Generates a mock OIDC config server response for the corresponding provider ID. + * + * @param {string} providerId The provider ID whose sample OIDCConfigServerResponse is to be returned. + * @return {OIDCConfigServerResponse} The corresponding sample OIDCConfigServerResponse. + */ +function getOIDCConfigServerResponse(providerId: string): OIDCConfigServerResponse { + return { + name: `projects/project_id/oauthIdpConfigs/${providerId}`, + displayName: 'OIDC_DISPLAY_NAME', + enabled: true, + clientId: 'CLIENT_ID', + issuer: 'https://oidc.com/issuer', + }; +} - oldProcessEnv = process.env; - }); +/** + * Generates a mock SAML config server response for the corresponding provider ID. + * + * @param {string} providerId The provider ID whose sample SAMLConfigServerResponse is to be returned. + * @return {SAMLConfigServerResponse} The corresponding sample SAMLConfigServerResponse. + */ +function getSAMLConfigServerResponse(providerId: string): SAMLConfigServerResponse { + return { + name: `projects/project_id/inboundSamlConfigs/${providerId}`, + idpConfig: { + idpEntityId: 'IDP_ENTITY_ID', + ssoUrl: 'https://example.com/login', + signRequest: true, + idpCertificates: [ + { x509Certificate: 'CERT1' }, + { x509Certificate: 'CERT2' }, + ], + }, + spConfig: { + spEntityId: 'RP_ENTITY_ID', + callbackUri: 'https://projectId.firebaseapp.com/__/auth/handler', + }, + displayName: 'SAML_DISPLAY_NAME', + enabled: true, + }; +} - afterEach(() => { - process.env = oldProcessEnv; - return mockApp.delete(); - }); +const INVALID_PROVIDER_IDS = [ + undefined, null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1 }, _.noop]; +const TENANT_ID = 'tenantId'; +const AUTH_CONFIGS: AuthTest[] = [ + { + name: 'Auth', + Auth, + supportsTenantManagement: true, + RequestHandler: AuthRequestHandler, + init: (app: FirebaseApp) => { + return new Auth(app); + }, + }, + { + name: 'TenantAwareAuth', + Auth: TenantAwareAuth, + supportsTenantManagement: false, + RequestHandler: TenantAwareAuthRequestHandler, + init: (app: FirebaseApp) => { + return new TenantAwareAuth(app, TENANT_ID); + }, + }, +]; +AUTH_CONFIGS.forEach((testConfig) => { + describe(testConfig.name, () => { + let auth: BaseAuth; + let mockApp: FirebaseApp; + let getTokenStub: sinon.SinonStub; + let oldProcessEnv: NodeJS.ProcessEnv; + let nullAccessTokenAuth: BaseAuth; + let malformedAccessTokenAuth: BaseAuth; + let rejectedPromiseAccessTokenAuth: BaseAuth; - 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 authAny: any = Auth; - return new authAny(invalidApp); - }).to.throw('First argument passed to admin.auth() must be a valid Firebase app instance.'); - }); - }); + beforeEach(() => { + mockApp = mocks.app(); + getTokenStub = utils.stubGetAccessToken(undefined, mockApp); + auth = testConfig.init(mockApp); - it('should throw given no app', () => { - expect(() => { - const authAny: any = Auth; - return new authAny(); - }).to.throw('First argument passed to admin.auth() must be a valid Firebase app instance.'); - }); + nullAccessTokenAuth = testConfig.init(mocks.appReturningNullAccessToken()); + malformedAccessTokenAuth = testConfig.init(mocks.appReturningMalformedAccessToken()); + rejectedPromiseAccessTokenAuth = testConfig.init(mocks.appRejectedWhileFetchingAccessToken()); - it('should not throw given a valid app', () => { - expect(() => { - return new Auth(mockApp); - }).not.to.throw(); + oldProcessEnv = process.env; + // Project ID not set in the environment. + delete process.env.GOOGLE_CLOUD_PROJECT; + delete process.env.GCLOUD_PROJECT; }); - }); - describe('app', () => { - it('returns the app from the constructor', () => { - // We expect referential equality here - expect(auth.app).to.equal(mockApp); - }); + afterEach(() => { + getTokenStub.restore(); + process.env = oldProcessEnv; + return mockApp.delete(); + }); + + if (testConfig.Auth === Auth) { + // Run tests for Auth. + 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 authAny: any = Auth; + return new authAny(invalidApp); + }).to.throw('First argument passed to admin.auth() must be a valid Firebase app instance.'); + }); + }); - it('is read-only', () => { - expect(() => { - (auth as any).app = mockApp; - }).to.throw('Cannot set property app of # which has only a getter'); - }); - }); + it('should throw given no app', () => { + expect(() => { + const authAny: any = Auth; + return new authAny(); + }).to.throw('First argument passed to admin.auth() must be a valid Firebase app instance.'); + }); - describe('createCustomToken()', () => { - let spy: sinon.SinonSpy; - beforeEach(() => { - spy = sinon.spy(FirebaseTokenGenerator.prototype, 'createCustomToken'); - }); + it('should reject given no project ID', () => { + const authWithoutProjectId = new Auth(mocks.mockCredentialApp()); + authWithoutProjectId.getUser('uid') + .should.eventually.be.rejectedWith( + 'Failed to determine project ID for Auth. Initialize the SDK with service ' + + 'account credentials or set project ID as an app option. Alternatively set the ' + + 'GOOGLE_CLOUD_PROJECT environment variable.'); + }); - afterEach(() => { - spy.restore(); - }); + it('should not throw given a valid app', () => { + expect(() => { + return new Auth(mockApp); + }).not.to.throw(); + }); + }); - it('should throw if a cert credential is not specified', () => { - const mockCredentialAuth = new Auth(mocks.mockCredentialApp()); + describe('app', () => { + it('returns the app from the constructor', () => { + // We expect referential equality here + expect((auth as Auth).app).to.equal(mockApp); + }); - expect(() => { - mockCredentialAuth.createCustomToken(mocks.uid, mocks.developerClaims); - }).to.throw('Must initialize app with a cert credential'); - }); + it('is read-only', () => { + expect(() => { + (auth as any).app = mockApp; + }).to.throw('Cannot set property app of # which has only a getter'); + }); + }); - it('should forward on the call to the token generator\'s createCustomToken() method', () => { - return auth.createCustomToken(mocks.uid, mocks.developerClaims) - .then(() => { - expect(spy) - .to.have.been.calledOnce - .and.calledWith(mocks.uid, mocks.developerClaims); + describe('tenantManager()', () => { + it('should return a TenantManager with the expected attributes', () => { + const tenantManager1 = (auth as Auth).tenantManager(); + const tenantManager2 = new TenantManager(mockApp); + expect(tenantManager1).to.deep.equal(tenantManager2); }); - }); - it('should be fulfilled given an app which returns null access tokens', () => { - // createCustomToken() does not rely on an access token and therefore works in this scenario. - return nullAccessTokenAuth.createCustomToken(mocks.uid, mocks.developerClaims) - .should.eventually.be.fulfilled; - }); + it('should return the same cached instance', () => { + const tenantManager1 = (auth as Auth).tenantManager(); + const tenantManager2 = (auth as Auth).tenantManager(); + expect(tenantManager1).to.equal(tenantManager2); + }); + }); + } - it('should be fulfilled given an app which returns invalid access tokens', () => { - // createCustomToken() does not rely on an access token and therefore works in this scenario. - return malformedAccessTokenAuth.createCustomToken(mocks.uid, mocks.developerClaims) - .should.eventually.be.fulfilled; - }); + describe('createCustomToken()', () => { + it('should return a jwt', async () => { + const token = await auth.createCustomToken('uid1'); + const decodedToken = jwt.decode(token, { complete: true }); + expect(decodedToken).to.have.property('header').that.has.property('typ', 'JWT'); + }); - it('should be fulfilled given an app which fails to generate access tokens', () => { - // createCustomToken() does not rely on an access token and therefore works in this scenario. - return rejectedPromiseAccessTokenAuth.createCustomToken(mocks.uid, mocks.developerClaims) - .should.eventually.be.fulfilled; - }); - }); + if (testConfig.Auth === TenantAwareAuth) { + it('should contain tenant_id', async () => { + const token = await auth.createCustomToken('uid1'); + expect(jwt.decode(token)).to.have.property('tenant_id', TENANT_ID); + }); + } else { + it('should not contain tenant_id', async () => { + const token = await auth.createCustomToken('uid1'); + expect(jwt.decode(token)).to.not.have.property('tenant_id'); + }); + } + + it('should be eventually rejected if a cert credential is not specified', () => { + const mockCredentialAuth = testConfig.init(mocks.mockCredentialApp()); + // Force the service account ID discovery to fail. + getTokenStub = sinon.stub(HttpClient.prototype, 'send').rejects(utils.errorFrom({})); + return mockCredentialAuth.createCustomToken(mocks.uid, mocks.developerClaims) + .should.eventually.be.rejected.and.have.property('code', 'auth/invalid-credential'); + }); - describe('verifyIdToken()', () => { - let stub: sinon.SinonStub; - let mockIdToken: string; - const expectedUserRecord = getValidUserRecord(getValidGetAccountInfoResponse()); - // Set auth_time of token to expected user's tokensValidAfterTime. - const validSince = new Date(expectedUserRecord.tokensValidAfterTime); - // Set expected uid to expected user's. - const uid = expectedUserRecord.uid; - // Set expected decoded ID token with expected UID and auth time. - const decodedIdToken = getDecodedIdToken(uid, validSince); - let clock; - - // Stubs used to simulate underlying api calls. - const stubs: sinon.SinonStub[] = []; - beforeEach(() => { - stub = sinon.stub(FirebaseTokenGenerator.prototype, 'verifyIdToken') - .returns(Promise.resolve(decodedIdToken)); - stubs.push(stub); - mockIdToken = mocks.generateIdToken(); - clock = sinon.useFakeTimers(validSince.getTime()); - }); - afterEach(() => { - _.forEach(stubs, (s) => s.restore()); - clock.restore(); - }); + it('should be fulfilled given an app which returns null access tokens', () => { + 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 throw if a cert credential is not specified', () => { - const mockCredentialAuth = new Auth(mocks.mockCredentialApp()); + it('should be fulfilled given an app which returns invalid access tokens', () => { + 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; + }); - expect(() => { - mockCredentialAuth.verifyIdToken(mockIdToken); - }).to.throw('Must initialize app with a cert credential'); + it('should be fulfilled given an app which fails to generate access tokens', () => { + getTokenStub = sinon.stub(ServiceAccountCredential.prototype, 'getAccessToken').rejects('error'); + // 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 forward on the call to the token generator\'s verifyIdToken() method', () => { - // Stub getUser call. - const getUserStub = sinon.stub(Auth.prototype, 'getUser'); - stubs.push(getUserStub); - return auth.verifyIdToken(mockIdToken).then((result) => { - // Confirm getUser never called. - expect(getUserStub).not.to.have.been.called; - expect(result).to.deep.equal(decodedIdToken); - expect(stub).to.have.been.calledOnce.and.calledWith(mockIdToken); + it('verifyIdToken() should reject when project ID is not specified', () => { + const mockCredentialAuth = testConfig.init(mocks.mockCredentialApp()); + const expected = 'Must initialize app with a cert credential or set your Firebase project ID ' + + 'as the GOOGLE_CLOUD_PROJECT environment variable to call verifyIdToken().'; + return mockCredentialAuth.verifyIdToken(mocks.generateIdToken()) + .should.eventually.be.rejectedWith(expected); + }); + + it('verifySessionCookie() should reject when project ID is not specified', () => { + const mockCredentialAuth = testConfig.init(mocks.mockCredentialApp()); + const expected = 'Must initialize app with a cert credential or set your Firebase project ID ' + + 'as the GOOGLE_CLOUD_PROJECT environment variable to call verifySessionCookie().'; + return mockCredentialAuth.verifySessionCookie(mocks.generateSessionCookie()) + .should.eventually.be.rejectedWith(expected); + }); + + it('_verifyAuthBlockingToken() should reject when project ID is not specified', () => { + const mockCredentialAuth = testConfig.init(mocks.mockCredentialApp()); + const expected = 'Must initialize app with a cert credential or set your Firebase project ID ' + + 'as the GOOGLE_CLOUD_PROJECT environment variable to call _verifyAuthBlockingToken().'; + return mockCredentialAuth._verifyAuthBlockingToken(mocks.generateAuthBlockingToken()) + .should.eventually.be.rejectedWith(expected); + }); + + describe('verifyIdToken()', () => { + let stub: sinon.SinonStub; + let mockIdToken: string; + const tenantId = testConfig.supportsTenantManagement ? undefined : TENANT_ID; + const expectedUserRecord = getValidUserRecord(getValidGetAccountInfoResponse(tenantId)); + // Set auth_time of token to expected user's tokensValidAfterTime. + expect( + expectedUserRecord.tokensValidAfterTime, + "getValidUserRecord didn't properly set tokensValueAfterTime", + ).to.exist; + const validSince = new Date(expectedUserRecord.tokensValidAfterTime!); + // Set expected uid to expected user's. + const uid = expectedUserRecord.uid; + // Set expected decoded ID token with expected UID and auth time. + const decodedIdToken = getDecodedIdToken(uid, validSince, tenantId); + let clock: sinon.SinonFakeTimers; + + // Stubs used to simulate underlying api calls. + const stubs: sinon.SinonStub[] = []; + beforeEach(() => { + stub = sinon.stub(FirebaseTokenVerifier.prototype, 'verifyJWT') + .resolves(decodedIdToken); + stubs.push(stub); + mockIdToken = mocks.generateIdToken(); + clock = sinon.useFakeTimers(validSince.getTime()); + }); + afterEach(() => { + _.forEach(stubs, (s) => s.restore()); + clock.restore(); }); - }); - it('should work with a non-cert credential when the GCLOUD_PROJECT environment variable is present', () => { - process.env.GCLOUD_PROJECT = mocks.projectId; + it('should forward on the call to the token generator\'s verifyIdToken() method', () => { + // Stub getUser call. + const getUserStub = sinon.stub(testConfig.Auth.prototype, 'getUser'); + stubs.push(getUserStub); + return auth.verifyIdToken(mockIdToken).then((result) => { + // Confirm getUser never called. + expect(getUserStub).not.to.have.been.called; + expect(result).to.deep.equal(decodedIdToken); + expect(stub).to.have.been.calledOnce.and.calledWith(mockIdToken); + }); + }); - const mockCredentialAuth = new Auth(mocks.mockCredentialApp()); + it('should reject when underlying idTokenVerifier.verifyJWT() rejects with expected error', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, 'Decoding Firebase ID token failed'); + // Restore verifyIdToken stub. + stub.restore(); + // Simulate ID token is invalid. + stub = sinon.stub(FirebaseTokenVerifier.prototype, 'verifyJWT') + .rejects(expectedError); + stubs.push(stub); + return auth.verifyIdToken(mockIdToken) + .should.eventually.be.rejectedWith('Decoding Firebase ID token failed'); + }); - return mockCredentialAuth.verifyIdToken(mockIdToken).then(() => { - expect(stub).to.have.been.calledOnce.and.calledWith(mockIdToken); + it('should be rejected with checkRevoked set to true and corresponding user disabled', () => { + const expectedAccountInfoResponse = getValidGetAccountInfoResponse(tenantId); + expectedAccountInfoResponse.users[0].disabled = true; + const expectedUserRecordDisabled = getValidUserRecord(expectedAccountInfoResponse); + const getUserStub = sinon.stub(testConfig.Auth.prototype, 'getUser') + .resolves(expectedUserRecordDisabled); + expect(expectedUserRecordDisabled.disabled).to.be.equal(true); + stubs.push(getUserStub); + return auth.verifyIdToken(mockIdToken, true) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(getUserStub).to.have.been.calledOnce.and.calledWith(uid); + // Confirm expected error returned. + expect(error).to.have.property('code', 'auth/user-disabled'); + }); }); - }); - it('should be fulfilled given an app which returns null access tokens', () => { - // verifyIdToken() does not rely on an access token and therefore works in this scenario. - return nullAccessTokenAuth.verifyIdToken(mockIdToken) - .should.eventually.be.fulfilled; - }); + it('verifyIdToken() should reject user disabled before ID tokens revoked', () => { + const expectedAccountInfoResponse = getValidGetAccountInfoResponse(tenantId); + const expectedAccountInfoResponseUserDisabled = Object.assign({}, expectedAccountInfoResponse); + expectedAccountInfoResponseUserDisabled.users[0].disabled = true; + const expectedUserRecordDisabled = getValidUserRecord(expectedAccountInfoResponseUserDisabled); + const validSince = new Date(expectedUserRecordDisabled.tokensValidAfterTime!); + // Restore verifyIdToken stub. + stub.restore(); + // One second before validSince. + const oneSecBeforeValidSince = new Date(validSince.getTime() - 1000); + stub = sinon.stub(FirebaseTokenVerifier.prototype, 'verifyJWT') + .resolves(getDecodedIdToken(expectedUserRecordDisabled.uid, oneSecBeforeValidSince)); + stubs.push(stub); + const getUserStub = sinon.stub(testConfig.Auth.prototype, 'getUser') + .resolves(expectedUserRecordDisabled); + expect(expectedUserRecordDisabled.disabled).to.be.equal(true); + stubs.push(getUserStub); + return auth.verifyIdToken(mockIdToken, true) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm expected error returned. + expect(error).to.have.property('code', 'auth/user-disabled'); + // Confirm underlying API called with expected parameters. + expect(getUserStub).to.have.been.calledOnce.and.calledWith(expectedUserRecordDisabled.uid); + }); + }); - it('should be fulfilled given an app which returns invalid access tokens', () => { - // verifyIdToken() does not rely on an access token and therefore works in this scenario. - return malformedAccessTokenAuth.verifyIdToken(mockIdToken) - .should.eventually.be.fulfilled; - }); + it('should work with a non-cert credential when the GOOGLE_CLOUD_PROJECT environment variable is present', () => { + process.env.GOOGLE_CLOUD_PROJECT = mocks.projectId; - it('should be fulfilled given an app which fails to generate access tokens', () => { - // verifyIdToken() does not rely on an access token and therefore works in this scenario. - return rejectedPromiseAccessTokenAuth.verifyIdToken(mockIdToken) - .should.eventually.be.fulfilled; - }); + const mockCredentialAuth = testConfig.init(mocks.mockCredentialApp()); - it('should be fulfilled with checkRevoked set to true using an unrevoked ID token', () => { - const getUserStub = sinon.stub(Auth.prototype, 'getUser') - .returns(Promise.resolve(expectedUserRecord)); - stubs.push(getUserStub); - // Verify ID token while checking if revoked. - return auth.verifyIdToken(mockIdToken, true) - .then((result) => { - // Confirm underlying API called with expected parameters. - expect(getUserStub).to.have.been.calledOnce.and.calledWith(uid); - expect(result).to.deep.equal(decodedIdToken); + return mockCredentialAuth.verifyIdToken(mockIdToken).then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith(mockIdToken); }); - }); + }); - it('should be rejected with checkRevoked set to true using a revoked ID token', () => { - // One second before validSince. - const oneSecBeforeValidSince = new Date(validSince.getTime() - 1000); - // Restore verifyIdToken stub. - stub.restore(); - // Simulate revoked ID token returned with auth_time one second before validSince. - stub = sinon.stub(FirebaseTokenGenerator.prototype, 'verifyIdToken') - .returns(Promise.resolve(getDecodedIdToken(uid, oneSecBeforeValidSince))); - stubs.push(stub); - const getUserStub = sinon.stub(Auth.prototype, 'getUser') - .returns(Promise.resolve(expectedUserRecord)); - stubs.push(getUserStub); - // Verify ID token while checking if revoked. - return auth.verifyIdToken(mockIdToken, true) - .then((result) => { - throw new Error('Unexpected success'); - }, (error) => { - // Confirm underlying API called with expected parameters. - expect(getUserStub).to.have.been.calledOnce.and.calledWith(uid); - // Confirm expected error returned. - expect(error).to.have.property('code', 'auth/id-token-revoked'); - }); - }); + it('should work with a non-cert credential when the GCLOUD_PROJECT environment variable is present', () => { + process.env.GCLOUD_PROJECT = mocks.projectId; - it('should be fulfilled with checkRevoked set to false using a revoked ID token', () => { - // One second before validSince. - const oneSecBeforeValidSince = new Date(validSince.getTime() - 1000); - const oneSecBeforeValidSinceDecodedIdToken = - getDecodedIdToken(uid, oneSecBeforeValidSince); - // Restore verifyIdToken stub. - stub.restore(); - // Simulate revoked ID token returned with auth_time one second before validSince. - stub = sinon.stub(FirebaseTokenGenerator.prototype, 'verifyIdToken') - .returns(Promise.resolve(oneSecBeforeValidSinceDecodedIdToken)); - stubs.push(stub); - // Verify ID token without checking if revoked. - // This call should succeed. - return auth.verifyIdToken(mockIdToken, false) - .then((result) => { - expect(result).to.deep.equal(oneSecBeforeValidSinceDecodedIdToken); - }); - }); + const mockCredentialAuth = testConfig.init(mocks.mockCredentialApp()); - it('should be rejected with checkRevoked set to true if underlying RPC fails', () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); - const getUserStub = sinon.stub(Auth.prototype, 'getUser') - .returns(Promise.reject(expectedError)); - stubs.push(getUserStub); - // Verify ID token while checking if revoked. - // This should fail with the underlying RPC error. - return auth.verifyIdToken(mockIdToken, true) - .then((result) => { - throw new Error('Unexpected success'); - }, (error) => { - // Confirm underlying API called with expected parameters. - expect(getUserStub).to.have.been.calledOnce.and.calledWith(uid); - // Confirm expected error returned. - expect(error).to.equal(expectedError); + return mockCredentialAuth.verifyIdToken(mockIdToken).then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith(mockIdToken); }); - }); + }); - it('should be fulfilled with checkRevoked set to true when no validSince available', () => { - // Simulate no validSince set on the user. - const noValidSinceGetAccountInfoResponse = getValidGetAccountInfoResponse(); - delete (noValidSinceGetAccountInfoResponse.users[0] as any).validSince; - const noValidSinceExpectedUserRecord = - getValidUserRecord(noValidSinceGetAccountInfoResponse); - // Confirm null tokensValidAfterTime on user. - expect(noValidSinceExpectedUserRecord.tokensValidAfterTime).to.be.null; - // Simulate getUser returns the expected user with no validSince. - const getUserStub = sinon.stub(Auth.prototype, 'getUser') - .returns(Promise.resolve(noValidSinceExpectedUserRecord)); - stubs.push(getUserStub); - // Verify ID token while checking if revoked. - return auth.verifyIdToken(mockIdToken, true) - .then((result) => { - // Confirm underlying API called with expected parameters. - expect(getUserStub).to.have.been.calledOnce.and.calledWith(uid); - expect(result).to.deep.equal(decodedIdToken); - }); - }); + it('should be fulfilled given an app which returns null access tokens', () => { + // verifyIdToken() does not rely on an access token and therefore works in this scenario. + return nullAccessTokenAuth.verifyIdToken(mockIdToken) + .should.eventually.be.fulfilled; + }); - it('should be rejected with checkRevoked set to true using an invalid ID token', () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_CREDENTIAL); - // Restore verifyIdToken stub. - stub.restore(); - // Simulate ID token is invalid. - stub = sinon.stub(FirebaseTokenGenerator.prototype, 'verifyIdToken') - .returns(Promise.reject(expectedError)); - stubs.push(stub); - // Verify ID token while checking if revoked. - // This should fail with the underlying token generator verifyIdToken error. - return auth.verifyIdToken(mockIdToken, true) - .then((result) => { - throw new Error('Unexpected success'); - }, (error) => { - // Confirm expected error returned. - expect(error).to.equal(expectedError); - }); - }); - }); + it('should be fulfilled given an app which returns invalid access tokens', () => { + // verifyIdToken() does not rely on an access token and therefore works in this scenario. + return malformedAccessTokenAuth.verifyIdToken(mockIdToken) + .should.eventually.be.fulfilled; + }); + + it('should be fulfilled given an app which fails to generate access tokens', () => { + // verifyIdToken() does not rely on an access token and therefore works in this scenario. + return rejectedPromiseAccessTokenAuth.verifyIdToken(mockIdToken) + .should.eventually.be.fulfilled; + }); - describe('getUser()', () => { - const uid = 'abcdefghijklmnopqrstuvwxyz'; - const expectedGetAccountInfoResult = getValidGetAccountInfoResponse(); - const expectedUserRecord = getValidUserRecord(expectedGetAccountInfoResult); - const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); + it('should be fulfilled with checkRevoked set to true using an unrevoked ID token', () => { + const getUserStub = sinon.stub(testConfig.Auth.prototype, 'getUser') + .resolves(expectedUserRecord); + stubs.push(getUserStub); + // Verify ID token while checking if revoked. + return auth.verifyIdToken(mockIdToken, true) + .then((result) => { + // Confirm underlying API called with expected parameters. + expect(getUserStub).to.have.been.calledOnce.and.calledWith(uid); + expect(result).to.deep.equal(decodedIdToken); + }); + }); - // Stubs used to simulate underlying api calls. - let stubs: sinon.SinonStub[] = []; - beforeEach(() => sinon.spy(validator, 'isUid')); - afterEach(() => { - (validator.isUid as any).restore(); - _.forEach(stubs, (stub) => stub.restore()); - stubs = []; - }); + it('should be rejected with checkRevoked set to true using a revoked ID token', () => { + // One second before validSince. + const oneSecBeforeValidSince = new Date(validSince.getTime() - 1000); + // Restore verifyIdToken stub. + stub.restore(); + // Simulate revoked ID token returned with auth_time one second before validSince. + stub = sinon.stub(FirebaseTokenVerifier.prototype, 'verifyJWT') + .resolves(getDecodedIdToken(uid, oneSecBeforeValidSince)); + stubs.push(stub); + const getUserStub = sinon.stub(testConfig.Auth.prototype, 'getUser') + .resolves(expectedUserRecord); + stubs.push(getUserStub); + // Verify ID token while checking if revoked. + return auth.verifyIdToken(mockIdToken, true) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(getUserStub).to.have.been.calledOnce.and.calledWith(uid); + // Confirm expected error returned. + expect(error).to.have.property('code', 'auth/id-token-revoked'); + }); + }); - it('should be rejected given no uid', () => { - return (auth as any).getUser() - .should.eventually.be.rejected.and.have.property('code', 'auth/invalid-uid'); - }); + it('should be fulfilled with checkRevoked set to false using a revoked ID token', () => { + // One second before validSince. + const oneSecBeforeValidSince = new Date(validSince.getTime() - 1000); + const oneSecBeforeValidSinceDecodedIdToken = + getDecodedIdToken(uid, oneSecBeforeValidSince, tenantId); + // Restore verifyIdToken stub. + stub.restore(); + // Simulate revoked ID token returned with auth_time one second before validSince. + stub = sinon.stub(FirebaseTokenVerifier.prototype, 'verifyJWT') + .resolves(oneSecBeforeValidSinceDecodedIdToken); + stubs.push(stub); + // Verify ID token without checking if revoked. + // This call should succeed. + return auth.verifyIdToken(mockIdToken, false) + .then((result) => { + expect(result).to.deep.equal(oneSecBeforeValidSinceDecodedIdToken); + }); + }); - it('should be rejected given an invalid uid', () => { - const invalidUid = ('a' as any).repeat(129); - return auth.getUser(invalidUid) - .then(() => { - throw new Error('Unexpected success'); - }) - .catch((error) => { - expect(error).to.have.property('code', 'auth/invalid-uid'); - expect(validator.isUid).to.have.been.calledOnce.and.calledWith(invalidUid); - }); - }); + it('should be rejected with checkRevoked set to true if underlying RPC fails', () => { + const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); + const getUserStub = sinon.stub(testConfig.Auth.prototype, 'getUser') + .rejects(expectedError); + stubs.push(getUserStub); + // Verify ID token while checking if revoked. + // This should fail with the underlying RPC error. + return auth.verifyIdToken(mockIdToken, true) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(getUserStub).to.have.been.calledOnce.and.calledWith(uid); + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); - it('should be rejected given an app which returns null access tokens', () => { - return nullAccessTokenAuth.getUser(uid) - .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); - }); + it('should be fulfilled with checkRevoked set to true when no validSince available', () => { + // Simulate no validSince set on the user. + const noValidSinceGetAccountInfoResponse = getValidGetAccountInfoResponse(tenantId); + delete (noValidSinceGetAccountInfoResponse.users[0] as any).validSince; + const noValidSinceExpectedUserRecord = + getValidUserRecord(noValidSinceGetAccountInfoResponse); + // Confirm null tokensValidAfterTime on user. + expect(noValidSinceExpectedUserRecord.tokensValidAfterTime).to.be.undefined; + // Simulate getUser returns the expected user with no validSince. + const getUserStub = sinon.stub(testConfig.Auth.prototype, 'getUser') + .resolves(noValidSinceExpectedUserRecord); + stubs.push(getUserStub); + // Verify ID token while checking if revoked. + return auth.verifyIdToken(mockIdToken, true) + .then((result) => { + // Confirm underlying API called with expected parameters. + expect(getUserStub).to.have.been.calledOnce.and.calledWith(uid); + expect(result).to.deep.equal(decodedIdToken); + }); + }); - it('should be rejected given an app which returns invalid access tokens', () => { - return malformedAccessTokenAuth.getUser(uid) - .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); - }); + it('should be rejected with checkRevoked set to true using an invalid ID token', () => { + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_CREDENTIAL); + // Restore verifyIdToken stub. + stub.restore(); + // Simulate ID token is invalid. + stub = sinon.stub(FirebaseTokenVerifier.prototype, 'verifyJWT') + .rejects(expectedError); + stubs.push(stub); + // Verify ID token while checking if revoked. + // This should fail with the underlying token generator verifyIdToken error. + return auth.verifyIdToken(mockIdToken, true) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); - it('should be rejected given an app which fails to generate access tokens', () => { - return rejectedPromiseAccessTokenAuth.getUser(uid) - .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); - }); + if (testConfig.Auth === TenantAwareAuth) { + it('should be rejected with ID token missing tenant ID', () => { + const expectedError = new FirebaseAuthError(AuthClientErrorCode.MISMATCHING_TENANT_ID); + // Restore verifyIdToken stub. + stub.restore(); + // Simulate JWT does not contain tenant ID. + stub = sinon.stub(FirebaseTokenVerifier.prototype, 'verifyJWT') + .returns(Promise.resolve(getDecodedIdToken(uid, validSince))); + // Verify ID token. + return auth.verifyIdToken(mockIdToken) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm expected error returned. + expect(error).to.deep.include(expectedError); + }); + }); - it('should resolve with a UserRecord on success', () => { - // Stub getAccountInfoByUid to return expected result. - const stub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'getAccountInfoByUid') - .returns(Promise.resolve(expectedGetAccountInfoResult)); - stubs.push(stub); - return auth.getUser(uid) - .then((userRecord) => { - // Confirm underlying API called with expected parameters. - expect(stub).to.have.been.calledOnce.and.calledWith(uid); - // Confirm expected user record response returned. - expect(userRecord).to.deep.equal(expectedUserRecord); + it('should be rejected with ID token containing mismatching tenant ID', () => { + const expectedError = new FirebaseAuthError(AuthClientErrorCode.MISMATCHING_TENANT_ID); + // Restore verifyIdToken stub. + stub.restore(); + // Simulate JWT does not contain matching tenant ID. + stub = sinon.stub(FirebaseTokenVerifier.prototype, 'verifyJWT') + .returns(Promise.resolve(getDecodedIdToken(uid, validSince, 'otherTenantId'))); + // Verify ID token. + return auth.verifyIdToken(mockIdToken) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm expected error returned. + expect(error).to.deep.include(expectedError); + }); }); - }); + } + }); + + describe('verifySessionCookie()', () => { + let stub: sinon.SinonStub; + let mockSessionCookie: string; + const tenantId = testConfig.supportsTenantManagement ? undefined : TENANT_ID; + const expectedUserRecord = getValidUserRecord(getValidGetAccountInfoResponse(tenantId)); + // Set auth_time of token to expected user's tokensValidAfterTime. + if (!expectedUserRecord.tokensValidAfterTime) { + throw new Error("getValidUserRecord didn't properly set tokensValidAfterTime."); + } + const validSince = new Date(expectedUserRecord.tokensValidAfterTime); + // Set expected uid to expected user's. + const uid = expectedUserRecord.uid; + // Set expected decoded session cookie with expected UID and auth time. + const decodedSessionCookie = getDecodedSessionCookie(uid, validSince, tenantId); + let clock: sinon.SinonFakeTimers; + + // Stubs used to simulate underlying api calls. + const stubs: sinon.SinonStub[] = []; + beforeEach(() => { + stub = sinon.stub(FirebaseTokenVerifier.prototype, 'verifyJWT') + .resolves(decodedSessionCookie); + stubs.push(stub); + mockSessionCookie = mocks.generateSessionCookie(); + clock = sinon.useFakeTimers(validSince.getTime()); + }); + afterEach(() => { + _.forEach(stubs, (s) => s.restore()); + clock.restore(); + }); - it('should throw an error when the backend returns an error', () => { - // Stub getAccountInfoByUid to throw a backend error. - const stub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'getAccountInfoByUid') - .returns(Promise.reject(expectedError)); - stubs.push(stub); - return auth.getUser(uid) - .then((userRecord) => { - throw new Error('Unexpected success'); - }, (error) => { - // Confirm underlying API called with expected parameters. - expect(stub).to.have.been.calledOnce.and.calledWith(uid); - // Confirm expected error returned. - expect(error).to.equal(expectedError); + it('should forward on the call to the token verifier\'s verifySessionCookie() method', () => { + // Stub getUser call. + const getUserStub = sinon.stub(testConfig.Auth.prototype, 'getUser'); + stubs.push(getUserStub); + return auth.verifySessionCookie(mockSessionCookie).then((result) => { + // Confirm getUser never called. + expect(getUserStub).not.to.have.been.called; + expect(result).to.deep.equal(decodedSessionCookie); + expect(stub).to.have.been.calledOnce.and.calledWith(mockSessionCookie); }); - }); - }); + }); - describe('getUserByEmail()', () => { - const email = 'user@gmail.com'; - const expectedGetAccountInfoResult = getValidGetAccountInfoResponse(); - const expectedUserRecord = getValidUserRecord(expectedGetAccountInfoResult); - const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); + it('should reject when underlying sessionCookieVerifier.verifyJWT() rejects with expected error', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, 'Decoding Firebase session cookie failed'); + // Restore verifySessionCookie stub. + stub.restore(); + // Simulate session cookie is invalid. + stub = sinon.stub(FirebaseTokenVerifier.prototype, 'verifyJWT') + .rejects(expectedError); + stubs.push(stub); + return auth.verifySessionCookie(mockSessionCookie) + .should.eventually.be.rejectedWith('Decoding Firebase session cookie failed'); + }); - // 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 work with a non-cert credential when the GOOGLE_CLOUD_PROJECT environment variable is present', () => { + process.env.GOOGLE_CLOUD_PROJECT = mocks.projectId; - it('should be rejected given no email', () => { - return (auth as any).getUserByEmail() - .should.eventually.be.rejected.and.have.property('code', 'auth/invalid-email'); - }); + const mockCredentialAuth = testConfig.init(mocks.mockCredentialApp()); - it('should be rejected given an invalid email', () => { - const invalidEmail = 'name-example-com'; - return auth.getUserByEmail(invalidEmail) - .then(() => { - throw new Error('Unexpected success'); - }) - .catch((error) => { - expect(error).to.have.property('code', 'auth/invalid-email'); - expect(validator.isEmail).to.have.been.calledOnce.and.calledWith(invalidEmail); + return mockCredentialAuth.verifySessionCookie(mockSessionCookie).then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith(mockSessionCookie); }); - }); - - it('should be rejected given an app which returns null access tokens', () => { - return nullAccessTokenAuth.getUserByEmail(email) - .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.getUserByEmail(email) - .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.getUserByEmail(email) - .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); - }); + it('should work with a non-cert credential when the GCLOUD_PROJECT environment variable is present', () => { + process.env.GCLOUD_PROJECT = mocks.projectId; - it('should resolve with a UserRecord on success', () => { - // Stub getAccountInfoByEmail to return expected result. - const stub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'getAccountInfoByEmail') - .returns(Promise.resolve(expectedGetAccountInfoResult)); - stubs.push(stub); - return auth.getUserByEmail(email) - .then((userRecord) => { - // Confirm underlying API called with expected parameters. - expect(stub).to.have.been.calledOnce.and.calledWith(email); - // Confirm expected user record response returned. - expect(userRecord).to.deep.equal(expectedUserRecord); - }); - }); + const mockCredentialAuth = testConfig.init(mocks.mockCredentialApp()); - it('should throw an error when the backend returns an error', () => { - // Stub getAccountInfoByEmail to throw a backend error. - const stub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'getAccountInfoByEmail') - .returns(Promise.reject(expectedError)); - stubs.push(stub); - return auth.getUserByEmail(email) - .then((userRecord) => { - throw new Error('Unexpected success'); - }, (error) => { - // Confirm underlying API called with expected parameters. - expect(stub).to.have.been.calledOnce.and.calledWith(email); - // Confirm expected error returned. - expect(error).to.equal(expectedError); + return mockCredentialAuth.verifySessionCookie(mockSessionCookie).then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith(mockSessionCookie); }); - }); - }); + }); - describe('getUserByPhoneNumber()', () => { - const phoneNumber = '+11234567890'; - const expectedGetAccountInfoResult = getValidGetAccountInfoResponse(); - const expectedUserRecord = getValidUserRecord(expectedGetAccountInfoResult); - const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); + it('should be fulfilled given an app which returns null access tokens', () => { + // verifySessionCookie() does not rely on an access token and therefore works in this scenario. + return nullAccessTokenAuth.verifySessionCookie(mockSessionCookie) + .should.eventually.be.fulfilled; + }); - // Stubs used to simulate underlying api calls. - let stubs: sinon.SinonStub[] = []; - beforeEach(() => sinon.spy(validator, 'isPhoneNumber')); - afterEach(() => { - (validator.isPhoneNumber as any).restore(); - _.forEach(stubs, (stub) => stub.restore()); - stubs = []; - }); + it('should be fulfilled given an app which returns invalid access tokens', () => { + // verifySessionCookie() does not rely on an access token and therefore works in this scenario. + return malformedAccessTokenAuth.verifySessionCookie(mockSessionCookie) + .should.eventually.be.fulfilled; + }); - it('should be rejected given no phone number', () => { - return (auth as any).getUserByPhoneNumber() - .should.eventually.be.rejected.and.have.property('code', 'auth/invalid-phone-number'); - }); + it('should be fulfilled given an app which fails to generate access tokens', () => { + // verifySessionCookie() does not rely on an access token and therefore works in this scenario. + return rejectedPromiseAccessTokenAuth.verifySessionCookie(mockSessionCookie) + .should.eventually.be.fulfilled; + }); - it('should be rejected given an invalid phone number', () => { - const invalidPhoneNumber = 'invalid'; - return auth.getUserByPhoneNumber(invalidPhoneNumber) - .then(() => { - throw new Error('Unexpected success'); - }) - .catch((error) => { - expect(error).to.have.property('code', 'auth/invalid-phone-number'); - expect(validator.isPhoneNumber) - .to.have.been.calledOnce.and.calledWith(invalidPhoneNumber); - }); - }); + it('should be fulfilled with checkRevoked set to true using an unrevoked session cookie', () => { + const getUserStub = sinon.stub(testConfig.Auth.prototype, 'getUser') + .resolves(expectedUserRecord); + stubs.push(getUserStub); + // Verify ID token while checking if revoked. + return auth.verifySessionCookie(mockSessionCookie, true) + .then((result) => { + // Confirm underlying API called with expected parameters. + expect(getUserStub).to.have.been.calledOnce.and.calledWith(uid); + expect(result).to.deep.equal(decodedSessionCookie); + }); + }); - it('should be rejected given an app which returns null access tokens', () => { - return nullAccessTokenAuth.getUserByPhoneNumber(phoneNumber) - .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); - }); + it('should be rejected with checkRevoked set to true using a revoked session cookie', () => { + // One second before validSince. + const oneSecBeforeValidSince = new Date(validSince.getTime() - 1000); + // Restore verifySessionCookie stub. + stub.restore(); + // Simulate revoked session cookie returned with auth_time one second before validSince. + stub = sinon.stub(FirebaseTokenVerifier.prototype, 'verifyJWT') + .resolves(getDecodedSessionCookie(uid, oneSecBeforeValidSince)); + stubs.push(stub); + const getUserStub = sinon.stub(testConfig.Auth.prototype, 'getUser') + .resolves(expectedUserRecord); + stubs.push(getUserStub); + // Verify session cookie while checking if revoked. + return auth.verifySessionCookie(mockSessionCookie, true) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(getUserStub).to.have.been.calledOnce.and.calledWith(uid); + // Confirm expected error returned. + expect(error).to.have.property('code', 'auth/session-cookie-revoked'); + }); + }); - it('should be rejected given an app which returns invalid access tokens', () => { - return malformedAccessTokenAuth.getUserByPhoneNumber(phoneNumber) - .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); - }); + it('should be fulfilled with checkRevoked set to false using a revoked session cookie', () => { + // One second before validSince. + const oneSecBeforeValidSince = new Date(validSince.getTime() - 1000); + const oneSecBeforeValidSinceDecodedSessionCookie = + getDecodedSessionCookie(uid, oneSecBeforeValidSince, tenantId); + // Restore verifySessionCookie stub. + stub.restore(); + // Simulate revoked session cookie returned with auth_time one second before validSince. + stub = sinon.stub(FirebaseTokenVerifier.prototype, 'verifyJWT') + .resolves(oneSecBeforeValidSinceDecodedSessionCookie); + stubs.push(stub); + // Verify session cookie without checking if revoked. + // This call should succeed. + return auth.verifySessionCookie(mockSessionCookie, false) + .then((result) => { + expect(result).to.deep.equal(oneSecBeforeValidSinceDecodedSessionCookie); + }); + }); - it('should be rejected given an app which fails to generate access tokens', () => { - return rejectedPromiseAccessTokenAuth.getUserByPhoneNumber(phoneNumber) - .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); - }); + it('should be rejected with checkRevoked set to true if underlying RPC fails', () => { + const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); + const getUserStub = sinon.stub(testConfig.Auth.prototype, 'getUser') + .rejects(expectedError); + stubs.push(getUserStub); + // Verify session cookie while checking if revoked. + // This should fail with the underlying RPC error. + return auth.verifySessionCookie(mockSessionCookie, true) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(getUserStub).to.have.been.calledOnce.and.calledWith(uid); + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); - it('should resolve with a UserRecord on success', () => { - // Stub getAccountInfoByPhoneNumber to return expected result. - const stub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'getAccountInfoByPhoneNumber') - .returns(Promise.resolve(expectedGetAccountInfoResult)); - stubs.push(stub); - return auth.getUserByPhoneNumber(phoneNumber) - .then((userRecord) => { - // Confirm underlying API called with expected parameters. - expect(stub).to.have.been.calledOnce.and.calledWith(phoneNumber); - // Confirm expected user record response returned. - expect(userRecord).to.deep.equal(expectedUserRecord); - }); - }); + it('should be rejected with checkRevoked set to true and corresponding user disabled', () => { + const expectedAccountInfoResponse = getValidGetAccountInfoResponse(tenantId); + expectedAccountInfoResponse.users[0].disabled = true; + const expectedUserRecordDisabled = getValidUserRecord(expectedAccountInfoResponse); + const getUserStub = sinon.stub(testConfig.Auth.prototype, 'getUser') + .resolves(expectedUserRecordDisabled); + expect(expectedUserRecordDisabled.disabled).to.be.equal(true); + stubs.push(getUserStub); + return auth.verifySessionCookie(mockSessionCookie, true) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(getUserStub).to.have.been.calledOnce.and.calledWith(uid); + // Confirm expected error returned. + expect(error).to.have.property('code', 'auth/user-disabled'); + }); + }); - it('should throw an error when the backend returns an error', () => { - // Stub getAccountInfoByPhoneNumber to throw a backend error. - const stub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'getAccountInfoByPhoneNumber') - .returns(Promise.reject(expectedError)); - stubs.push(stub); - return auth.getUserByPhoneNumber(phoneNumber) - .then((userRecord) => { - throw new Error('Unexpected success'); - }, (error) => { - // Confirm underlying API called with expected parameters. - expect(stub).to.have.been.calledOnce.and.calledWith(phoneNumber); - // Confirm expected error returned. - expect(error).to.equal(expectedError); - }); - }); - }); + it('verifySessionCookie() should reject user disabled before ID tokens revoked', () => { + const expectedAccountInfoResponse = getValidGetAccountInfoResponse(tenantId); + const expectedAccountInfoResponseUserDisabled = Object.assign({}, expectedAccountInfoResponse); + expectedAccountInfoResponseUserDisabled.users[0].disabled = true; + const expectedUserRecordDisabled = getValidUserRecord(expectedAccountInfoResponseUserDisabled); + const validSince = new Date(expectedUserRecordDisabled.tokensValidAfterTime!); + // Restore verifySessionCookie stub. + stub.restore(); + // One second before validSince. + const oneSecBeforeValidSince = new Date(validSince.getTime() - 1000); + stub = sinon.stub(FirebaseTokenVerifier.prototype, 'verifyJWT') + .resolves(getDecodedIdToken(expectedUserRecordDisabled.uid, oneSecBeforeValidSince)); + stubs.push(stub); + const getUserStub = sinon.stub(testConfig.Auth.prototype, 'getUser') + .resolves(expectedUserRecordDisabled); + expect(expectedUserRecordDisabled.disabled).to.be.equal(true); + stubs.push(getUserStub); + return auth.verifySessionCookie(mockSessionCookie, true) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(getUserStub).to.have.been.calledOnce.and.calledWith(expectedUserRecordDisabled.uid); + // Confirm expected error returned. + expect(error).to.have.property('code', 'auth/user-disabled'); + }); + }); - describe('deleteUser()', () => { - const uid = 'abcdefghijklmnopqrstuvwxyz'; - const expectedDeleteAccountResult = {kind: 'identitytoolkit#DeleteAccountResponse'}; - const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); + it('should be fulfilled with checkRevoked set to true when no validSince available', () => { + // Simulate no validSince set on the user. + const noValidSinceGetAccountInfoResponse = getValidGetAccountInfoResponse(tenantId); + delete (noValidSinceGetAccountInfoResponse.users[0] as any).validSince; + const noValidSinceExpectedUserRecord = + getValidUserRecord(noValidSinceGetAccountInfoResponse); + // Confirm null tokensValidAfterTime on user. + expect(noValidSinceExpectedUserRecord.tokensValidAfterTime).to.be.undefined; + // Simulate getUser returns the expected user with no validSince. + const getUserStub = sinon.stub(testConfig.Auth.prototype, 'getUser') + .resolves(noValidSinceExpectedUserRecord); + stubs.push(getUserStub); + // Verify session cookie while checking if revoked. + return auth.verifySessionCookie(mockSessionCookie, true) + .then((result) => { + // Confirm underlying API called with expected parameters. + expect(getUserStub).to.have.been.calledOnce.and.calledWith(uid); + expect(result).to.deep.equal(decodedSessionCookie); + }); + }); - // Stubs used to simulate underlying api calls. - let stubs: sinon.SinonStub[] = []; - beforeEach(() => sinon.spy(validator, 'isUid')); - afterEach(() => { - (validator.isUid as any).restore(); - _.forEach(stubs, (stub) => stub.restore()); - stubs = []; - }); + it('should be rejected with checkRevoked set to true using an invalid session cookie', () => { + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_CREDENTIAL); + // Restore verifySessionCookie stub. + stub.restore(); + // Simulate session cookie is invalid. + stub = sinon.stub(FirebaseTokenVerifier.prototype, 'verifyJWT') + .rejects(expectedError); + stubs.push(stub); + // Verify session cookie while checking if revoked. + // This should fail with the underlying token generator verifySessionCookie error. + return auth.verifySessionCookie(mockSessionCookie, true) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); - it('should be rejected given no uid', () => { - return (auth as any).deleteUser() - .should.eventually.be.rejected.and.have.property('code', 'auth/invalid-uid'); - }); + if (testConfig.Auth === TenantAwareAuth) { + it('should be rejected with session cookie missing tenant ID', () => { + const expectedError = new FirebaseAuthError(AuthClientErrorCode.MISMATCHING_TENANT_ID); + // Restore verifyIdToken stub. + stub.restore(); + // Simulate JWT does not contain tenant ID.. + stub = sinon.stub(FirebaseTokenVerifier.prototype, 'verifyJWT') + .returns(Promise.resolve(getDecodedSessionCookie(uid, validSince))); + // Verify session cookie token. + return auth.verifySessionCookie(mockSessionCookie) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm expected error returned. + expect(error).to.deep.include(expectedError); + }); + }); - it('should be rejected given an invalid uid', () => { - const invalidUid = ('a' as any).repeat(129); - return auth.deleteUser(invalidUid) - .then(() => { - throw new Error('Unexpected success'); - }) - .catch((error) => { - expect(error).to.have.property('code', 'auth/invalid-uid'); - expect(validator.isUid).to.have.been.calledOnce.and.calledWith(invalidUid); + it('should be rejected with ID token containing mismatching tenant ID', () => { + const expectedError = new FirebaseAuthError(AuthClientErrorCode.MISMATCHING_TENANT_ID); + // Restore verifyIdToken stub. + stub.restore(); + // Simulate JWT does not contain matching tenant ID.. + stub = sinon.stub(FirebaseTokenVerifier.prototype, 'verifyJWT') + .returns(Promise.resolve(getDecodedSessionCookie(uid, validSince, 'otherTenantId'))); + // Verify session cookie token. + return auth.verifySessionCookie(mockSessionCookie) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm expected error returned. + expect(error).to.deep.include(expectedError); + }); }); - }); + } + }); + + describe('_verifyAuthBlockingToken()', () => { + let stub: sinon.SinonStub; + let mockAuthBlockingToken: string; + const tenantId = testConfig.supportsTenantManagement ? undefined : TENANT_ID; + const expectedUserRecord = getValidUserRecord(getValidGetAccountInfoResponse(tenantId)); + // Set auth_time of token to expected user's tokensValidAfterTime. + expect( + expectedUserRecord.tokensValidAfterTime, + "getValidUserRecord didn't properly set tokensValueAfterTime", + ).to.exist; + const validSince = new Date(expectedUserRecord.tokensValidAfterTime!); + // Set expected uid to expected user's. + const uid = expectedUserRecord.uid; + // Set expected decoded Auth Blocking token with expected UID and auth time. + const decodedAuthBlockingToken = getDecodedAuthBlockingToken(uid, validSince); + let clock: sinon.SinonFakeTimers; + + // Stubs used to simulate underlying api calls. + const stubs: sinon.SinonStub[] = []; + beforeEach(() => { + stub = sinon.stub(FirebaseTokenVerifier.prototype, '_verifyAuthBlockingToken') + .resolves(decodedAuthBlockingToken); + stubs.push(stub); + mockAuthBlockingToken = mocks.generateAuthBlockingToken(); + clock = sinon.useFakeTimers(validSince.getTime()); + }); + afterEach(() => { + _.forEach(stubs, (s) => s.restore()); + clock.restore(); + }); - it('should be rejected given an app which returns null access tokens', () => { - return nullAccessTokenAuth.deleteUser(uid) - .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); - }); + it('should forward on the call to the token generator\'s _verifyAuthBlockingToken() method', () => { + // Stub getUser call. + const getUserStub = sinon.stub(testConfig.Auth.prototype, 'getUser'); + stubs.push(getUserStub); + return auth._verifyAuthBlockingToken(mockAuthBlockingToken).then((result) => { + // Confirm getUser never called. + expect(getUserStub).not.to.have.been.called; + expect(result).to.deep.equal(decodedAuthBlockingToken); + expect(stub).to.have.been.calledOnce.and.calledWith(mockAuthBlockingToken); + }); + }); - it('should be rejected given an app which returns invalid access tokens', () => { - return malformedAccessTokenAuth.deleteUser(uid) - .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); - }); + it('should reject when underlying idTokenVerifier._verifyAuthBlockingToken() rejects', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, 'Decoding Firebase Auth Blocking token failed'); + // Restore _verifyAuthBlockingToken stub. + stub.restore(); + // Simulate Auth Blocking token is invalid. + stub = sinon.stub(FirebaseTokenVerifier.prototype, '_verifyAuthBlockingToken') + .rejects(expectedError); + stubs.push(stub); + return auth._verifyAuthBlockingToken(mockAuthBlockingToken) + .should.eventually.be.rejectedWith('Decoding Firebase Auth Blocking token failed'); + }); - it('should be rejected given an app which fails to generate access tokens', () => { - return rejectedPromiseAccessTokenAuth.deleteUser(uid) - .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); - }); + it('should work with a non-cert credential when the GOOGLE_CLOUD_PROJECT environment variable is present', () => { + process.env.GOOGLE_CLOUD_PROJECT = mocks.projectId; - it('should resolve with void on success', () => { - // Stub deleteAccount to return expected result. - const stub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'deleteAccount') - .returns(Promise.resolve(expectedDeleteAccountResult)); - stubs.push(stub); - return auth.deleteUser(uid) - .then((result) => { - // Confirm underlying API called with expected parameters. - expect(stub).to.have.been.calledOnce.and.calledWith(uid); - // Confirm expected result is undefined. - expect(result).to.be.undefined; - }); - }); + const mockCredentialAuth = testConfig.init(mocks.mockCredentialApp()); - it('should throw an error when the backend returns an error', () => { - // Stub deleteAccount to throw a backend error. - const stub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'deleteAccount') - .returns(Promise.reject(expectedError)); - stubs.push(stub); - return auth.deleteUser(uid) - .then(() => { - throw new Error('Unexpected success'); - }, (error) => { - // Confirm underlying API called with expected parameters. - expect(stub).to.have.been.calledOnce.and.calledWith(uid); - // Confirm expected error returned. - expect(error).to.equal(expectedError); + return mockCredentialAuth._verifyAuthBlockingToken(mockAuthBlockingToken).then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith(mockAuthBlockingToken); }); - }); - }); + }); - describe('createUser()', () => { - const uid = 'abcdefghijklmnopqrstuvwxyz'; - const expectedGetAccountInfoResult = getValidGetAccountInfoResponse(); - const expectedUserRecord = getValidUserRecord(expectedGetAccountInfoResult); - const expectedError = new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, - 'Unable to create the user record provided.'); - const unableToCreateUserError = new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, - 'Unable to create the user record provided.'); - const propertiesToCreate = { - displayName: expectedUserRecord.displayName, - photoURL: expectedUserRecord.photoURL, - email: expectedUserRecord.email, - emailVerified: expectedUserRecord.emailVerified, - password: 'password', - phoneNumber: expectedUserRecord.phoneNumber, - }; - // Stubs used to simulate underlying api calls. - let stubs: sinon.SinonStub[] = []; - beforeEach(() => sinon.spy(validator, 'isNonNullObject')); - afterEach(() => { - (validator.isNonNullObject as any).restore(); - _.forEach(stubs, (stub) => stub.restore()); - stubs = []; - }); + it('should work with a non-cert credential when the GCLOUD_PROJECT environment variable is present', () => { + process.env.GCLOUD_PROJECT = mocks.projectId; - it('should be rejected given no properties', () => { - return (auth as any).createUser() - .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); - }); + const mockCredentialAuth = testConfig.init(mocks.mockCredentialApp()); - it('should be rejected given invalid properties', () => { - return auth.createUser(null) - .then(() => { - throw new Error('Unexpected success'); - }) - .catch((error) => { - expect(error).to.have.property('code', 'auth/argument-error'); - expect(validator.isNonNullObject).to.have.been.calledOnce.and.calledWith(null); + return mockCredentialAuth._verifyAuthBlockingToken(mockAuthBlockingToken).then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith(mockAuthBlockingToken); }); - }); + }); - it('should be rejected given an app which returns null access tokens', () => { - return nullAccessTokenAuth.createUser(propertiesToCreate) - .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); - }); + it('should be fulfilled given an app which returns null access tokens', () => { + // _verifyAuthBlockingToken() does not rely on an access token and therefore works in this scenario. + return nullAccessTokenAuth._verifyAuthBlockingToken(mockAuthBlockingToken) + .should.eventually.be.fulfilled; + }); - it('should be rejected given an app which returns invalid access tokens', () => { - return malformedAccessTokenAuth.createUser(propertiesToCreate) - .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); - }); + it('should be fulfilled given an app which returns invalid access tokens', () => { + // _verifyAuthBlockingToken() does not rely on an access token and therefore works in this scenario. + return malformedAccessTokenAuth._verifyAuthBlockingToken(mockAuthBlockingToken) + .should.eventually.be.fulfilled; + }); - it('should be rejected given an app which fails to generate access tokens', () => { - return rejectedPromiseAccessTokenAuth.createUser(propertiesToCreate) - .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + it('should be fulfilled given an app which fails to generate access tokens', () => { + // _verifyAuthBlockingToken() does not rely on an access token and therefore works in this scenario. + return rejectedPromiseAccessTokenAuth._verifyAuthBlockingToken(mockAuthBlockingToken) + .should.eventually.be.fulfilled; + }); }); - it('should resolve with a UserRecord on createNewAccount request success', () => { - // Stub createNewAccount to return expected uid. - const createUserStub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'createNewAccount') - .returns(Promise.resolve(uid)); - // Stub getAccountInfoByUid to return expected result. - const getUserStub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'getAccountInfoByUid') - .returns(Promise.resolve(expectedGetAccountInfoResult)); - stubs.push(createUserStub); - stubs.push(getUserStub); - return auth.createUser(propertiesToCreate) - .then((userRecord) => { - // Confirm underlying API called with expected parameters. - expect(createUserStub).to.have.been.calledOnce.and.calledWith(propertiesToCreate); - expect(getUserStub).to.have.been.calledOnce.and.calledWith(uid); - // Confirm expected user record response returned. - expect(userRecord).to.deep.equal(expectedUserRecord); - }); - }); + describe('getUser()', () => { + const uid = 'abcdefghijklmnopqrstuvwxyz'; + const tenantId = testConfig.supportsTenantManagement ? undefined : TENANT_ID; + const expectedGetAccountInfoResult = getValidGetAccountInfoResponse(tenantId); + const expectedUserRecord = getValidUserRecord(expectedGetAccountInfoResult); + const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); - it('should throw an error when createNewAccount returns an error', () => { - // Stub createNewAccount to throw a backend error. - const createUserStub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'createNewAccount') - .returns(Promise.reject(expectedError)); - stubs.push(createUserStub); - return auth.createUser(propertiesToCreate) - .then((userRecord) => { - throw new Error('Unexpected success'); - }, (error) => { - // Confirm underlying API called with expected parameters. - expect(createUserStub).to.have.been.calledOnce.and.calledWith(propertiesToCreate); - // Confirm expected error returned. - expect(error).to.equal(expectedError); - }); - }); + // Stubs used to simulate underlying api calls. + let stubs: sinon.SinonStub[] = []; + beforeEach(() => sinon.spy(validator, 'isUid')); + afterEach(() => { + (validator.isUid as any).restore(); + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); - it('should throw an error when getUser returns a User not found error', () => { - // Stub createNewAccount to return expected uid. - const createUserStub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'createNewAccount') - .returns(Promise.resolve(uid)); - // Stub getAccountInfoByUid to throw user not found error. - const userNotFoundError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); - const getUserStub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'getAccountInfoByUid') - .returns(Promise.reject(userNotFoundError)); - stubs.push(createUserStub); - stubs.push(getUserStub); - return auth.createUser(propertiesToCreate) - .then((userRecord) => { - throw new Error('Unexpected success'); - }, (error) => { - // Confirm underlying API called with expected parameters. - expect(createUserStub).to.have.been.calledOnce.and.calledWith(propertiesToCreate); - expect(getUserStub).to.have.been.calledOnce.and.calledWith(uid); - // Confirm expected error returned. - expect(error.toString()).to.equal(unableToCreateUserError.toString()); - }); - }); + it('should be rejected given no uid', () => { + return (auth as any).getUser() + .should.eventually.be.rejected.and.have.property('code', 'auth/invalid-uid'); + }); - it('should echo getUser error if an error occurs while retrieving the user record', () => { - // Stub createNewAccount to return expected uid. - const createUserStub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'createNewAccount') - .returns(Promise.resolve(uid)); - // Stub getAccountInfoByUid to throw expected error. - const getUserStub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'getAccountInfoByUid') - .returns(Promise.reject(expectedError)); - stubs.push(createUserStub); - stubs.push(getUserStub); - return auth.createUser(propertiesToCreate) - .then((userRecord) => { - throw new Error('Unexpected success'); - }, (error) => { - // Confirm underlying API called with expected parameters. - expect(createUserStub).to.have.been.calledOnce.and.calledWith(propertiesToCreate); - expect(getUserStub).to.have.been.calledOnce.and.calledWith(uid); - // Confirm expected error returned (same error thrown by getUser). - expect(error).to.equal(expectedError); - }); - }); - }); + it('should be rejected given an invalid uid', () => { + const invalidUid = ('a' as any).repeat(129); + return auth.getUser(invalidUid) + .then(() => { + throw new Error('Unexpected success'); + }) + .catch((error) => { + expect(error).to.have.property('code', 'auth/invalid-uid'); + expect(validator.isUid).to.have.been.calledOnce.and.calledWith(invalidUid); + }); + }); - describe('updateUser()', () => { - const uid = 'abcdefghijklmnopqrstuvwxyz'; - const expectedGetAccountInfoResult = getValidGetAccountInfoResponse(); - const expectedUserRecord = getValidUserRecord(expectedGetAccountInfoResult); - const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); - const propertiesToEdit = { - displayName: expectedUserRecord.displayName, - photoURL: expectedUserRecord.photoURL, - email: expectedUserRecord.email, - emailVerified: expectedUserRecord.emailVerified, - password: 'password', - phoneNumber: expectedUserRecord.phoneNumber, - }; - // Stubs used to simulate underlying api calls. - let stubs: sinon.SinonStub[] = []; - beforeEach(() => { - sinon.spy(validator, 'isUid'); - sinon.spy(validator, 'isNonNullObject'); - }); - afterEach(() => { - (validator.isUid as any).restore(); - (validator.isNonNullObject as any).restore(); - _.forEach(stubs, (stub) => stub.restore()); - stubs = []; - }); + it('should be rejected given an app which returns null access tokens', () => { + return nullAccessTokenAuth.getUser(uid) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); - it('should be rejected given no uid', () => { - return (auth as any).updateUser(undefined, propertiesToEdit) - .should.eventually.be.rejected.and.have.property('code', 'auth/invalid-uid'); - }); + it('should be rejected given an app which returns invalid access tokens', () => { + return malformedAccessTokenAuth.getUser(uid) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); - it('should be rejected given an invalid uid', () => { - const invalidUid = ('a' as any).repeat(129); - return auth.updateUser(invalidUid, propertiesToEdit) - .then(() => { - throw new Error('Unexpected success'); - }) - .catch((error) => { - expect(error).to.have.property('code', 'auth/invalid-uid'); - expect(validator.isUid).to.have.been.calledOnce.and.calledWith(invalidUid); - }); - }); + it('should be rejected given an app which fails to generate access tokens', () => { + return rejectedPromiseAccessTokenAuth.getUser(uid) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); - it('should be rejected given no properties', () => { - return (auth as any).updateUser(uid) - .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); - }); + it('should resolve with a UserRecord on success', () => { + // Stub getAccountInfoByUid to return expected result. + const stub = sinon.stub(testConfig.RequestHandler.prototype, 'getAccountInfoByUid') + .resolves(expectedGetAccountInfoResult); + stubs.push(stub); + return auth.getUser(uid) + .then((userRecord) => { + // Confirm underlying API called with expected parameters. + expect(stub).to.have.been.calledOnce.and.calledWith(uid); + // Confirm expected user record response returned. + expect(userRecord).to.deep.equal(expectedUserRecord); + }); + }); - it('should be rejected given invalid properties', () => { - return auth.updateUser(uid, null) - .then(() => { - throw new Error('Unexpected success'); - }) - .catch((error) => { - expect(error).to.have.property('code', 'auth/argument-error'); - expect(validator.isNonNullObject).to.have.been.calledOnce.and.calledWith(null); - }); + it('should throw an error when the backend returns an error', () => { + // Stub getAccountInfoByUid to throw a backend error. + const stub = sinon.stub(testConfig.RequestHandler.prototype, 'getAccountInfoByUid') + .rejects(expectedError); + stubs.push(stub); + return auth.getUser(uid) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(stub).to.have.been.calledOnce.and.calledWith(uid); + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); }); - it('should be rejected given an app which returns null access tokens', () => { - return nullAccessTokenAuth.updateUser(uid, propertiesToEdit) - .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); - }); + describe('getUserByEmail()', () => { + const email = 'user@gmail.com'; + const tenantId = testConfig.supportsTenantManagement ? undefined : TENANT_ID; + const expectedGetAccountInfoResult = getValidGetAccountInfoResponse(tenantId); + const expectedUserRecord = getValidUserRecord(expectedGetAccountInfoResult); + const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); - it('should be rejected given an app which returns invalid access tokens', () => { - return malformedAccessTokenAuth.updateUser(uid, propertiesToEdit) - .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); - }); + // 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 an app which fails to generate access tokens', () => { - return rejectedPromiseAccessTokenAuth.updateUser(uid, propertiesToEdit) - .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); - }); + it('should be rejected given no email', () => { + return (auth as any).getUserByEmail() + .should.eventually.be.rejected.and.have.property('code', 'auth/invalid-email'); + }); - it('should resolve with a UserRecord on updateExistingAccount request success', () => { - // Stub updateExistingAccount to return expected uid. - const updateUserStub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'updateExistingAccount') - .returns(Promise.resolve(uid)); - // Stub getAccountInfoByUid to return expected result. - const getUserStub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'getAccountInfoByUid') - .returns(Promise.resolve(expectedGetAccountInfoResult)); - stubs.push(updateUserStub); - stubs.push(getUserStub); - return auth.updateUser(uid, propertiesToEdit) - .then((userRecord) => { - // Confirm underlying API called with expected parameters. - expect(updateUserStub).to.have.been.calledOnce.and.calledWith(uid, propertiesToEdit); - expect(getUserStub).to.have.been.calledOnce.and.calledWith(uid); - // Confirm expected user record response returned. - expect(userRecord).to.deep.equal(expectedUserRecord); - }); - }); + it('should be rejected given an invalid email', () => { + const invalidEmail = 'name-example-com'; + return auth.getUserByEmail(invalidEmail) + .then(() => { + throw new Error('Unexpected success'); + }) + .catch((error) => { + expect(error).to.have.property('code', 'auth/invalid-email'); + expect(validator.isEmail).to.have.been.calledOnce.and.calledWith(invalidEmail); + }); + }); - it('should throw an error when updateExistingAccount returns an error', () => { - // Stub updateExistingAccount to throw a backend error. - const updateUserStub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'updateExistingAccount') - .returns(Promise.reject(expectedError)); - stubs.push(updateUserStub); - return auth.updateUser(uid, propertiesToEdit) - .then((userRecord) => { - throw new Error('Unexpected success'); - }, (error) => { - // Confirm underlying API called with expected parameters. - expect(updateUserStub).to.have.been.calledOnce.and.calledWith(uid, propertiesToEdit); - // Confirm expected error returned. - expect(error).to.equal(expectedError); - }); - }); + it('should be rejected given an app which returns null access tokens', () => { + return nullAccessTokenAuth.getUserByEmail(email) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); - it('should echo getUser error if an error occurs while retrieving the user record', () => { - // Stub updateExistingAccount to return expected uid. - const updateUserStub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'updateExistingAccount') - .returns(Promise.resolve(uid)); - // Stub getAccountInfoByUid to throw an expected error. - const getUserStub = sinon.stub(FirebaseAuthRequestHandler.prototype, 'getAccountInfoByUid') - .returns(Promise.reject(expectedError)); - stubs.push(updateUserStub); - stubs.push(getUserStub); - return auth.updateUser(uid, propertiesToEdit) - .then((userRecord) => { - throw new Error('Unexpected success'); - }, (error) => { - // Confirm underlying API called with expected parameters. - expect(updateUserStub).to.have.been.calledOnce.and.calledWith(uid, propertiesToEdit); - expect(getUserStub).to.have.been.calledOnce.and.calledWith(uid); - // Confirm expected error returned (same error thrown by getUser). - expect(error).to.equal(expectedError); - }); - }); - }); + it('should be rejected given an app which returns invalid access tokens', () => { + return malformedAccessTokenAuth.getUserByEmail(email) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); - describe('setCustomUserClaims()', () => { - const uid = 'abcdefghijklmnopqrstuvwxyz'; - const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); - const customClaims = { - admin: true, - groupId: '123456', - }; - // Stubs used to simulate underlying api calls. - let stubs: sinon.SinonStub[] = []; - beforeEach(() => { - sinon.spy(validator, 'isUid'); - sinon.spy(validator, 'isObject'); - }); - afterEach(() => { - (validator.isUid as any).restore(); - (validator.isObject as any).restore(); - _.forEach(stubs, (stub) => stub.restore()); - stubs = []; + it('should be rejected given an app which fails to generate access tokens', () => { + return rejectedPromiseAccessTokenAuth.getUserByEmail(email) + .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, 'getAccountInfoByEmail') + .resolves(expectedGetAccountInfoResult); + stubs.push(stub); + return auth.getUserByEmail(email) + .then((userRecord) => { + // Confirm underlying API called with expected parameters. + expect(stub).to.have.been.calledOnce.and.calledWith(email); + // Confirm expected user record response returned. + expect(userRecord).to.deep.equal(expectedUserRecord); + }); + }); + + it('should throw an error when the backend returns an error', () => { + // Stub getAccountInfoByEmail to throw a backend error. + const stub = sinon.stub(testConfig.RequestHandler.prototype, 'getAccountInfoByEmail') + .rejects(expectedError); + stubs.push(stub); + return auth.getUserByEmail(email) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(stub).to.have.been.calledOnce.and.calledWith(email); + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); }); - it('should be rejected given no uid', () => { - return (auth as any).setCustomUserClaims(undefined, customClaims) - .should.eventually.be.rejected.and.have.property('code', 'auth/invalid-uid'); + describe('getUserByPhoneNumber()', () => { + const phoneNumber = '+11234567890'; + 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, 'isPhoneNumber')); + afterEach(() => { + (validator.isPhoneNumber as any).restore(); + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + it('should be rejected given no phone number', () => { + return (auth as any).getUserByPhoneNumber() + .should.eventually.be.rejected.and.have.property('code', 'auth/invalid-phone-number'); + }); + + it('should be rejected given an invalid phone number', () => { + const invalidPhoneNumber = 'invalid'; + return auth.getUserByPhoneNumber(invalidPhoneNumber) + .then(() => { + throw new Error('Unexpected success'); + }) + .catch((error) => { + expect(error).to.have.property('code', 'auth/invalid-phone-number'); + expect(validator.isPhoneNumber) + .to.have.been.calledOnce.and.calledWith(invalidPhoneNumber); + }); + }); + + it('should be rejected given an app which returns null access tokens', () => { + return nullAccessTokenAuth.getUserByPhoneNumber(phoneNumber) + .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.getUserByPhoneNumber(phoneNumber) + .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.getUserByPhoneNumber(phoneNumber) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should resolve with a UserRecord on success', () => { + // Stub getAccountInfoByPhoneNumber to return expected result. + const stub = sinon.stub(testConfig.RequestHandler.prototype, 'getAccountInfoByPhoneNumber') + .resolves(expectedGetAccountInfoResult); + stubs.push(stub); + return auth.getUserByPhoneNumber(phoneNumber) + .then((userRecord) => { + // Confirm underlying API called with expected parameters. + expect(stub).to.have.been.calledOnce.and.calledWith(phoneNumber); + // Confirm expected user record response returned. + expect(userRecord).to.deep.equal(expectedUserRecord); + }); + }); + + it('should throw an error when the backend returns an error', () => { + // Stub getAccountInfoByPhoneNumber to throw a backend error. + const stub = sinon.stub(testConfig.RequestHandler.prototype, 'getAccountInfoByPhoneNumber') + .rejects(expectedError); + stubs.push(stub); + return auth.getUserByPhoneNumber(phoneNumber) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(stub).to.have.been.calledOnce.and.calledWith(phoneNumber); + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); }); - it('should be rejected given an invalid uid', () => { - const invalidUid = ('a' as any).repeat(129); - return auth.setCustomUserClaims(invalidUid, customClaims) - .then(() => { - throw new Error('Unexpected success'); - }) - .catch((error) => { - expect(error).to.have.property('code', 'auth/invalid-uid'); - expect(validator.isUid).to.have.been.calledOnce.and.calledWith(invalidUid); + 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 be rejected given no custom user claims', () => { - return (auth as any).setCustomUserClaims(uid) - .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); + 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); + }); + }); }); - it('should be rejected given invalid custom user claims', () => { - return auth.setCustomUserClaims(uid, 'invalid' as any) - .then(() => { - throw new Error('Unexpected success'); - }) - .catch((error) => { - expect(error).to.have.property('code', 'auth/argument-error'); - expect(validator.isObject).to.have.been.calledOnce.and.calledWith('invalid'); + describe('getUsers()', () => { + let stubs: sinon.SinonStub[] = []; + + afterEach(() => { + stubs.forEach((stub) => stub.restore()); + stubs = []; + }); + + it('should throw when given a non array parameter', () => { + const nonArrayValues = [ null, undefined, 42, 3.14, "i'm not an array", {} ]; + nonArrayValues.forEach((v) => { + expect(() => auth.getUsers(v as any)) + .to.throw(FirebaseAuthError) + .with.property('code', 'auth/argument-error'); }); + }); + + it('should return no results when given no identifiers', () => { + return auth.getUsers([]) + .then((getUsersResult) => { + expect(getUsersResult.users).to.deep.equal([]); + expect(getUsersResult.notFound).to.deep.equal([]); + }); + }); + + it('should return no users when given identifiers that do not exist', () => { + const stub = sinon.stub(testConfig.RequestHandler.prototype, 'getAccountInfoByIdentifiers') + .resolves({}); + stubs.push(stub); + const notFoundIds = [{ uid: 'id that doesnt exist' }]; + return auth.getUsers(notFoundIds) + .then((getUsersResult) => { + expect(getUsersResult.users).to.deep.equal([]); + expect(getUsersResult.notFound).to.deep.equal(notFoundIds); + }); + }); + + it('returns users by various identifier types in a single call', async () => { + const mockUsers = [{ + localId: 'uid1', + email: 'user1@example.com', + phoneNumber: '+15555550001', + }, { + localId: 'uid2', + email: 'user2@example.com', + phoneNumber: '+15555550002', + }, { + localId: 'uid3', + email: 'user3@example.com', + phoneNumber: '+15555550003', + }, { + localId: 'uid4', + email: 'user4@example.com', + phoneNumber: '+15555550004', + providerUserInfo: [{ + providerId: 'google.com', + rawId: 'google_uid4', + }], + }]; + + const stub = sinon.stub(testConfig.RequestHandler.prototype, 'getAccountInfoByIdentifiers') + .resolves({ users: mockUsers }); + stubs.push(stub); + + const users = await auth.getUsers([ + { uid: 'uid1' }, + { email: 'user2@example.com' }, + { phoneNumber: '+15555550003' }, + { providerId: 'google.com', providerUid: 'google_uid4' }, + { uid: 'this-user-doesnt-exist' }, + ]); + + 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' }]); + }); }); - it('should be rejected given an app which returns null access tokens', () => { - return nullAccessTokenAuth.setCustomUserClaims(uid, customClaims) - .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + describe('deleteUser()', () => { + const uid = 'abcdefghijklmnopqrstuvwxyz'; + const expectedDeleteAccountResult = { kind: 'identitytoolkit#DeleteAccountResponse' }; + const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); + + // Stubs used to simulate underlying api calls. + let stubs: sinon.SinonStub[] = []; + beforeEach(() => sinon.spy(validator, 'isUid')); + afterEach(() => { + (validator.isUid as any).restore(); + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + it('should be rejected given no uid', () => { + return (auth as any).deleteUser() + .should.eventually.be.rejected.and.have.property('code', 'auth/invalid-uid'); + }); + + it('should be rejected given an invalid uid', () => { + const invalidUid = ('a' as any).repeat(129); + return auth.deleteUser(invalidUid) + .then(() => { + throw new Error('Unexpected success'); + }) + .catch((error) => { + expect(error).to.have.property('code', 'auth/invalid-uid'); + expect(validator.isUid).to.have.been.calledOnce.and.calledWith(invalidUid); + }); + }); + + it('should be rejected given an app which returns null access tokens', () => { + return nullAccessTokenAuth.deleteUser(uid) + .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.deleteUser(uid) + .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.deleteUser(uid) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should resolve with void on success', () => { + // Stub deleteAccount to return expected result. + const stub = sinon.stub(testConfig.RequestHandler.prototype, 'deleteAccount') + .resolves(expectedDeleteAccountResult); + stubs.push(stub); + return auth.deleteUser(uid) + .then((result) => { + // Confirm underlying API called with expected parameters. + expect(stub).to.have.been.calledOnce.and.calledWith(uid); + // Confirm expected result is undefined. + expect(result).to.be.undefined; + }); + }); + + it('should throw an error when the backend returns an error', () => { + // Stub deleteAccount to throw a backend error. + const stub = sinon.stub(testConfig.RequestHandler.prototype, 'deleteAccount') + .rejects(expectedError); + stubs.push(stub); + return auth.deleteUser(uid) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(stub).to.have.been.calledOnce.and.calledWith(uid); + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); }); - it('should be rejected given an app which returns invalid access tokens', () => { - return malformedAccessTokenAuth.setCustomUserClaims(uid, customClaims) - .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + describe('deleteUsers()', () => { + it('should succeed given an empty list', () => { + return auth.deleteUsers([]) + .then((deleteUsersResult) => { + expect(deleteUsersResult.successCount).to.equal(0); + expect(deleteUsersResult.failureCount).to.equal(0); + expect(deleteUsersResult.errors).to.have.length(0); + }); + }); + + it('should index errors correctly in result', async () => { + const stub = sinon.stub(testConfig.RequestHandler.prototype, 'deleteAccounts') + .resolves({ + errors: [{ + index: 0, + localId: 'uid1', + message: 'NOT_DISABLED : Disable the account before batch deletion.', + }, { + index: 2, + localId: 'uid3', + message: 'something awful', + }], + }); + + try { + const deleteUsersResult = await auth.deleteUsers(['uid1', 'uid2', 'uid3', 'uid4']); + + expect(deleteUsersResult.successCount).to.equal(2); + expect(deleteUsersResult.failureCount).to.equal(2); + expect(deleteUsersResult.errors).to.have.length(2); + expect(deleteUsersResult.errors[0].index).to.equal(0); + expect(deleteUsersResult.errors[0].error).to.have.property('code', 'auth/user-not-disabled'); + expect(deleteUsersResult.errors[1].index).to.equal(2); + expect(deleteUsersResult.errors[1].error).to.have.property('code', 'auth/internal-error'); + } finally { + stub.restore(); + } + }); + + it('should resolve with void on success', async () => { + const stub = sinon.stub(testConfig.RequestHandler.prototype, 'deleteAccounts') + .resolves({}); + try { + await auth.deleteUsers(['uid1', 'uid2', 'uid3']) + .then((result) => { + // Confirm underlying API called with expected parameters. + expect(stub).to.have.been.calledOnce.and.calledWith(['uid1', 'uid2', 'uid3']); + + expect(result.failureCount).to.equal(0); + expect(result.successCount).to.equal(3); + expect(result.errors).to.be.empty; + }); + } finally { + stub.restore(); + } + }); }); - it('should be rejected given an app which fails to generate access tokens', () => { - return rejectedPromiseAccessTokenAuth.setCustomUserClaims(uid, customClaims) - .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + describe('createUser()', () => { + const uid = 'abcdefghijklmnopqrstuvwxyz'; + const tenantId = testConfig.supportsTenantManagement ? undefined : TENANT_ID; + const expectedGetAccountInfoResult = getValidGetAccountInfoResponse(tenantId); + const expectedUserRecord = getValidUserRecord(expectedGetAccountInfoResult); + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'Unable to create the user record provided.'); + const unableToCreateUserError = new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'Unable to create the user record provided.'); + const propertiesToCreate = { + displayName: expectedUserRecord.displayName, + photoURL: expectedUserRecord.photoURL, + email: expectedUserRecord.email, + emailVerified: expectedUserRecord.emailVerified, + password: 'password', + phoneNumber: expectedUserRecord.phoneNumber, + }; + // Stubs used to simulate underlying api calls. + let stubs: sinon.SinonStub[] = []; + beforeEach(() => sinon.spy(validator, 'isNonNullObject')); + afterEach(() => { + (validator.isNonNullObject as any).restore(); + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + it('should be rejected given no properties', () => { + return (auth as any).createUser() + .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); + }); + + it('should be rejected given invalid properties', () => { + return auth.createUser(null as any) + .then(() => { + throw new Error('Unexpected success'); + }) + .catch((error) => { + expect(error).to.have.property('code', 'auth/argument-error'); + expect(validator.isNonNullObject).to.have.been.calledOnce.and.calledWith(null); + }); + }); + + it('should be rejected given an app which returns null access tokens', () => { + return nullAccessTokenAuth.createUser(propertiesToCreate) + .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.createUser(propertiesToCreate) + .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.createUser(propertiesToCreate) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should resolve with a UserRecord on createNewAccount request success', () => { + // Stub createNewAccount to return expected uid. + const createUserStub = sinon.stub(testConfig.RequestHandler.prototype, 'createNewAccount') + .resolves(uid); + // Stub getAccountInfoByUid to return expected result. + const getUserStub = sinon.stub(testConfig.RequestHandler.prototype, 'getAccountInfoByUid') + .resolves(expectedGetAccountInfoResult); + stubs.push(createUserStub); + stubs.push(getUserStub); + return auth.createUser(propertiesToCreate) + .then((userRecord) => { + // Confirm underlying API called with expected parameters. + expect(createUserStub).to.have.been.calledOnce.and.calledWith(propertiesToCreate); + expect(getUserStub).to.have.been.calledOnce.and.calledWith(uid); + // Confirm expected user record response returned. + expect(userRecord).to.deep.equal(expectedUserRecord); + }); + }); + + it('should throw an error when createNewAccount returns an error', () => { + // Stub createNewAccount to throw a backend error. + const createUserStub = sinon.stub(testConfig.RequestHandler.prototype, 'createNewAccount') + .rejects(expectedError); + stubs.push(createUserStub); + return auth.createUser(propertiesToCreate) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(createUserStub).to.have.been.calledOnce.and.calledWith(propertiesToCreate); + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); + + it('should throw an error when getUser returns a User not found error', () => { + // Stub createNewAccount to return expected uid. + const createUserStub = sinon.stub(testConfig.RequestHandler.prototype, 'createNewAccount') + .resolves(uid); + // Stub getAccountInfoByUid to throw user not found error. + const userNotFoundError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); + const getUserStub = sinon.stub(testConfig.RequestHandler.prototype, 'getAccountInfoByUid') + .rejects(userNotFoundError); + stubs.push(createUserStub); + stubs.push(getUserStub); + return auth.createUser(propertiesToCreate) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(createUserStub).to.have.been.calledOnce.and.calledWith(propertiesToCreate); + expect(getUserStub).to.have.been.calledOnce.and.calledWith(uid); + // Confirm expected error returned. + expect(error.toString()).to.equal(unableToCreateUserError.toString()); + }); + }); + + it('should echo getUser error if an error occurs while retrieving the user record', () => { + // Stub createNewAccount to return expected uid. + const createUserStub = sinon.stub(testConfig.RequestHandler.prototype, 'createNewAccount') + .resolves(uid); + // Stub getAccountInfoByUid to throw expected error. + const getUserStub = sinon.stub(testConfig.RequestHandler.prototype, 'getAccountInfoByUid') + .rejects(expectedError); + stubs.push(createUserStub); + stubs.push(getUserStub); + return auth.createUser(propertiesToCreate) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(createUserStub).to.have.been.calledOnce.and.calledWith(propertiesToCreate); + expect(getUserStub).to.have.been.calledOnce.and.calledWith(uid); + // Confirm expected error returned (same error thrown by getUser). + expect(error).to.equal(expectedError); + }); + }); }); - it('should resolve on setCustomUserClaims request success', () => { - // Stub setCustomUserClaims to return expected uid. - const setCustomUserClaimsStub = sinon - .stub(FirebaseAuthRequestHandler.prototype, 'setCustomUserClaims') - .returns(Promise.resolve(uid)); - stubs.push(setCustomUserClaimsStub); - return auth.setCustomUserClaims(uid, customClaims) - .then((response) => { - expect(response).to.be.undefined; - // Confirm underlying API called with expected parameters. - expect(setCustomUserClaimsStub) - .to.have.been.calledOnce.and.calledWith(uid, customClaims); + describe('updateUser()', () => { + const uid = 'abcdefghijklmnopqrstuvwxyz'; + const tenantId = testConfig.supportsTenantManagement ? undefined : TENANT_ID; + const expectedGetAccountInfoResult = getValidGetAccountInfoResponse(tenantId); + const expectedUserRecord = getValidUserRecord(expectedGetAccountInfoResult); + const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); + const propertiesToEdit = { + displayName: expectedUserRecord.displayName, + photoURL: expectedUserRecord.photoURL, + email: expectedUserRecord.email, + 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[] = []; + beforeEach(() => { + sinon.spy(validator, 'isUid'); + sinon.spy(validator, 'isNonNullObject'); + }); + afterEach(() => { + (validator.isUid as any).restore(); + (validator.isNonNullObject as any).restore(); + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + it('should be rejected given no uid', () => { + return (auth as any).updateUser(undefined, propertiesToEdit) + .should.eventually.be.rejected.and.have.property('code', 'auth/invalid-uid'); + }); + + it('should be rejected given an invalid uid', () => { + const invalidUid = ('a' as any).repeat(129); + return auth.updateUser(invalidUid, propertiesToEdit) + .then(() => { + throw new Error('Unexpected success'); + }) + .catch((error) => { + expect(error).to.have.property('code', 'auth/invalid-uid'); + expect(validator.isUid).to.have.been.calledOnce.and.calledWith(invalidUid); + }); + }); + + it('should be rejected given no properties', () => { + return (auth as any).updateUser(uid) + .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); + }); + + it('should be rejected given invalid properties', () => { + return auth.updateUser(uid, null as unknown as UpdateRequest) + .then(() => { + throw new Error('Unexpected success'); + }) + .catch((error) => { + expect(error).to.have.property('code', 'auth/argument-error'); + 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 throw an error when setCustomUserClaims returns an error', () => { - // Stub setCustomUserClaims to throw a backend error. - const setCustomUserClaimsStub = sinon - .stub(FirebaseAuthRequestHandler.prototype, 'setCustomUserClaims') - .returns(Promise.reject(expectedError)); - stubs.push(setCustomUserClaimsStub); - return auth.setCustomUserClaims(uid, customClaims) - .then(() => { - throw new Error('Unexpected success'); - }, (error) => { - // Confirm underlying API called with expected parameters. - expect(setCustomUserClaimsStub) - .to.have.been.calledOnce.and.calledWith(uid, customClaims); - // Confirm expected error returned. - expect(error).to.equal(expectedError); + 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', + }, }); - }); - }); - describe('listUsers()', () => { - const expectedError = new FirebaseAuthError(AuthClientErrorCode.INTERNAL_ERROR); - const pageToken = 'PAGE_TOKEN'; - const maxResult = 500; - const downloadAccountResponse: any = { - users: [ - {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'}), - ], - pageToken: 'NEXT_PAGE_TOKEN', - }; - const emptyDownloadAccountResponse = { - users: [], - }; - const emptyExpectedResult: any = { - users: [], - }; - // Stubs used to simulate underlying api calls. - let stubs: sinon.SinonStub[] = []; - beforeEach(() => { - sinon.spy(validator, 'isNonEmptyString'); - sinon.spy(validator, 'isNumber'); - }); - afterEach(() => { - (validator.isNonEmptyString as any).restore(); - (validator.isNumber as any).restore(); - _.forEach(stubs, (stub) => stub.restore()); - stubs = []; - }); + expect(invokeRequestHandlerStub).to.have.been.calledOnce.and.calledWith( + sinon.match.any, sinon.match.any, { + localId: uid, + linkProviderUserInfo: { + providerId: 'google.com', + rawId: 'google_uid', + }, + }); + }); - it('should be rejected given an invalid page token', () => { - const invalidToken = {}; - return auth.listUsers(undefined, invalidToken as any) - .then(() => { - throw new Error('Unexpected success'); - }) - .catch((error) => { - expect(error).to.have.property('code', 'auth/invalid-page-token'); - expect(validator.isNonEmptyString) - .to.have.been.calledOnce.and.calledWith(invalidToken); + 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); - it('should be rejected given an invalid max result', () => { - const invalidResults = 5000; - return auth.listUsers(invalidResults) - .then(() => { - throw new Error('Unexpected success'); - }) - .catch((error) => { - expect(error).to.have.property('code', 'auth/argument-error'); - expect(validator.isNumber) - .to.have.been.calledOnce.and.calledWith(invalidResults); + await auth.updateUser(uid, { + phoneNumber: null, + providersToUnlink: [ 'google.com' ], }); - }); - it('should be rejected given an app which returns null access tokens', () => { - return nullAccessTokenAuth.listUsers(maxResult) - .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); - }); + expect(invokeRequestHandlerStub).to.have.been.calledOnce.and.calledWith( + sinon.match.any, sinon.match.any, { + localId: uid, + deleteProvider: [ 'phone', 'google.com' ], + }); + }); - it('should be rejected given an app which returns invalid access tokens', () => { - return malformedAccessTokenAuth.listUsers(maxResult) - .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); - }); + 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('should be rejected given an app which fails to generate access tokens', () => { - return rejectedPromiseAccessTokenAuth.listUsers(maxResult) - .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); - }); + 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('should resolve on downloadAccount request success with users in response', () => { - // Stub downloadAccount to return expected response. - const downloadAccountStub = sinon - .stub(FirebaseAuthRequestHandler.prototype, 'downloadAccount') - .returns(Promise.resolve(downloadAccountResponse)); - stubs.push(downloadAccountStub); - return auth.listUsers(maxResult, pageToken) - .then((response) => { - expect(response).to.deep.equal(expectedResult); - // Confirm underlying API called with expected parameters. - expect(downloadAccountStub) - .to.have.been.calledOnce.and.calledWith(maxResult, pageToken); + 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('should resolve on downloadAccount request success with default options', () => { - // Stub downloadAccount to return expected response. - const downloadAccountStub = sinon - .stub(FirebaseAuthRequestHandler.prototype, 'downloadAccount') - .returns(Promise.resolve(downloadAccountResponse)); - stubs.push(downloadAccountStub); - return auth.listUsers() - .then((response) => { - expect(response).to.deep.equal(expectedResult); - // Confirm underlying API called with expected parameters. - expect(downloadAccountStub) - .to.have.been.calledOnce.and.calledWith(undefined, undefined); + 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('should resolve on downloadAccount request success with no users in response', () => { - // Stub downloadAccount to return expected response. - const downloadAccountStub = sinon - .stub(FirebaseAuthRequestHandler.prototype, 'downloadAccount') - .returns(Promise.resolve(emptyDownloadAccountResponse)); - stubs.push(downloadAccountStub); - return auth.listUsers(maxResult, pageToken) - .then((response) => { - expect(response).to.deep.equal(emptyExpectedResult); - // Confirm underlying API called with expected parameters. - expect(downloadAccountStub) - .to.have.been.calledOnce.and.calledWith(maxResult, pageToken); + 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('should throw an error when downloadAccount returns an error', () => { - // Stub downloadAccount to throw a backend error. - const downloadAccountStub = sinon - .stub(FirebaseAuthRequestHandler.prototype, 'downloadAccount') - .returns(Promise.reject(expectedError)); - stubs.push(downloadAccountStub); - return auth.listUsers(maxResult, pageToken) - .then((results) => { - throw new Error('Unexpected success'); - }, (error) => { - // Confirm underlying API called with expected parameters. - expect(downloadAccountStub) - .to.have.been.calledOnce.and.calledWith(maxResult, pageToken); - // Confirm expected error returned. - expect(error).to.equal(expectedError); + 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', + }, + }); }); + }); + + 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', () => { + return nullAccessTokenAuth.updateUser(uid, propertiesToEdit) + .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.updateUser(uid, propertiesToEdit) + .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.updateUser(uid, propertiesToEdit) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should resolve with a UserRecord on updateExistingAccount request success', () => { + // Stub updateExistingAccount to return expected uid. + const updateUserStub = sinon.stub(testConfig.RequestHandler.prototype, 'updateExistingAccount') + .resolves(uid); + // Stub getAccountInfoByUid to return expected result. + const getUserStub = sinon.stub(testConfig.RequestHandler.prototype, 'getAccountInfoByUid') + .resolves(expectedGetAccountInfoResult); + stubs.push(updateUserStub); + stubs.push(getUserStub); + return auth.updateUser(uid, propertiesToEdit) + .then((userRecord) => { + // Confirm underlying API called with expected parameters. + expect(updateUserStub).to.have.been.calledOnce.and.calledWith(uid, propertiesToEdit); + expect(getUserStub).to.have.been.calledOnce.and.calledWith(uid); + // Confirm expected user record response returned. + expect(userRecord).to.deep.equal(expectedUserRecord); + }); + }); + + it('should throw an error when updateExistingAccount returns an error', () => { + // Stub updateExistingAccount to throw a backend error. + const updateUserStub = sinon.stub(testConfig.RequestHandler.prototype, 'updateExistingAccount') + .rejects(expectedError); + stubs.push(updateUserStub); + return auth.updateUser(uid, propertiesToEdit) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(updateUserStub).to.have.been.calledOnce.and.calledWith(uid, propertiesToEdit); + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); + + it('should echo getUser error if an error occurs while retrieving the user record', () => { + // Stub updateExistingAccount to return expected uid. + const updateUserStub = sinon.stub(testConfig.RequestHandler.prototype, 'updateExistingAccount') + .resolves(uid); + // Stub getAccountInfoByUid to throw an expected error. + const getUserStub = sinon.stub(testConfig.RequestHandler.prototype, 'getAccountInfoByUid') + .rejects(expectedError); + stubs.push(updateUserStub); + stubs.push(getUserStub); + return auth.updateUser(uid, propertiesToEdit) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(updateUserStub).to.have.been.calledOnce.and.calledWith(uid, propertiesToEdit); + expect(getUserStub).to.have.been.calledOnce.and.calledWith(uid); + // Confirm expected error returned (same error thrown by getUser). + expect(error).to.equal(expectedError); + }); + }); }); - }); - describe('revokeRefreshTokens()', () => { - const uid = 'abcdefghijklmnopqrstuvwxyz'; - const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); - // Stubs used to simulate underlying api calls. - let stubs: sinon.SinonStub[] = []; - beforeEach(() => { - sinon.spy(validator, 'isUid'); + describe('setCustomUserClaims()', () => { + const uid = 'abcdefghijklmnopqrstuvwxyz'; + const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); + const customClaims = { + admin: true, + groupId: '123456', + }; + // Stubs used to simulate underlying api calls. + let stubs: sinon.SinonStub[] = []; + beforeEach(() => { + sinon.spy(validator, 'isUid'); + sinon.spy(validator, 'isObject'); + }); + afterEach(() => { + (validator.isUid as any).restore(); + (validator.isObject as any).restore(); + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + it('should be rejected given no uid', () => { + return (auth as any).setCustomUserClaims(undefined, customClaims) + .should.eventually.be.rejected.and.have.property('code', 'auth/invalid-uid'); + }); + + it('should be rejected given an invalid uid', () => { + const invalidUid = ('a' as any).repeat(129); + return auth.setCustomUserClaims(invalidUid, customClaims) + .then(() => { + throw new Error('Unexpected success'); + }) + .catch((error) => { + expect(error).to.have.property('code', 'auth/invalid-uid'); + expect(validator.isUid).to.have.been.calledOnce.and.calledWith(invalidUid); + }); + }); + + it('should be rejected given no custom user claims', () => { + return (auth as any).setCustomUserClaims(uid) + .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); + }); + + it('should be rejected given invalid custom user claims', () => { + return auth.setCustomUserClaims(uid, 'invalid' as any) + .then(() => { + throw new Error('Unexpected success'); + }) + .catch((error) => { + expect(error).to.have.property('code', 'auth/argument-error'); + expect(validator.isObject).to.have.been.calledOnce.and.calledWith('invalid'); + }); + }); + + it('should be rejected given an app which returns null access tokens', () => { + return nullAccessTokenAuth.setCustomUserClaims(uid, customClaims) + .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.setCustomUserClaims(uid, customClaims) + .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.setCustomUserClaims(uid, customClaims) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should resolve on setCustomUserClaims request success', () => { + // Stub setCustomUserClaims to return expected uid. + const setCustomUserClaimsStub = sinon + .stub(testConfig.RequestHandler.prototype, 'setCustomUserClaims') + .resolves(uid); + stubs.push(setCustomUserClaimsStub); + return auth.setCustomUserClaims(uid, customClaims) + .then((response) => { + expect(response).to.be.undefined; + // Confirm underlying API called with expected parameters. + expect(setCustomUserClaimsStub) + .to.have.been.calledOnce.and.calledWith(uid, customClaims); + }); + }); + + it('should throw an error when setCustomUserClaims returns an error', () => { + // Stub setCustomUserClaims to throw a backend error. + const setCustomUserClaimsStub = sinon + .stub(testConfig.RequestHandler.prototype, 'setCustomUserClaims') + .rejects(expectedError); + stubs.push(setCustomUserClaimsStub); + return auth.setCustomUserClaims(uid, customClaims) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(setCustomUserClaimsStub) + .to.have.been.calledOnce.and.calledWith(uid, customClaims); + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); }); - afterEach(() => { - (validator.isUid as any).restore(); - _.forEach(stubs, (stub) => stub.restore()); - stubs = []; + + describe('listUsers()', () => { + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INTERNAL_ERROR); + const pageToken = 'PAGE_TOKEN'; + const maxResult = 500; + const downloadAccountResponse: any = { + users: [ + { 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' }), + ], + pageToken: 'NEXT_PAGE_TOKEN', + }; + const emptyDownloadAccountResponse: any = { + users: [], + }; + const emptyExpectedResult: any = { + users: [], + }; + // Stubs used to simulate underlying api calls. + let stubs: sinon.SinonStub[] = []; + beforeEach(() => { + sinon.spy(validator, 'isNonEmptyString'); + sinon.spy(validator, 'isNumber'); + }); + afterEach(() => { + (validator.isNonEmptyString as any).restore(); + (validator.isNumber as any).restore(); + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + it('should be rejected given an invalid page token', () => { + const invalidToken = {}; + return auth.listUsers(undefined, invalidToken as any) + .then(() => { + throw new Error('Unexpected success'); + }) + .catch((error) => { + expect(error).to.have.property('code', 'auth/invalid-page-token'); + }); + }); + + it('should be rejected given an invalid max result', () => { + const invalidResults = 5000; + return auth.listUsers(invalidResults) + .then(() => { + throw new Error('Unexpected success'); + }) + .catch((error) => { + expect(error).to.have.property('code', 'auth/argument-error'); + expect(validator.isNumber) + .to.have.been.calledOnce.and.calledWith(invalidResults); + }); + }); + + it('should be rejected given an app which returns null access tokens', () => { + return nullAccessTokenAuth.listUsers(maxResult) + .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.listUsers(maxResult) + .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.listUsers(maxResult) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should resolve on downloadAccount request success with users in response', () => { + // Stub downloadAccount to return expected response. + const downloadAccountStub = sinon + .stub(testConfig.RequestHandler.prototype, 'downloadAccount') + .resolves(downloadAccountResponse); + stubs.push(downloadAccountStub); + return auth.listUsers(maxResult, pageToken) + .then((response) => { + expect(response).to.deep.equal(expectedResult); + // Confirm underlying API called with expected parameters. + expect(downloadAccountStub) + .to.have.been.calledOnce.and.calledWith(maxResult, pageToken); + }); + }); + + it('should resolve on downloadAccount request success with default options', () => { + // Stub downloadAccount to return expected response. + const downloadAccountStub = sinon + .stub(testConfig.RequestHandler.prototype, 'downloadAccount') + .resolves(downloadAccountResponse); + stubs.push(downloadAccountStub); + return auth.listUsers() + .then((response) => { + expect(response).to.deep.equal(expectedResult); + // Confirm underlying API called with expected parameters. + expect(downloadAccountStub) + .to.have.been.calledOnce.and.calledWith(undefined, undefined); + }); + }); + + it('should resolve on downloadAccount request success with no users in response', () => { + // Stub downloadAccount to return expected response. + const downloadAccountStub = sinon + .stub(testConfig.RequestHandler.prototype, 'downloadAccount') + .resolves(emptyDownloadAccountResponse); + stubs.push(downloadAccountStub); + return auth.listUsers(maxResult, pageToken) + .then((response) => { + expect(response).to.deep.equal(emptyExpectedResult); + // Confirm underlying API called with expected parameters. + expect(downloadAccountStub) + .to.have.been.calledOnce.and.calledWith(maxResult, pageToken); + }); + }); + + it('should throw an error when downloadAccount returns an error', () => { + // Stub downloadAccount to throw a backend error. + const downloadAccountStub = sinon + .stub(testConfig.RequestHandler.prototype, 'downloadAccount') + .rejects(expectedError); + stubs.push(downloadAccountStub); + return auth.listUsers(maxResult, pageToken) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(downloadAccountStub) + .to.have.been.calledOnce.and.calledWith(maxResult, pageToken); + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); }); - it('should be rejected given no uid', () => { - return (auth as any).revokeRefreshTokens(undefined) - .should.eventually.be.rejected.and.have.property('code', 'auth/invalid-uid'); + describe('revokeRefreshTokens()', () => { + const uid = 'abcdefghijklmnopqrstuvwxyz'; + const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); + // Stubs used to simulate underlying api calls. + let stubs: sinon.SinonStub[] = []; + beforeEach(() => { + sinon.spy(validator, 'isUid'); + }); + afterEach(() => { + (validator.isUid as any).restore(); + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + it('should be rejected given no uid', () => { + return (auth as any).revokeRefreshTokens(undefined) + .should.eventually.be.rejected.and.have.property('code', 'auth/invalid-uid'); + }); + + it('should be rejected given an invalid uid', () => { + const invalidUid = ('a' as any).repeat(129); + return auth.revokeRefreshTokens(invalidUid) + .then(() => { + throw new Error('Unexpected success'); + }) + .catch((error) => { + expect(error).to.have.property('code', 'auth/invalid-uid'); + expect(validator.isUid).to.have.been.calledOnce.and.calledWith(invalidUid); + }); + }); + + it('should be rejected given an app which returns null access tokens', () => { + return nullAccessTokenAuth.revokeRefreshTokens(uid) + .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.revokeRefreshTokens(uid) + .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.revokeRefreshTokens(uid) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should resolve on underlying revokeRefreshTokens request success', () => { + // Stub revokeRefreshTokens to return expected uid. + const revokeRefreshTokensStub = + sinon.stub(testConfig.RequestHandler.prototype, 'revokeRefreshTokens') + .resolves(uid); + stubs.push(revokeRefreshTokensStub); + return auth.revokeRefreshTokens(uid) + .then((result) => { + // Confirm underlying API called with expected parameters. + expect(revokeRefreshTokensStub).to.have.been.calledOnce.and.calledWith(uid); + // Confirm expected response returned. + expect(result).to.be.undefined; + }); + }); + + it('should throw when underlying revokeRefreshTokens request returns an error', () => { + // Stub revokeRefreshTokens to throw a backend error. + const revokeRefreshTokensStub = + sinon.stub(testConfig.RequestHandler.prototype, 'revokeRefreshTokens') + .rejects(expectedError); + stubs.push(revokeRefreshTokensStub); + return auth.revokeRefreshTokens(uid) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(revokeRefreshTokensStub).to.have.been.calledOnce.and.calledWith(uid); + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); }); - it('should be rejected given an invalid uid', () => { - const invalidUid = ('a' as any).repeat(129); - return auth.revokeRefreshTokens(invalidUid) - .then(() => { - throw new Error('Unexpected success'); - }) - .catch((error) => { - expect(error).to.have.property('code', 'auth/invalid-uid'); - expect(validator.isUid).to.have.been.calledOnce.and.calledWith(invalidUid); + describe('importUsers()', () => { + const users = [ + { uid: '1234', email: 'user@example.com', passwordHash: Buffer.from('password') }, + { uid: '5678', phoneNumber: 'invalid' }, + ]; + const options = { + hash: { + algorithm: 'BCRYPT' as any, + }, + }; + const expectedUserImportResultError = + new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER); + const expectedOptionsError = + new FirebaseAuthError(AuthClientErrorCode.INVALID_HASH_ALGORITHM); + const expectedServerError = + new FirebaseAuthError(AuthClientErrorCode.INTERNAL_ERROR); + const expectedUserImportResult = { + successCount: 1, + failureCount: 1, + errors: [ + { + index: 1, + error: expectedUserImportResultError, + }, + ], + }; + // Stubs used to simulate underlying api calls. + let stubs: sinon.SinonStub[] = []; + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + it('should be rejected given an app which returns null access tokens', () => { + return nullAccessTokenAuth.importUsers(users, options) + .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.importUsers(users, options) + .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.importUsers(users, options) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should resolve on underlying uploadAccount request resolution', () => { + // Stub uploadAccount to return expected result. + const uploadAccountStub = + sinon.stub(testConfig.RequestHandler.prototype, 'uploadAccount') + .resolves(expectedUserImportResult); + stubs.push(uploadAccountStub); + return auth.importUsers(users, options) + .then((result) => { + // Confirm underlying API called with expected parameters. + expect(uploadAccountStub).to.have.been.calledOnce.and.calledWith(users, options); + // Confirm expected response returned. + expect(result).to.be.equal(expectedUserImportResult); + }); + }); + + it('should reject when underlying uploadAccount request rejects with an error', () => { + // Stub uploadAccount to reject with expected error. + const uploadAccountStub = + sinon.stub(testConfig.RequestHandler.prototype, 'uploadAccount') + .rejects(expectedServerError); + stubs.push(uploadAccountStub); + return auth.importUsers(users, options) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(uploadAccountStub).to.have.been.calledOnce.and.calledWith(users, options); + // Confirm expected error returned. + expect(error).to.equal(expectedServerError); + }); + }); + + it('should throw and fail quickly when underlying uploadAccount throws', () => { + // Stub uploadAccount to throw with expected error. + const uploadAccountStub = + sinon.stub(testConfig.RequestHandler.prototype, 'uploadAccount') + .throws(expectedOptionsError); + stubs.push(uploadAccountStub); + expect(() => { + return auth.importUsers(users, { hash: { algorithm: 'invalid' as any } }); + }).to.throw(expectedOptionsError); + }); + + if (testConfig.Auth === TenantAwareAuth) { + it('should throw and fail quickly when users provided have mismatching tenant IDs', () => { + const usersCopy = deepCopy(users); + // Simulate one user with mismatching tenant ID. + (usersCopy[0] as any).tenantId = 'otherTenantId'; + expect(() => { + return auth.importUsers(usersCopy, options); + }).to.throw('UserRecord of index "0" has mismatching tenant ID "otherTenantId"'); + }); + + it('should resolve when users provided have matching tenant IDs', () => { + // Stub uploadAccount to return expected result. + const uploadAccountStub = + sinon.stub(testConfig.RequestHandler.prototype, 'uploadAccount') + .returns(Promise.resolve(expectedUserImportResult)); + const usersCopy = deepCopy(users); + usersCopy.forEach((user) => { + (user as any).tenantId = TENANT_ID; + }); + stubs.push(uploadAccountStub); + return auth.importUsers(usersCopy, options) + .then((result) => { + // Confirm underlying API called with expected parameters. + expect(uploadAccountStub).to.have.been.calledOnce.and.calledWith(usersCopy, options); + // Confirm expected response returned. + expect(result).to.be.equal(expectedUserImportResult); + }); + }); + } + }); + + describe('createSessionCookie()', () => { + const tenantId = testConfig.supportsTenantManagement ? undefined : TENANT_ID; + const idToken = 'ID_TOKEN'; + const options = { expiresIn: 60 * 60 * 24 * 1000 }; + const sessionCookie = 'SESSION_COOKIE'; + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_ID_TOKEN); + const expectedUserRecord = getValidUserRecord(getValidGetAccountInfoResponse(tenantId)); + // Set auth_time of token to expected user's tokensValidAfterTime. + if (!expectedUserRecord.tokensValidAfterTime) { + throw new Error("getValidUserRecord didn't properly set tokensValidAfterTime."); + } + const validSince = new Date(expectedUserRecord.tokensValidAfterTime); + // Set expected uid to expected user's. + const uid = expectedUserRecord.uid; + // Set expected decoded ID token with expected UID and auth time. + const decodedIdToken = getDecodedIdToken(uid, validSince, tenantId); + // Stubs used to simulate underlying api calls. + let stubs: sinon.SinonStub[] = []; + beforeEach(() => { + // If verifyIdToken stubbed, restore it. + if (testConfig.Auth.prototype.verifyIdToken.restore) { + testConfig.Auth.prototype.verifyIdToken.restore(); + } + sinon.spy(validator, 'isNonEmptyString'); + }); + afterEach(() => { + (validator.isNonEmptyString as any).restore(); + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + it('should be rejected given no ID token', () => { + return (auth as any).createSessionCookie(undefined, options) + .should.eventually.be.rejected.and.have.property('code', 'auth/invalid-id-token'); + }); + + it('should be rejected given an invalid ID token', () => { + const invalidIdToken = {} as any; + return auth.createSessionCookie(invalidIdToken, options) + .then(() => { + throw new Error('Unexpected success'); + }) + .catch((error) => { + expect(error).to.have.property('code', 'auth/invalid-id-token'); + }); + }); + + it('should be rejected given no session duration', () => { + // Simulate auth.verifyIdToken() succeeds if called. + stubs.push(sinon.stub(testConfig.Auth.prototype, 'verifyIdToken') + .returns(Promise.resolve(decodedIdToken))); + return (auth as any).createSessionCookie(idToken, undefined) + .should.eventually.be.rejected.and.have.property( + 'code', 'auth/invalid-session-cookie-duration'); + }); + + it('should be rejected given an invalid session duration', () => { + // Invalid object. + const invalidOptions = {} as any; + return auth.createSessionCookie(idToken, invalidOptions) + .should.eventually.be.rejected.and.have.property( + 'code', 'auth/invalid-session-cookie-duration'); + }); + + it('should be rejected given out of range session duration', () => { + // Simulate auth.verifyIdToken() succeeds if called. + stubs.push(sinon.stub(testConfig.Auth.prototype, 'verifyIdToken') + .returns(Promise.resolve(decodedIdToken))); + // 1 minute duration. + const invalidOptions = { expiresIn: 60 * 1000 }; + return auth.createSessionCookie(idToken, invalidOptions) + .should.eventually.be.rejected.and.have.property( + 'code', 'auth/invalid-session-cookie-duration'); + }); + + it('should be rejected given an app which returns null access tokens', () => { + // Simulate auth.verifyIdToken() succeeds if called. + stubs.push(sinon.stub(testConfig.Auth.prototype, 'verifyIdToken') + .returns(Promise.resolve(decodedIdToken))); + return nullAccessTokenAuth.createSessionCookie(idToken, options) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should be rejected given an app which returns invalid access tokens', () => { + stubs.push(sinon.stub(testConfig.Auth.prototype, 'verifyIdToken') + .returns(Promise.resolve(decodedIdToken))); + return malformedAccessTokenAuth.createSessionCookie(idToken, options) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should be rejected given an app which fails to generate access tokens', () => { + stubs.push(sinon.stub(testConfig.Auth.prototype, 'verifyIdToken') + .returns(Promise.resolve(decodedIdToken))); + return rejectedPromiseAccessTokenAuth.createSessionCookie(idToken, options) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should resolve on underlying createSessionCookie request success', () => { + // Simulate auth.verifyIdToken() succeeds if called. + const verifyIdTokenStub = sinon.stub(testConfig.Auth.prototype, 'verifyIdToken') + .returns(Promise.resolve(decodedIdToken)); + // Stub createSessionCookie to return expected sessionCookie. + const createSessionCookieStub = + sinon.stub(testConfig.RequestHandler.prototype, 'createSessionCookie') + .resolves(sessionCookie); + stubs.push(createSessionCookieStub); + return auth.createSessionCookie(idToken, options) + .then((result) => { + // Confirm underlying API called with expected parameters. + expect(createSessionCookieStub) + .to.have.been.calledOnce.and.calledWith(idToken, options.expiresIn); + // TenantAwareAuth should verify the ID token first. + if (testConfig.Auth === TenantAwareAuth) { + expect(verifyIdTokenStub) + .to.have.been.calledOnce.and.calledWith(idToken); + } else { + expect(verifyIdTokenStub).to.have.not.been.called; + } + // Confirm expected response returned. + expect(result).to.be.equal(sessionCookie); + }); + }); + + it('should throw when underlying createSessionCookie request returns an error', () => { + // Simulate auth.verifyIdToken() succeeds if called. + stubs.push(sinon.stub(testConfig.Auth.prototype, 'verifyIdToken') + .resolves(decodedIdToken)); + // Stub createSessionCookie to throw a backend error. + const createSessionCookieStub = + sinon.stub(testConfig.RequestHandler.prototype, 'createSessionCookie') + .rejects(expectedError); + stubs.push(createSessionCookieStub); + return auth.createSessionCookie(idToken, options) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(createSessionCookieStub) + .to.have.been.calledOnce.and.calledWith(idToken, options.expiresIn); + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); + + if (testConfig.Auth === TenantAwareAuth) { + it('should be rejected when ID token provided is invalid', () => { + // Simulate auth.verifyIdToken() fails when called. + const verifyIdTokenStub = sinon.stub(testConfig.Auth.prototype, 'verifyIdToken') + .returns(Promise.reject(expectedError)); + stubs.push(verifyIdTokenStub); + return auth.createSessionCookie(idToken, options) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(verifyIdTokenStub) + .to.have.been.calledOnce.and.calledWith(idToken); + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); + } + }); + + 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: 'generateVerifyAndChangeEmailLink', requestType: 'VERIFY_AND_CHANGE_EMAIL', requiresSettings: false }, + ]; + emailActionFlows.forEach((emailActionFlow) => { + describe(`${emailActionFlow.api}()`, () => { + const email = 'user@example.com'; + const newEmail = 'usernew@example.com'; + const actionCodeSettings = { + url: 'https://www.example.com/path/file?a=1&b=2', + handleCodeInApp: true, + iOS: { + bundleId: 'com.example.ios', + }, + android: { + packageName: 'com.example.android', + installApp: true, + minimumVersion: '6', + }, + dynamicLinkDomain: 'custom.page.link', + }; + const expectedLink = 'https://custom.page.link?link=' + + encodeURIComponent('https://projectId.firebaseapp.com/__/auth/action?oobCode=CODE') + + '&apn=com.example.android&ibi=com.example.ios'; + const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); + // Stubs used to simulate underlying api calls. + let stubs: sinon.SinonStub[] = []; + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + it('should be rejected given no email', () => { + let args: any = [ undefined, actionCodeSettings ]; + if (emailActionFlow.api === 'generateVerifyAndChangeEmailLink') { + args = [ undefined, newEmail, actionCodeSettings ]; + } + return (auth as any)[emailActionFlow.api](...args) + .should.eventually.be.rejected.and.have.property('code', 'auth/invalid-email'); + }); + + it('should be rejected given an invalid email', () => { + let args: any = [ 'invalid', actionCodeSettings ]; + if (emailActionFlow.api === 'generateVerifyAndChangeEmailLink') { + args = [ 'invalid', newEmail, actionCodeSettings ]; + } + return (auth as any)[emailActionFlow.api](...args) + .should.eventually.be.rejected.and.have.property('code', 'auth/invalid-email'); + }); + + it('should be rejected given no new email when request type is `generateVerifyAndChangeEmailLink`', () => { + if (emailActionFlow.api === 'generateVerifyAndChangeEmailLink') { + return (auth as any)[emailActionFlow.api](email) + .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); + } + }); + + it('should be rejected given an invalid new email when request type is `generateVerifyAndChangeEmailLink`', + () => { + if (emailActionFlow.api === 'generateVerifyAndChangeEmailLink') { + return (auth as any)[emailActionFlow.api](email, 'invalid') + .should.eventually.be.rejected.and.have.property('code', 'auth/invalid-new-email'); + } + }); + + it('should be rejected given an invalid ActionCodeSettings object', () => { + let args: any = [ email, 'invalid' ]; + if (emailActionFlow.api === 'generateVerifyAndChangeEmailLink') { + args = [ email, newEmail, 'invalid' ]; + } + return (auth as any)[emailActionFlow.api](...args) + .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); + }); + + it('should be rejected given an app which returns null access tokens', () => { + let args: any = [ email, actionCodeSettings ]; + if (emailActionFlow.api === 'generateVerifyAndChangeEmailLink') { + args = [ email, newEmail, actionCodeSettings ]; + } + return (nullAccessTokenAuth as any)[emailActionFlow.api](...args) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should be rejected given an app which returns invalid access tokens', () => { + let args: any = [ email, actionCodeSettings ]; + if (emailActionFlow.api === 'generateVerifyAndChangeEmailLink') { + args = [ email, newEmail, actionCodeSettings ]; + } + return (malformedAccessTokenAuth as any)[emailActionFlow.api](...args) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should be rejected given an app which fails to generate access tokens', () => { + let args: any = [ email, actionCodeSettings ]; + if (emailActionFlow.api === 'generateVerifyAndChangeEmailLink') { + args = [ email, newEmail, actionCodeSettings ]; + } + return (rejectedPromiseAccessTokenAuth as any)[emailActionFlow.api](...args) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should resolve when called with actionCodeSettings with a generated link on success', () => { + // Stub getEmailActionLink to return expected link. + const getEmailActionLinkStub = sinon.stub(testConfig.RequestHandler.prototype, 'getEmailActionLink') + .resolves(expectedLink); + stubs.push(getEmailActionLinkStub); + let args: any = [ email, actionCodeSettings ]; + if (emailActionFlow.api === 'generateVerifyAndChangeEmailLink') { + args = [ email, newEmail, actionCodeSettings ]; + } + return (auth as any)[emailActionFlow.api](...args) + .then((actualLink: string) => { + // Confirm underlying API called with expected parameters. + expect(getEmailActionLinkStub).to.have.been.calledOnce.and.calledWith( + emailActionFlow.requestType, email, actionCodeSettings); + // Confirm expected user record response returned. + expect(actualLink).to.equal(expectedLink); + }); }); + + if (emailActionFlow.requiresSettings) { + it('should reject when called without actionCodeSettings', () => { + return (auth as any)[emailActionFlow.api](email, undefined) + .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); + }); + } else { + it('should resolve when called without actionCodeSettings with a generated link on success', () => { + // Stub getEmailActionLink to return expected link. + const getEmailActionLinkStub = sinon.stub(testConfig.RequestHandler.prototype, 'getEmailActionLink') + .resolves(expectedLink); + stubs.push(getEmailActionLinkStub); + let args: any = [ email ]; + if (emailActionFlow.api === 'generateVerifyAndChangeEmailLink') { + args = [ email, newEmail ]; + } + return (auth as any)[emailActionFlow.api](...args) + .then((actualLink: string) => { + // Confirm underlying API called with expected parameters. + expect(getEmailActionLinkStub).to.have.been.calledOnce.and.calledWith( + emailActionFlow.requestType, email, undefined); + // Confirm expected user record response returned. + expect(actualLink).to.equal(expectedLink); + }); + }); + } + + it('should throw an error when getEmailAction returns an error', () => { + // Stub getEmailActionLink to throw a backend error. + const getEmailActionLinkStub = sinon.stub(testConfig.RequestHandler.prototype, 'getEmailActionLink') + .rejects(expectedError); + stubs.push(getEmailActionLinkStub); + let args: any = [ email, actionCodeSettings ]; + if (emailActionFlow.api === 'generateVerifyAndChangeEmailLink') { + args = [ email, newEmail, actionCodeSettings ]; + } + return (auth as any)[emailActionFlow.api](...args) + .then(() => { + throw new Error('Unexpected success'); + }, (error: any) => { + // Confirm underlying API called with expected parameters. + expect(getEmailActionLinkStub).to.have.been.calledOnce.and.calledWith( + emailActionFlow.requestType, email, actionCodeSettings); + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); + }); }); - it('should be rejected given an app which returns null access tokens', () => { - return nullAccessTokenAuth.revokeRefreshTokens(uid) - .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + describe('getProviderConfig()', () => { + let stubs: sinon.SinonStub[] = []; + + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + it('should be rejected given no provider ID', () => { + return (auth as any).getProviderConfig() + .should.eventually.be.rejected.and.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).getProviderConfig(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', () => { + const providerId = 'oidc.provider'; + return (nullAccessTokenAuth as Auth).getProviderConfig(providerId) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should be rejected given an app which returns invalid access tokens', () => { + const providerId = 'oidc.provider'; + return (malformedAccessTokenAuth as Auth).getProviderConfig(providerId) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should be rejected given an app which fails to generate access tokens', () => { + const providerId = 'oidc.provider'; + return (rejectedPromiseAccessTokenAuth as Auth).getProviderConfig(providerId) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + describe('using OIDC configurations', () => { + const providerId = 'oidc.provider'; + const serverResponse = { + name: `projects/project_id/oauthIdpConfigs/${providerId}`, + displayName: 'OIDC_DISPLAY_NAME', + enabled: true, + clientId: 'CLIENT_ID', + issuer: 'https://oidc.com/issuer', + }; + const expectedConfig = new OIDCConfig(serverResponse); + const expectedError = new FirebaseAuthError(AuthClientErrorCode.CONFIGURATION_NOT_FOUND); + + it('should resolve with an OIDCConfig on success', () => { + // Stub getOAuthIdpConfig to return expected result. + const stub = sinon.stub(testConfig.RequestHandler.prototype, 'getOAuthIdpConfig') + .resolves(serverResponse); + stubs.push(stub); + return (auth as Auth).getProviderConfig(providerId) + .then((result) => { + // Confirm underlying API called with expected parameters. + expect(stub).to.have.been.calledOnce.and.calledWith(providerId); + // Confirm expected config returned. + expect(result).to.deep.equal(expectedConfig); + }); + }); + + it('should throw an error when the backend returns an error', () => { + // Stub getOAuthIdpConfig to throw a backend error. + const stub = sinon.stub(testConfig.RequestHandler.prototype, 'getOAuthIdpConfig') + .rejects(expectedError); + stubs.push(stub); + return (auth as Auth).getProviderConfig(providerId) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(stub).to.have.been.calledOnce.and.calledWith(providerId); + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); + }); + + describe('using SAML configurations', () => { + const providerId = 'saml.provider'; + const serverResponse = { + name: `projects/project_id/inboundSamlConfigs/${providerId}`, + idpConfig: { + idpEntityId: 'IDP_ENTITY_ID', + ssoUrl: 'https://example.com/login', + signRequest: true, + idpCertificates: [ + { x509Certificate: 'CERT1' }, + { x509Certificate: 'CERT2' }, + ], + }, + spConfig: { + spEntityId: 'RP_ENTITY_ID', + callbackUri: 'https://projectId.firebaseapp.com/__/auth/handler', + }, + displayName: 'SAML_DISPLAY_NAME', + enabled: true, + }; + const expectedConfig = new SAMLConfig(serverResponse); + const expectedError = new FirebaseAuthError(AuthClientErrorCode.CONFIGURATION_NOT_FOUND); + + it('should resolve with a SAMLConfig on success', () => { + // Stub getInboundSamlConfig to return expected result. + const stub = sinon.stub(testConfig.RequestHandler.prototype, 'getInboundSamlConfig') + .resolves(serverResponse); + stubs.push(stub); + return (auth as Auth).getProviderConfig(providerId) + .then((result) => { + // Confirm underlying API called with expected parameters. + expect(stub).to.have.been.calledOnce.and.calledWith(providerId); + // Confirm expected config returned. + expect(result).to.deep.equal(expectedConfig); + }); + }); + + it('should throw an error when the backend returns an error', () => { + // Stub getInboundSamlConfig to throw a backend error. + const stub = sinon.stub(testConfig.RequestHandler.prototype, 'getInboundSamlConfig') + .rejects(expectedError); + stubs.push(stub); + return (auth as Auth).getProviderConfig(providerId) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(stub).to.have.been.calledOnce.and.calledWith(providerId); + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); + }); }); - it('should be rejected given an app which returns invalid access tokens', () => { - return malformedAccessTokenAuth.revokeRefreshTokens(uid) - .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + describe('listProviderConfigs()', () => { + const options: AuthProviderConfigFilter = { + type: 'oidc', + }; + let stubs: sinon.SinonStub[] = []; + + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + it('should be rejected given no options', () => { + return (auth as any).listProviderConfigs() + .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); + }); + + it('should be rejected given an invalid AuthProviderConfigFilter type', () => { + const invalidOptions = { + type: 'unsupported', + }; + return (auth as Auth).listProviderConfigs(invalidOptions as any) + .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); + }); + + it('should be rejected given an app which returns null access tokens', () => { + return (nullAccessTokenAuth as Auth).listProviderConfigs(options) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should be rejected given an app which returns invalid access tokens', () => { + return (malformedAccessTokenAuth as Auth).listProviderConfigs(options) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should be rejected given an app which fails to generate access tokens', () => { + return (rejectedPromiseAccessTokenAuth as Auth).listProviderConfigs(options) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + describe('using OIDC type filter', () => { + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INTERNAL_ERROR); + const pageToken = 'PAGE_TOKEN'; + const maxResults = 50; + const filterOptions: AuthProviderConfigFilter = { + type: 'oidc', + pageToken, + maxResults, + }; + const listConfigsResponse: any = { + oauthIdpConfigs : [ + getOIDCConfigServerResponse('oidc.provider1'), + getOIDCConfigServerResponse('oidc.provider2'), + ], + nextPageToken: 'NEXT_PAGE_TOKEN', + }; + const expectedResult: any = { + providerConfigs: [ + new OIDCConfig(listConfigsResponse.oauthIdpConfigs[0]), + new OIDCConfig(listConfigsResponse.oauthIdpConfigs[1]), + ], + pageToken: 'NEXT_PAGE_TOKEN', + }; + const emptyListConfigsResponse: any = { + oauthIdpConfigs: [], + }; + const emptyExpectedResult: any = { + providerConfigs: [], + }; + + it('should resolve on success with configs in response', () => { + // Stub listOAuthIdpConfigs to return expected response. + const listConfigsStub = sinon + .stub(testConfig.RequestHandler.prototype, 'listOAuthIdpConfigs') + .resolves(listConfigsResponse); + stubs.push(listConfigsStub); + return auth.listProviderConfigs(filterOptions) + .then((response) => { + expect(response).to.deep.equal(expectedResult); + // Confirm underlying API called with expected parameters. + expect(listConfigsStub) + .to.have.been.calledOnce.and.calledWith(maxResults, pageToken); + }); + }); + + it('should resolve on success with default options', () => { + // Stub listOAuthIdpConfigs to return expected response. + const listConfigsStub = sinon + .stub(testConfig.RequestHandler.prototype, 'listOAuthIdpConfigs') + .resolves(listConfigsResponse); + stubs.push(listConfigsStub); + return (auth as Auth).listProviderConfigs({ type: 'oidc' }) + .then((response) => { + expect(response).to.deep.equal(expectedResult); + // Confirm underlying API called with expected parameters. + expect(listConfigsStub) + .to.have.been.calledOnce.and.calledWith(undefined, undefined); + }); + }); + + + it('should resolve on success with no configs in response', () => { + // Stub listOAuthIdpConfigs to return expected response. + const listConfigsStub = sinon + .stub(testConfig.RequestHandler.prototype, 'listOAuthIdpConfigs') + .resolves(emptyListConfigsResponse); + stubs.push(listConfigsStub); + return auth.listProviderConfigs(filterOptions) + .then((response) => { + expect(response).to.deep.equal(emptyExpectedResult); + // Confirm underlying API called with expected parameters. + expect(listConfigsStub) + .to.have.been.calledOnce.and.calledWith(maxResults, pageToken); + }); + }); + + it('should throw an error when listOAuthIdpConfigs returns an error', () => { + // Stub listOAuthIdpConfigs to throw a backend error. + const listConfigsStub = sinon + .stub(testConfig.RequestHandler.prototype, 'listOAuthIdpConfigs') + .rejects(expectedError); + stubs.push(listConfigsStub); + return auth.listProviderConfigs(filterOptions) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(listConfigsStub) + .to.have.been.calledOnce.and.calledWith(maxResults, pageToken); + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); + }); + + describe('using SAML type filter', () => { + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INTERNAL_ERROR); + const pageToken = 'PAGE_TOKEN'; + const maxResults = 50; + const filterOptions: AuthProviderConfigFilter = { + type: 'saml', + pageToken, + maxResults, + }; + const listConfigsResponse: any = { + inboundSamlConfigs : [ + getSAMLConfigServerResponse('saml.provider1'), + getSAMLConfigServerResponse('saml.provider2'), + ], + nextPageToken: 'NEXT_PAGE_TOKEN', + }; + const expectedResult: any = { + providerConfigs: [ + new SAMLConfig(listConfigsResponse.inboundSamlConfigs[0]), + new SAMLConfig(listConfigsResponse.inboundSamlConfigs[1]), + ], + pageToken: 'NEXT_PAGE_TOKEN', + }; + const emptyListConfigsResponse: any = { + inboundSamlConfigs: [], + }; + const emptyExpectedResult: any = { + providerConfigs: [], + }; + + it('should resolve on success with configs in response', () => { + // Stub listInboundSamlConfigs to return expected response. + const listConfigsStub = sinon + .stub(testConfig.RequestHandler.prototype, 'listInboundSamlConfigs') + .resolves(listConfigsResponse); + stubs.push(listConfigsStub); + return auth.listProviderConfigs(filterOptions) + .then((response) => { + expect(response).to.deep.equal(expectedResult); + // Confirm underlying API called with expected parameters. + expect(listConfigsStub) + .to.have.been.calledOnce.and.calledWith(maxResults, pageToken); + }); + }); + + it('should resolve on success with default options', () => { + // Stub listInboundSamlConfigs to return expected response. + const listConfigsStub = sinon + .stub(testConfig.RequestHandler.prototype, 'listInboundSamlConfigs') + .resolves(listConfigsResponse); + stubs.push(listConfigsStub); + return (auth as Auth).listProviderConfigs({ type: 'saml' }) + .then((response) => { + expect(response).to.deep.equal(expectedResult); + // Confirm underlying API called with expected parameters. + expect(listConfigsStub) + .to.have.been.calledOnce.and.calledWith(undefined, undefined); + }); + }); + + + it('should resolve on success with no configs in response', () => { + // Stub listInboundSamlConfigs to return expected response. + const listConfigsStub = sinon + .stub(testConfig.RequestHandler.prototype, 'listInboundSamlConfigs') + .resolves(emptyListConfigsResponse); + stubs.push(listConfigsStub); + return auth.listProviderConfigs(filterOptions) + .then((response) => { + expect(response).to.deep.equal(emptyExpectedResult); + // Confirm underlying API called with expected parameters. + expect(listConfigsStub) + .to.have.been.calledOnce.and.calledWith(maxResults, pageToken); + }); + }); + + it('should throw an error when listInboundSamlConfigs returns an error', () => { + // Stub listInboundSamlConfigs to throw a backend error. + const listConfigsStub = sinon + .stub(testConfig.RequestHandler.prototype, 'listInboundSamlConfigs') + .rejects(expectedError); + stubs.push(listConfigsStub); + return auth.listProviderConfigs(filterOptions) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(listConfigsStub) + .to.have.been.calledOnce.and.calledWith(maxResults, pageToken); + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); + }); }); - it('should be rejected given an app which fails to generate access tokens', () => { - return rejectedPromiseAccessTokenAuth.revokeRefreshTokens(uid) - .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + describe('deleteProviderConfig()', () => { + let stubs: sinon.SinonStub[] = []; + + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + it('should be rejected given no provider ID', () => { + return (auth as any).deleteProviderConfig() + .should.eventually.be.rejected.and.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', () => { + const providerId = 'oidc.provider'; + return (nullAccessTokenAuth as Auth).deleteProviderConfig(providerId) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should be rejected given an app which returns invalid access tokens', () => { + const providerId = 'oidc.provider'; + return (malformedAccessTokenAuth as Auth).deleteProviderConfig(providerId) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should be rejected given an app which fails to generate access tokens', () => { + const providerId = 'oidc.provider'; + return (rejectedPromiseAccessTokenAuth as Auth).deleteProviderConfig(providerId) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + describe('using OIDC configurations', () => { + const providerId = 'oidc.provider'; + const expectedError = new FirebaseAuthError(AuthClientErrorCode.CONFIGURATION_NOT_FOUND); + + it('should resolve with void on success', () => { + // Stub deleteOAuthIdpConfig to resolve. + const stub = sinon.stub(testConfig.RequestHandler.prototype, 'deleteOAuthIdpConfig') + .resolves(); + stubs.push(stub); + return (auth as Auth).deleteProviderConfig(providerId) + .then((result) => { + // Confirm underlying API called with expected parameters. + expect(stub).to.have.been.calledOnce.and.calledWith(providerId); + // Confirm expected result returned. + expect(result).to.be.undefined; + }); + }); + + it('should throw an error when the backend returns an error', () => { + // Stub deleteOAuthIdpConfig to throw a backend error. + const stub = sinon.stub(testConfig.RequestHandler.prototype, 'deleteOAuthIdpConfig') + .rejects(expectedError); + stubs.push(stub); + return (auth as Auth).deleteProviderConfig(providerId) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(stub).to.have.been.calledOnce.and.calledWith(providerId); + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); + }); + + describe('using SAML configurations', () => { + const providerId = 'saml.provider'; + const expectedError = new FirebaseAuthError(AuthClientErrorCode.CONFIGURATION_NOT_FOUND); + + it('should resolve with void on success', () => { + // Stub deleteInboundSamlConfig to resolve. + const stub = sinon.stub(testConfig.RequestHandler.prototype, 'deleteInboundSamlConfig') + .resolves(); + stubs.push(stub); + return (auth as Auth).deleteProviderConfig(providerId) + .then((result) => { + // Confirm underlying API called with expected parameters. + expect(stub).to.have.been.calledOnce.and.calledWith(providerId); + // Confirm expected result returned. + expect(result).to.be.undefined; + }); + }); + + it('should throw an error when the backend returns an error', () => { + // Stub deleteInboundSamlConfig to throw a backend error. + const stub = sinon.stub(testConfig.RequestHandler.prototype, 'deleteInboundSamlConfig') + .rejects(expectedError); + stubs.push(stub); + return (auth as Auth).deleteProviderConfig(providerId) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(stub).to.have.been.calledOnce.and.calledWith(providerId); + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); + }); }); - it('should resolve on underlying revokeRefreshTokens request success', () => { - // Stub revokeRefreshTokens to return expected uid. - const revokeRefreshTokensStub = - sinon.stub(FirebaseAuthRequestHandler.prototype, 'revokeRefreshTokens') - .returns(Promise.resolve(uid)); - stubs.push(revokeRefreshTokensStub); - return auth.revokeRefreshTokens(uid) - .then((result) => { - // Confirm underlying API called with expected parameters. - expect(revokeRefreshTokensStub).to.have.been.calledOnce.and.calledWith(uid); - // Confirm expected response returned. - expect(result).to.be.undefined; + describe('updateProviderConfig()', () => { + const oidcConfigOptions = { + displayName: 'OIDC_DISPLAY_NAME', + enabled: true, + clientId: 'CLIENT_ID', + issuer: 'https://oidc.com/issuer', + }; + let stubs: sinon.SinonStub[] = []; + + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + it('should be rejected given no provider ID', () => { + return (auth as any).updateProviderConfig(undefined, oidcConfigOptions) + .should.eventually.be.rejected.and.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', () => { + const providerId = 'oidc.provider'; + return (auth as any).updateProviderConfig(providerId) + .then(() => { + throw new Error('Unexpected success'); + }) + .catch((error: FirebaseAuthError) => { + expect(error).to.have.property('code', 'auth/invalid-config'); + }); + }); + + it('should be rejected given an app which returns null access tokens', () => { + const providerId = 'oidc.provider'; + return (nullAccessTokenAuth as Auth).updateProviderConfig(providerId, oidcConfigOptions) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should be rejected given an app which returns invalid access tokens', () => { + const providerId = 'oidc.provider'; + return (malformedAccessTokenAuth as Auth).updateProviderConfig(providerId, oidcConfigOptions) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should be rejected given an app which fails to generate access tokens', () => { + const providerId = 'oidc.provider'; + return (rejectedPromiseAccessTokenAuth as Auth).updateProviderConfig(providerId, oidcConfigOptions) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + describe('using OIDC configurations', () => { + const providerId = 'oidc.provider'; + const configOptions = { + displayName: 'OIDC_DISPLAY_NAME', + enabled: true, + clientId: 'CLIENT_ID', + issuer: 'https://oidc.com/issuer', + }; + const serverResponse = { + name: `projects/project_id/oauthIdpConfigs/${providerId}`, + displayName: 'OIDC_DISPLAY_NAME', + enabled: true, + clientId: 'CLIENT_ID', + issuer: 'https://oidc.com/issuer', + }; + const expectedConfig = new OIDCConfig(serverResponse); + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_CONFIG); + + it('should resolve with an OIDCConfig on updateOAuthIdpConfig request success', () => { + // Stub updateOAuthIdpConfig to return expected server response. + const updateConfigStub = sinon.stub(testConfig.RequestHandler.prototype, 'updateOAuthIdpConfig') + .resolves(serverResponse); + stubs.push(updateConfigStub); + + return auth.updateProviderConfig(providerId, configOptions) + .then((actualConfig) => { + // Confirm underlying API called with expected parameters. + expect(updateConfigStub).to.have.been.calledOnce.and.calledWith(providerId, configOptions); + // Confirm expected config response returned. + expect(actualConfig).to.deep.equal(expectedConfig); + }); + }); + + it('should throw an error when updateOAuthIdpConfig returns an error', () => { + // Stub updateOAuthIdpConfig to throw a backend error. + const updateConfigStub = sinon.stub(testConfig.RequestHandler.prototype, 'updateOAuthIdpConfig') + .rejects(expectedError); + stubs.push(updateConfigStub); + + return auth.updateProviderConfig(providerId, configOptions) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(updateConfigStub).to.have.been.calledOnce.and.calledWith(providerId, configOptions); + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); + }); + + describe('using SAML configurations', () => { + const providerId = 'saml.provider'; + const configOptions = { + displayName: 'SAML_DISPLAY_NAME', + enabled: true, + idpEntityId: 'IDP_ENTITY_ID', + ssoURL: 'https://example.com/login', + x509Certificates: ['CERT1', 'CERT2'], + rpEntityId: 'RP_ENTITY_ID', + callbackURL: 'https://projectId.firebaseapp.com/__/auth/handler', + enableRequestSigning: true, + }; + const serverResponse = { + name: `projects/project_id/inboundSamlConfigs/${providerId}`, + idpConfig: { + idpEntityId: 'IDP_ENTITY_ID', + ssoUrl: 'https://example.com/login', + signRequest: true, + idpCertificates: [ + { x509Certificate: 'CERT1' }, + { x509Certificate: 'CERT2' }, + ], + }, + spConfig: { + spEntityId: 'RP_ENTITY_ID', + callbackUri: 'https://projectId.firebaseapp.com/__/auth/handler', + }, + displayName: 'SAML_DISPLAY_NAME', + enabled: true, + }; + const expectedConfig = new SAMLConfig(serverResponse); + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_CONFIG); + + it('should resolve with a SAMLConfig on updateInboundSamlConfig request success', () => { + // Stub updateInboundSamlConfig to return expected server response. + const updateConfigStub = sinon.stub(testConfig.RequestHandler.prototype, 'updateInboundSamlConfig') + .resolves(serverResponse); + stubs.push(updateConfigStub); + + return auth.updateProviderConfig(providerId, configOptions) + .then((actualConfig) => { + // Confirm underlying API called with expected parameters. + expect(updateConfigStub).to.have.been.calledOnce.and.calledWith(providerId, configOptions); + // Confirm expected config response returned. + expect(actualConfig).to.deep.equal(expectedConfig); + }); + }); + + it('should throw an error when updateInboundSamlConfig returns an error', () => { + // Stub updateInboundSamlConfig to throw a backend error. + const updateConfigStub = sinon.stub(testConfig.RequestHandler.prototype, 'updateInboundSamlConfig') + .rejects(expectedError); + stubs.push(updateConfigStub); + + return auth.updateProviderConfig(providerId, configOptions) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(updateConfigStub).to.have.been.calledOnce.and.calledWith(providerId, configOptions); + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); }); + }); }); - it('should throw when underlying revokeRefreshTokens request returns an error', () => { - // Stub revokeRefreshTokens to throw a backend error. - const revokeRefreshTokensStub = - sinon.stub(FirebaseAuthRequestHandler.prototype, 'revokeRefreshTokens') - .returns(Promise.reject(expectedError)); - stubs.push(revokeRefreshTokensStub); - return auth.revokeRefreshTokens(uid) - .then((result) => { - throw new Error('Unexpected success'); - }, (error) => { - // Confirm underlying API called with expected parameters. - expect(revokeRefreshTokensStub).to.have.been.calledOnce.and.calledWith(uid); - // Confirm expected error returned. - expect(error).to.equal(expectedError); + describe('createProviderConfig()', () => { + const oidcConfigOptions = { + providerId: 'oidc.provider', + displayName: 'OIDC_DISPLAY_NAME', + enabled: true, + clientId: 'CLIENT_ID', + issuer: 'https://oidc.com/issuer', + }; + let stubs: sinon.SinonStub[] = []; + + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + it('should be rejected given no configuration options', () => { + return (auth as any).createProviderConfig() + .should.eventually.be.rejected.and.have.property('code', 'auth/invalid-config'); + }); + + it('should be rejected given an invalid provider ID', () => { + const invalidConfigOptions = deepCopy(oidcConfigOptions); + invalidConfigOptions.providerId = 'unsupported'; + return (auth as Auth).createProviderConfig(invalidConfigOptions) + .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', () => { + return (nullAccessTokenAuth as Auth).createProviderConfig(oidcConfigOptions) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should be rejected given an app which returns invalid access tokens', () => { + return (malformedAccessTokenAuth as Auth).createProviderConfig(oidcConfigOptions) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should be rejected given an app which fails to generate access tokens', () => { + return (rejectedPromiseAccessTokenAuth as Auth).createProviderConfig(oidcConfigOptions) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + describe('using OIDC configurations', () => { + const providerId = 'oidc.provider'; + const configOptions = { + providerId, + displayName: 'OIDC_DISPLAY_NAME', + enabled: true, + clientId: 'CLIENT_ID', + issuer: 'https://oidc.com/issuer', + }; + const serverResponse = { + name: `projects/project_id/oauthIdpConfigs/${providerId}`, + displayName: 'OIDC_DISPLAY_NAME', + enabled: true, + clientId: 'CLIENT_ID', + issuer: 'https://oidc.com/issuer', + }; + const expectedConfig = new OIDCConfig(serverResponse); + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_CONFIG); + + it('should resolve with an OIDCConfig on createOAuthIdpConfig request success', () => { + // Stub createOAuthIdpConfig to return expected server response. + const createConfigStub = sinon.stub(testConfig.RequestHandler.prototype, 'createOAuthIdpConfig') + .resolves(serverResponse); + stubs.push(createConfigStub); + + return (auth as Auth).createProviderConfig(configOptions) + .then((actualConfig) => { + // Confirm underlying API called with expected parameters. + expect(createConfigStub).to.have.been.calledOnce.and.calledWith(configOptions); + // Confirm expected config response returned. + expect(actualConfig).to.deep.equal(expectedConfig); + }); + }); + + it('should throw an error when createOAuthIdpConfig returns an error', () => { + // Stub createOAuthIdpConfig to throw a backend error. + const createConfigStub = sinon.stub(testConfig.RequestHandler.prototype, 'createOAuthIdpConfig') + .rejects(expectedError); + stubs.push(createConfigStub); + + return (auth as Auth).createProviderConfig(configOptions) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(createConfigStub).to.have.been.calledOnce.and.calledWith(configOptions); + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); + }); + + describe('using SAML configurations', () => { + const providerId = 'saml.provider'; + const configOptions = { + providerId, + displayName: 'SAML_DISPLAY_NAME', + enabled: true, + idpEntityId: 'IDP_ENTITY_ID', + ssoURL: 'https://example.com/login', + x509Certificates: ['CERT1', 'CERT2'], + rpEntityId: 'RP_ENTITY_ID', + callbackURL: 'https://projectId.firebaseapp.com/__/auth/handler', + enableRequestSigning: true, + }; + const serverResponse = { + name: `projects/project_id/inboundSamlConfigs/${providerId}`, + idpConfig: { + idpEntityId: 'IDP_ENTITY_ID', + ssoUrl: 'https://example.com/login', + signRequest: true, + idpCertificates: [ + { x509Certificate: 'CERT1' }, + { x509Certificate: 'CERT2' }, + ], + }, + spConfig: { + spEntityId: 'RP_ENTITY_ID', + callbackUri: 'https://projectId.firebaseapp.com/__/auth/handler', + }, + displayName: 'SAML_DISPLAY_NAME', + enabled: true, + }; + const expectedConfig = new SAMLConfig(serverResponse); + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_CONFIG); + + it('should resolve with a SAMLConfig on createInboundSamlConfig request success', () => { + // Stub createInboundSamlConfig to return expected server response. + const createConfigStub = sinon.stub(testConfig.RequestHandler.prototype, 'createInboundSamlConfig') + .resolves(serverResponse); + stubs.push(createConfigStub); + + return (auth as Auth).createProviderConfig(configOptions) + .then((actualConfig) => { + // Confirm underlying API called with expected parameters. + expect(createConfigStub).to.have.been.calledOnce.and.calledWith(configOptions); + // Confirm expected config response returned. + expect(actualConfig).to.deep.equal(expectedConfig); + }); + }); + + it('should throw an error when createInboundSamlConfig returns an error', () => { + // Stub createInboundSamlConfig to throw a backend error. + const createConfigStub = sinon.stub(testConfig.RequestHandler.prototype, 'createInboundSamlConfig') + .rejects(expectedError); + stubs.push(createConfigStub); + + return (auth as Auth).createProviderConfig(configOptions) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(createConfigStub).to.have.been.calledOnce.and.calledWith(configOptions); + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); }); + }); }); - }); - describe('INTERNAL.delete()', () => { - it('should delete Auth instance', () => { - auth.INTERNAL.delete().should.eventually.be.fulfilled; + 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 = '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 () => { + 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, undefined as any, { algorithms: ['none'] }); + }); + + 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', + subject: uid, + header: {}, + }, { + iat: oneSecBeforeValidSince, + auth_time: oneSecBeforeValidSince, + }, 'secret'); + + // 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('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, + }, 'secret'); + + // 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' + }, undefined, 'secret'); + + 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); + + // 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/credential.spec.ts b/test/unit/auth/credential.spec.ts deleted file mode 100644 index a3535143f0..0000000000 --- a/test/unit/auth/credential.spec.ts +++ /dev/null @@ -1,367 +0,0 @@ -/*! - * 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'; - -// Use untyped import syntax for Node built-ins -import fs = require('fs'); -import path = require('path'); -import http = require('http'); -import stream = require('stream'); - -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 utils from '../utils'; -import * as mocks from '../../resources/mocks'; - -import { - ApplicationDefaultCredential, CertCredential, Certificate, GoogleOAuthAccessToken, - MetadataServiceCredential, RefreshToken, RefreshTokenCredential, -} from '../../../src/auth/credential'; - -chai.should(); -chai.use(sinonChai); -chai.use(chaiAsPromised); - -const expect = chai.expect; - -let TEST_GCLOUD_CREDENTIALS; -const GCLOUD_CREDENTIAL_SUFFIX = 'gcloud/application_default_credentials.json'; -const GCLOUD_CREDENTIAL_PATH = path.resolve(process.env.HOME, '.config', GCLOUD_CREDENTIAL_SUFFIX); -try { - TEST_GCLOUD_CREDENTIALS = JSON.parse(fs.readFileSync(GCLOUD_CREDENTIAL_PATH).toString()); -} catch (error) { - // tslint:disable-next-line:no-console - console.log( - 'WARNING: gcloud credentials not found. Run `gcloud beta auth application-default login`. ' + - 'Relevant tests will be skipped.', - ); -} - -/** - * Logs a warning and returns true if no gcloud credentials are found, meaning the test which calls - * this will be skipped. - * - * The only thing that should ever skip these tests is continuous integration. When developing - * locally, these tests should be run. - * - * @return {boolean} Whether or not the caller should skip the current test. - */ -const skipAndLogWarningIfNoGcloud = () => { - if (typeof TEST_GCLOUD_CREDENTIALS === 'undefined') { - // tslint:disable-next-line:no-console - console.log( - 'WARNING: Test being skipped because gcloud credentials not found. Run `gcloud beta auth ' + - 'application-default login`.', - ); - - return true; - } - - return false; -}; - -const ONE_HOUR_IN_SECONDS = 60 * 60; -const FIVE_MINUTES_IN_SECONDS = 5 * 60; - - -describe('Credential', () => { - let mockedRequests: nock.Scope[] = []; - let mockCertificateObject; - let oldProcessEnv: NodeJS.ProcessEnv; - - before(() => utils.mockFetchAccessTokenRequests()); - - after(() => nock.cleanAll()); - - beforeEach(() => { - mockCertificateObject = _.clone(mocks.certificateObject); - oldProcessEnv = process.env; - }); - - afterEach(() => { - _.forEach(mockedRequests, (mockedRequest) => mockedRequest.done()); - mockedRequests = []; - process.env = oldProcessEnv; - }); - - - describe('Certificate', () => { - describe('fromPath', () => { - const invalidFilePaths = [null, NaN, 0, 1, true, false, {}, [], { a: 1 }, [1, 'a'], _.noop]; - invalidFilePaths.forEach((invalidFilePath) => { - it('should throw if called with non-string argument: ' + JSON.stringify(invalidFilePath), () => { - expect(() => { - Certificate.fromPath(invalidFilePath as any); - }).to.throw('Failed to parse certificate key file: TypeError: path must be a string'); - }); - }); - - it('should throw if called with no argument', () => { - expect(() => { - (Certificate as any).fromPath(); - }).to.throw('Failed to parse certificate key file: TypeError: path must be a string'); - }); - - it('should throw if called with the path to a non-existent file', () => { - expect(() => Certificate.fromPath('invalid-file')) - .to.throw('Failed to parse certificate key file: Error: ENOENT: no such file or directory'); - }); - - it('should throw if called with the path to an invalid file', () => { - const invalidPath = path.resolve(__dirname, '../../resources/unparesable.json'); - expect(() => Certificate.fromPath(invalidPath)) - .to.throw('Failed to parse certificate key file: Error: ENOENT: no such file or directory'); - }); - - it('should throw if called with an empty string path', () => { - expect(() => Certificate.fromPath('')) - .to.throw('Failed to parse certificate key file: Error: ENOENT: no such file or directory'); - }); - - it('should not throw given a valid path to a key file', () => { - const validPath = path.resolve(__dirname, '../../resources/mock.key.json'); - expect(() => Certificate.fromPath(validPath)).not.to.throw(); - }); - }); - - describe('constructor', () => { - const invalidCertificateObjects = [null, NaN, 0, 1, true, false, _.noop]; - invalidCertificateObjects.forEach((invalidCertificateObject) => { - it('should throw if called with non-object argument: ' + JSON.stringify(invalidCertificateObject), () => { - expect(() => { - return new Certificate(invalidCertificateObject as any); - }).to.throw('Certificate object must be an object.'); - }); - }); - - it('should throw if called with no argument', () => { - expect(() => { - return new (Certificate as any)(); - }).to.throw('Certificate object must be an object.'); - }); - - it('should throw if certificate object does not contain a valid "client_email"', () => { - mockCertificateObject.client_email = ''; - - expect(() => { - return new Certificate(mockCertificateObject); - }).to.throw('Certificate object must contain a string "client_email" property'); - - delete mockCertificateObject.client_email; - - expect(() => { - return new Certificate(mockCertificateObject); - }).to.throw('Certificate object must contain a string "client_email" property'); - }); - - it('should throw if certificate object does not contain a "private_key"', () => { - mockCertificateObject.private_key = ''; - - expect(() => { - return new Certificate(mockCertificateObject); - }).to.throw('Certificate object must contain a string "private_key" property'); - - delete mockCertificateObject.private_key; - - expect(() => { - return new Certificate(mockCertificateObject); - }).to.throw('Certificate object must contain a string "private_key" property'); - }); - - it('should throw if certificate object does not contain a valid "private_key"', () => { - mockCertificateObject.private_key = 'invalid.key'; - - expect(() => { - return new Certificate(mockCertificateObject); - }).to.throw('Failed to parse private key: Error: Invalid PEM formatted message.'); - }); - - it('should not throw given a valid certificate object', () => { - expect(() => { - return new Certificate(mockCertificateObject); - }).not.to.throw(); - }); - - it('should accept "clientEmail" in place of "client_email" for the certificate object', () => { - mockCertificateObject.clientEmail = mockCertificateObject.client_email; - delete mockCertificateObject.client_email; - - expect(() => { - return new Certificate(mockCertificateObject); - }).not.to.throw(); - }); - - it('should accept "privateKey" in place of "private_key" for the certificate object', () => { - mockCertificateObject.privateKey = mockCertificateObject.private_key; - delete mockCertificateObject.private_key; - - expect(() => { - return new Certificate(mockCertificateObject); - }).not.to.throw(); - }); - }); - }); - - describe('CertCredential', () => { - it('should return a Credential', () => { - const c = new CertCredential(mockCertificateObject); - expect(c.getCertificate()).to.deep.equal({ - projectId: mockCertificateObject.project_id, - clientEmail: mockCertificateObject.client_email, - privateKey: mockCertificateObject.private_key, - }); - }); - - it('should create access tokens', () => { - const c = new CertCredential(mockCertificateObject); - return c.getAccessToken().then((token) => { - expect(token.access_token).to.be.a('string').and.to.not.be.empty; - expect(token.expires_in).to.equal(ONE_HOUR_IN_SECONDS); - }); - }); - }); - - describe('RefreshTokenCredential', () => { - it('should not return a certificate', () => { - if (skipAndLogWarningIfNoGcloud()) { - return; - } - - const c = new RefreshTokenCredential(TEST_GCLOUD_CREDENTIALS); - expect(c.getCertificate()).to.be.null; - }); - - it('should create access tokens', () => { - if (skipAndLogWarningIfNoGcloud()) { - return; - } - - const c = new RefreshTokenCredential(TEST_GCLOUD_CREDENTIALS); - return c.getAccessToken().then((token) => { - expect(token.access_token).to.be.a('string').and.to.not.be.empty; - expect(token.expires_in).to.greaterThan(FIVE_MINUTES_IN_SECONDS); - }); - }); - }); - - describe('MetadataServiceCredential', () => { - let httpStub; - before(() => httpStub = sinon.stub(http, 'request')); - after(() => httpStub.restore()); - - it('should not return a certificate', () => { - const c = new MetadataServiceCredential(); - expect(c.getCertificate()).to.be.null; - }); - - it('should create access tokens', () => { - const expected: GoogleOAuthAccessToken = { - access_token: 'anAccessToken', - expires_in: 42, - }; - const response = new stream.PassThrough(); - response.write(JSON.stringify(expected)); - response.end(); - - const request = new stream.PassThrough(); - - httpStub.callsArgWith(1, response) - .returns(request); - - const c = new MetadataServiceCredential(); - return c.getAccessToken().then((token) => { - expect(token.access_token).to.equal('anAccessToken'); - expect(token.expires_in).to.equal(42); - }); - }); - }); - - describe('ApplicationDefaultCredential', () => { - let credPath: string; - let fsStub; - - beforeEach(() => credPath = process.env.GOOGLE_APPLICATION_CREDENTIALS); - - afterEach(() => { - if (fsStub) { - fsStub.restore(); - } - process.env.GOOGLE_APPLICATION_CREDENTIALS = this.credPath; - }); - - it('should return a CertCredential with GOOGLE_APPLICATION_CREDENTIALS set', () => { - process.env.GOOGLE_APPLICATION_CREDENTIALS = path.resolve(__dirname, '../../resources/mock.key.json'); - const c = new ApplicationDefaultCredential(); - expect(c.getCredential()).to.be.an.instanceof(CertCredential); - }); - - it('should throw if explicitly pointing to an invalid path', () => { - process.env.GOOGLE_APPLICATION_CREDENTIALS = 'invalidpath'; - expect(() => new ApplicationDefaultCredential()).to.throw(Error); - }); - - it('should throw if explicitly pointing to an invalid cert file', () => { - fsStub = sinon.stub(fs, 'readFileSync').returns('invalidjson'); - expect(() => new ApplicationDefaultCredential()).to.throw(Error); - }); - - it('should return a RefreshTokenCredential with gcloud login', () => { - if (skipAndLogWarningIfNoGcloud()) { - return; - } - - delete process.env.GOOGLE_APPLICATION_CREDENTIALS; - expect((new ApplicationDefaultCredential()).getCredential()).to.be.an.instanceof(RefreshTokenCredential); - }); - - it('should throw if a the gcloud login cache is invalid', () => { - delete process.env.GOOGLE_APPLICATION_CREDENTIALS; - fsStub = sinon.stub(fs, 'readFileSync').returns('invalidjson'); - expect(() => new ApplicationDefaultCredential()).to.throw(Error); - }); - - it('should return a MetadataServiceCredential as a last resort', () => { - delete process.env.GOOGLE_APPLICATION_CREDENTIALS; - fsStub = sinon.stub(fs, 'readFileSync').throws(new Error('no gcloud credential file')); - expect((new ApplicationDefaultCredential()).getCredential()).to.be.an.instanceof(MetadataServiceCredential); - }); - - it('should create access tokens', () => { - process.env.GOOGLE_APPLICATION_CREDENTIALS = path.resolve(__dirname, '../../resources/mock.key.json'); - const c = new ApplicationDefaultCredential(); - return c.getAccessToken().then((token) => { - expect(token.access_token).to.be.a('string').and.to.not.be.empty; - expect(token.expires_in).to.equal(ONE_HOUR_IN_SECONDS); - }); - }); - - it('should return a Credential', () => { - process.env.GOOGLE_APPLICATION_CREDENTIALS = path.resolve(__dirname, '../../resources/mock.key.json'); - const c = new ApplicationDefaultCredential(); - expect(c.getCertificate()).to.deep.equal({ - projectId: mockCertificateObject.project_id, - clientEmail: mockCertificateObject.client_email, - privateKey: mockCertificateObject.private_key, - }); - }); - }); -}); diff --git a/test/unit/auth/index.spec.ts b/test/unit/auth/index.spec.ts new file mode 100644 index 0000000000..099bb00cd4 --- /dev/null +++ b/test/unit/auth/index.spec.ts @@ -0,0 +1,75 @@ +/*! + * @license + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +import * as chai from 'chai'; +import * as sinonChai from 'sinon-chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +import * as mocks from '../../resources/mocks'; +import { App } from '../../../src/app/index'; +import { getAuth, Auth } from '../../../src/auth/index'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +describe('Auth', () => { + let mockApp: App; + let mockCredentialApp: App; + + const noProjectIdError = 'Failed to determine project ID for Auth. Initialize the SDK ' + + 'with service account credentials or set project ID as an app option. Alternatively set the ' + + 'GOOGLE_CLOUD_PROJECT environment variable.'; + + beforeEach(() => { + mockApp = mocks.app(); + mockCredentialApp = mocks.mockCredentialApp(); + }); + + describe('getAuth()', () => { + it('should throw when default app is not available', () => { + expect(() => { + return getAuth(); + }).to.throw('The default Firebase app does not exist.'); + }); + + it('should reject given an invalid credential without project ID', () => { + // Project ID not set in the environment. + delete process.env.GOOGLE_CLOUD_PROJECT; + delete process.env.GCLOUD_PROJECT; + const auth = getAuth(mockCredentialApp); + return auth.getUser('uid') + .should.eventually.rejectedWith(noProjectIdError); + }); + + it('should not throw given a valid app', () => { + expect(() => { + return getAuth(mockApp); + }).not.to.throw(); + }); + + it('should return the same instance for a given app instance', () => { + const auth1: Auth = getAuth(mockApp); + const auth2: Auth = getAuth(mockApp); + expect(auth1).to.equal(auth2); + }); + }); +}); diff --git a/test/unit/auth/project-config-manager.spec.ts b/test/unit/auth/project-config-manager.spec.ts new file mode 100644 index 0000000000..3fc0770b36 --- /dev/null +++ b/test/unit/auth/project-config-manager.spec.ts @@ -0,0 +1,214 @@ +/*! + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +import * as _ from 'lodash'; +import * as chai from 'chai'; +import * as sinon from 'sinon'; +import * as sinonChai from 'sinon-chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +import * as mocks from '../../resources/mocks'; +import { FirebaseApp } from '../../../src/app/firebase-app'; +import { AuthRequestHandler } from '../../../src/auth/auth-api-request'; +import { AuthClientErrorCode, FirebaseAuthError } from '../../../src/utils/error'; +import { ProjectConfigManager } from '../../../src/auth/project-config-manager'; +import { + ProjectConfig, + ProjectConfigServerResponse, + UpdateProjectConfigRequest +} from '../../../src/auth/project-config'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +describe('ProjectConfigManager', () => { + let mockApp: FirebaseApp; + let projectConfigManager: ProjectConfigManager; + let nullAccessTokenProjectConfigManager: ProjectConfigManager; + let malformedAccessTokenProjectConfigManager: ProjectConfigManager; + let rejectedPromiseAccessTokenProjectConfigManager: ProjectConfigManager; + const GET_CONFIG_RESPONSE: ProjectConfigServerResponse = { + smsRegionConfig: { + allowlistOnly: { + allowedRegions: [ 'AC', 'AD' ], + }, + }, + recaptchaConfig: { + emailPasswordEnforcementState: 'AUDIT', + managedRules: [ { + endScore: 0.2, + action: 'BLOCK' + } ], + recaptchaKeys: [ { + type: 'WEB', + key: 'test-key-1' } + ], + } + }; + + before(() => { + mockApp = mocks.app(); + projectConfigManager = new ProjectConfigManager(mockApp); + nullAccessTokenProjectConfigManager = new ProjectConfigManager( + mocks.appReturningNullAccessToken()); + malformedAccessTokenProjectConfigManager = new ProjectConfigManager( + mocks.appReturningMalformedAccessToken()); + rejectedPromiseAccessTokenProjectConfigManager = new ProjectConfigManager( + mocks.appRejectedWhileFetchingAccessToken()); + }); + + after(() => { + return mockApp.delete(); + }); + + describe('getProjectConfig()', () => { + const expectedProjectConfig = new ProjectConfig(GET_CONFIG_RESPONSE); + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_CONFIG); + // Stubs used to simulate underlying API calls. + let stubs: sinon.SinonStub[] = []; + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + it('should be rejected given an app which returns null access tokens', () => { + return nullAccessTokenProjectConfigManager.getProjectConfig() + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should be rejected given an app which returns invalid access tokens', () => { + return malformedAccessTokenProjectConfigManager.getProjectConfig() + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should be rejected given an app which fails to generate access tokens', () => { + return rejectedPromiseAccessTokenProjectConfigManager.getProjectConfig() + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should resolve with a Project Config on success', () => { + // Stub getProjectConfig to return expected result. + const stub = sinon.stub(AuthRequestHandler.prototype, 'getProjectConfig') + .returns(Promise.resolve(GET_CONFIG_RESPONSE)); + stubs.push(stub); + return projectConfigManager.getProjectConfig() + .then((result) => { + // Confirm underlying API called with expected parameters. + expect(stub).to.have.been.calledOnce; + // Confirm expected project config returned. + expect(result).to.deep.equal(expectedProjectConfig); + }); + }); + + it('should throw an error when the backend returns an error', () => { + // Stub getConfig to throw a backend error. + const stub = sinon.stub(AuthRequestHandler.prototype, 'getProjectConfig') + .returns(Promise.reject(expectedError)); + stubs.push(stub); + return projectConfigManager.getProjectConfig() + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(stub).to.have.been.calledOnce; + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); + }); + + describe('updateProjectConfig()', () => { + const projectConfigOptions: UpdateProjectConfigRequest = { + smsRegionConfig: { + allowByDefault: { + disallowedRegions: [ 'AC', 'AD' ], + }, + }, + recaptchaConfig: { + emailPasswordEnforcementState: 'AUDIT', + managedRules: [ { + endScore: 0.2, + action: 'BLOCK' + } ], + } + }; + const expectedProjectConfig = new ProjectConfig(GET_CONFIG_RESPONSE); + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'Unable to update the config provided.'); + // Stubs used to simulate underlying API calls. + let stubs: sinon.SinonStub[] = []; + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + it('should be rejected given no projectConfigOptions', () => { + return (projectConfigManager as any).updateProjectConfig(null as unknown as UpdateProjectConfigRequest) + .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); + }); + + it('should be rejected given an app which returns null access tokens', () => { + return nullAccessTokenProjectConfigManager.updateProjectConfig(projectConfigOptions) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should be rejected given an app which returns invalid access tokens', () => { + return malformedAccessTokenProjectConfigManager.updateProjectConfig(projectConfigOptions) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should be rejected given an app which fails to generate access tokens', () => { + return rejectedPromiseAccessTokenProjectConfigManager.updateProjectConfig(projectConfigOptions) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should resolve with a ProjectConfig on updateProjectConfig request success', () => { + // Stub updateProjectConfig to return expected result. + const updateConfigStub = sinon.stub(AuthRequestHandler.prototype, 'updateProjectConfig') + .returns(Promise.resolve(GET_CONFIG_RESPONSE)); + stubs.push(updateConfigStub); + return projectConfigManager.updateProjectConfig(projectConfigOptions) + .then((actualProjectConfig) => { + // Confirm underlying API called with expected parameters. + expect(updateConfigStub).to.have.been.calledOnce.and.calledWith(projectConfigOptions); + // Confirm expected Project Config object returned. + expect(actualProjectConfig).to.deep.equal(expectedProjectConfig); + }); + }); + + it('should throw an error when updateProjectConfig returns an error', () => { + // Stub updateProjectConfig to throw a backend error. + const updateConfigStub = sinon.stub(AuthRequestHandler.prototype, 'updateProjectConfig') + .returns(Promise.reject(expectedError)); + stubs.push(updateConfigStub); + return projectConfigManager.updateProjectConfig(projectConfigOptions) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(updateConfigStub).to.have.been.calledOnce.and.calledWith(projectConfigOptions); + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); + }); +}); diff --git a/test/unit/auth/project-config.spec.ts b/test/unit/auth/project-config.spec.ts new file mode 100644 index 0000000000..5934dd15fd --- /dev/null +++ b/test/unit/auth/project-config.spec.ts @@ -0,0 +1,576 @@ +/*! + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as _ from 'lodash'; +import * as chai from 'chai'; +import * as sinonChai from 'sinon-chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +import { deepCopy } from '../../../src/utils/deep-copy'; +import { RecaptchaAuthConfig } from '../../../src/auth/auth-config'; +import { + ProjectConfig, + ProjectConfigServerResponse, + UpdateProjectConfigRequest, +} from '../../../src/auth/project-config'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +describe('ProjectConfig', () => { + const serverResponse: ProjectConfigServerResponse = { + smsRegionConfig: { + allowByDefault: { + disallowedRegions: [ 'AC', 'AD' ], + }, + }, + mfa: { + state: 'DISABLED', + providerConfigs: [ + { + state: 'ENABLED', + totpProviderConfig: { + adjacentIntervals: 5, + }, + }, + ], + }, + passwordPolicyConfig: { + passwordPolicyEnforcementState: 'ENFORCE', + forceUpgradeOnSignin: true, + passwordPolicyVersions: [ + { + customStrengthOptions: { + containsLowercaseCharacter: true, + containsNonAlphanumericCharacter: true, + containsNumericCharacter: true, + containsUppercaseCharacter: true, + minPasswordLength: 8, + maxPasswordLength: 30, + }, + }, + ], + }, + emailPrivacyConfig: { + enableImprovedEmailPrivacy: true, + }, + }; + + const updateProjectConfigRequest1: UpdateProjectConfigRequest = { + smsRegionConfig: { + allowByDefault: { + disallowedRegions: [ 'AC', 'AD' ], + }, + }, + passwordPolicyConfig: { + enforcementState: 'ENFORCE', + forceUpgradeOnSignin: true, + constraints: { + requireLowercase: true, + requireNonAlphanumeric: true, + requireNumeric: true, + requireUppercase: true, + minLength: 8, + maxLength: 30, + }, + }, + emailPrivacyConfig: { + enableImprovedEmailPrivacy: false, + }, + }; + + const updateProjectConfigRequest2: UpdateProjectConfigRequest = { + smsRegionConfig: { + allowlistOnly: { + allowedRegions: [ 'AC', 'AD' ], + }, + }, + }; + + const updateProjectConfigRequest3: any = { + smsRegionConfig: { + allowlistOnly: { + allowedRegions: [ 'AC', 'AD' ], + }, + allowByDefault: { + disallowedRegions: ['AC', 'AD'], + }, + }, + recaptchaConfig: { + emailPasswordEnforcementState: 'AUDIT', + managedRules: [ { + endScore: 0.2, + action: 'BLOCK' + } ], + recaptchaKeys: [ { + type: 'WEB', + key: 'test-key-1' } + ], + useAccountDefender: true, + } + }; + + const updateProjectConfigRequest: UpdateProjectConfigRequest = { + recaptchaConfig: { + emailPasswordEnforcementState: 'AUDIT', + managedRules: [ { + endScore: 0.2, + action: 'BLOCK' + } ], + useAccountDefender: true, + } + }; + + describe('buildServerRequest()', () => { + + describe('for an update request', () => { + it('should throw on null SmsRegionConfig attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + configOptionsClientRequest.smsRegionConfig = null; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"SmsRegionConfig" must be a non-null object.'); + }); + + it('should throw on invalid SmsRegionConfig attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + configOptionsClientRequest.smsRegionConfig.invalidParameter = 'invalid'; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"invalidParameter" is not a valid SmsRegionConfig parameter.'); + }); + + it('should throw on invalid allowlistOnly attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest2) as any; + configOptionsClientRequest.smsRegionConfig.allowlistOnly.disallowedRegions = [ 'AC', 'AD' ]; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"disallowedRegions" is not a valid SmsRegionConfig.allowlistOnly parameter.'); + }); + + it('should throw on invalid allowByDefault attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + configOptionsClientRequest.smsRegionConfig.allowByDefault.allowedRegions = [ 'AC', 'AD' ]; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"allowedRegions" is not a valid SmsRegionConfig.allowByDefault parameter.'); + }); + + it('should throw on non-array disallowedRegions attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + configOptionsClientRequest.smsRegionConfig.allowByDefault.disallowedRegions = 'non-array'; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"SmsRegionConfig.allowByDefault.disallowedRegions" must be a valid string array.'); + }); + + it('should throw on non-array allowedRegions attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest2) as any; + configOptionsClientRequest.smsRegionConfig.allowlistOnly.allowedRegions = 'non-array'; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"SmsRegionConfig.allowlistOnly.allowedRegions" must be a valid string array.'); + }); + + it('should throw when both allowlistOnly and allowByDefault attributes are presented', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest3) as any; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('SmsRegionConfig cannot have both "allowByDefault" and "allowlistOnly" parameters.'); + }); + + it('should not throw on valid client request object', () => { + const configOptionsClientRequest1 = deepCopy(updateProjectConfigRequest1); + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest1); + }).not.to.throw; + const configOptionsClientRequest2 = deepCopy(updateProjectConfigRequest2); + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest2); + }).not.to.throw; + }); + it('should throw on null RecaptchaConfig attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any; + configOptionsClientRequest.recaptchaConfig = null; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"RecaptchaConfig" must be a non-null object.'); + }); + + it('should throw on invalid RecaptchaConfig attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any; + configOptionsClientRequest.recaptchaConfig.invalidParameter = 'invalid'; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"invalidParameter" is not a valid RecaptchaConfig parameter.'); + }); + + it('should throw on null emailPasswordEnforcementState attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any; + configOptionsClientRequest.recaptchaConfig.emailPasswordEnforcementState = null; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"RecaptchaConfig.emailPasswordEnforcementState" must be a valid non-empty string.'); + }); + + it('should throw on invalid emailPasswordEnforcementState attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any; + configOptionsClientRequest.recaptchaConfig + .emailPasswordEnforcementState = 'INVALID'; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"RecaptchaConfig.emailPasswordEnforcementState" must be either "OFF", "AUDIT" or "ENFORCE".'); + }); + + const invalidUseAccountDefender = [null, NaN, 0, 1, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidUseAccountDefender.forEach((useAccountDefender) => { + it(`should throw given invalid useAccountDefender parameter: ${JSON.stringify(useAccountDefender)}`, () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any; + configOptionsClientRequest.recaptchaConfig.useAccountDefender = useAccountDefender; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"RecaptchaConfig.useAccountDefender" must be a boolean value".'); + }); + }); + + it('should throw on non-array managedRules attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any; + configOptionsClientRequest.recaptchaConfig.managedRules = 'non-array'; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"RecaptchaConfig.managedRules" must be an array of valid "RecaptchaManagedRule".'); + }); + + it('should throw on invalid managedRules attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any; + configOptionsClientRequest.recaptchaConfig.managedRules = + [{ 'score': 0.1, 'action': 'BLOCK' }]; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"score" is not a valid RecaptchaManagedRule parameter.'); + }); + + it('should throw on invalid RecaptchaManagedRule.action attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any; + configOptionsClientRequest.recaptchaConfig.managedRules = + [{ 'endScore': 0.1, 'action': 'ALLOW' }]; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"RecaptchaManagedRule.action" must be "BLOCK".'); + }); + + it('should throw on null PasswordPolicyConfig attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + configOptionsClientRequest.passwordPolicyConfig = null; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"PasswordPolicyConfig" must be a non-null object.'); + }); + + it('should throw on invalid PasswordPolicyConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + tenantOptionsClientRequest.passwordPolicyConfig.invalidParameter = 'invalid', + expect(() => { + ProjectConfig.buildServerRequest(tenantOptionsClientRequest); + }).to.throw('"invalidParameter" is not a valid PasswordPolicyConfig parameter.'); + }); + + it('should throw on missing enforcementState', () => { + const tenantOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + delete tenantOptionsClientRequest.passwordPolicyConfig.enforcementState; + expect(() => { + ProjectConfig.buildServerRequest(tenantOptionsClientRequest); + }).to.throw('"PasswordPolicyConfig.enforcementState" must be either "ENFORCE" or "OFF".'); + }); + + it('should throw on invalid enforcementState', () => { + const tenantOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + tenantOptionsClientRequest.passwordPolicyConfig.enforcementState = 'INVALID_STATE'; + expect(() => { + ProjectConfig.buildServerRequest(tenantOptionsClientRequest); + }).to.throw('"PasswordPolicyConfig.enforcementState" must be either "ENFORCE" or "OFF".'); + }); + + it('should throw on invalid forceUpgradeOnSignin', () => { + const tenantOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + tenantOptionsClientRequest.passwordPolicyConfig.forceUpgradeOnSignin = 'INVALID'; + expect(() => { + ProjectConfig.buildServerRequest(tenantOptionsClientRequest); + }).to.throw('"PasswordPolicyConfig.forceUpgradeOnSignin" must be a boolean.'); + }); + + it('should throw on undefined constraints when state is enforced', () => { + const tenantOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + delete tenantOptionsClientRequest.passwordPolicyConfig.constraints; + expect(() => { + ProjectConfig.buildServerRequest(tenantOptionsClientRequest); + }).to.throw('"PasswordPolicyConfig.constraints" must be defined.'); + }); + + it('should throw on invalid constraints attribute', ()=> { + const tenantOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + tenantOptionsClientRequest.passwordPolicyConfig.constraints.invalidParameter = 'invalid'; + expect(() => { + ProjectConfig.buildServerRequest(tenantOptionsClientRequest); + }).to.throw('"invalidParameter" is not a valid PasswordPolicyConfig.constraints parameter.'); + }); + + it('should throw on null constraints object', ()=> { + const tenantOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + tenantOptionsClientRequest.passwordPolicyConfig.constraints = null; + expect(() => { + ProjectConfig.buildServerRequest(tenantOptionsClientRequest); + }).to.throw('"PasswordPolicyConfig.constraints" must be a non-empty object.'); + }); + + it('should throw on invalid constraints object', () => { + const tenantOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + tenantOptionsClientRequest.passwordPolicyConfig.constraints = 'invalid'; + expect(() => { + ProjectConfig.buildServerRequest(tenantOptionsClientRequest); + }).to.throw('"PasswordPolicyConfig.constraints" must be a non-empty object.'); + }); + + it('should throw on invalid uppercase type', () => { + const tenantOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + tenantOptionsClientRequest.passwordPolicyConfig.constraints.requireUppercase = 'invalid'; + expect(() => { + ProjectConfig.buildServerRequest(tenantOptionsClientRequest); + }).to.throw('"PasswordPolicyConfig.constraints.requireUppercase"' + + ' must be a boolean.'); + }); + + it('should throw on invalid lowercase type', () => { + const tenantOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + tenantOptionsClientRequest.passwordPolicyConfig.constraints.requireLowercase = 'invalid'; + expect(() => { + ProjectConfig.buildServerRequest(tenantOptionsClientRequest); + }).to.throw('"PasswordPolicyConfig.constraints.requireLowercase"' + + ' must be a boolean.'); + }); + + it('should throw on invalid numeric type', () => { + const tenantOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + tenantOptionsClientRequest.passwordPolicyConfig.constraints.requireNumeric = 'invalid'; + expect(() => { + ProjectConfig.buildServerRequest(tenantOptionsClientRequest); + }).to.throw('"PasswordPolicyConfig.constraints.requireNumeric"' + + ' must be a boolean.'); + }); + + it('should throw on invalid non-alphanumeric type', () => { + const tenantOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + tenantOptionsClientRequest.passwordPolicyConfig.constraints.requireNonAlphanumeric = 'invalid'; + expect(() => { + ProjectConfig.buildServerRequest(tenantOptionsClientRequest); + }).to.throw('"PasswordPolicyConfig.constraints.requireNonAlphanumeric"' + + ' must be a boolean.'); + }); + + it('should throw on invalid minLength type', () => { + const tenantOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + tenantOptionsClientRequest.passwordPolicyConfig.constraints.minLength = 'invalid'; + expect(() => { + ProjectConfig.buildServerRequest(tenantOptionsClientRequest); + }).to.throw('"PasswordPolicyConfig.constraints.minLength" must be a number.'); + }); + + it('should throw on invalid maxLength type', () => { + const tenantOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + tenantOptionsClientRequest.passwordPolicyConfig.constraints.maxLength = 'invalid'; + expect(() => { + ProjectConfig.buildServerRequest(tenantOptionsClientRequest); + }).to.throw('"PasswordPolicyConfig.constraints.maxLength" must be a number.'); + }); + + it('should throw on invalid minLength range', () => { + const tenantOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + tenantOptionsClientRequest.passwordPolicyConfig.constraints.minLength = 45; + expect(() => { + ProjectConfig.buildServerRequest(tenantOptionsClientRequest); + }).to.throw('"PasswordPolicyConfig.constraints.minLength"' + + ' must be an integer between 6 and 30, inclusive.'); + }); + + it('should throw on invalid maxLength range', () => { + const tenantOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + tenantOptionsClientRequest.passwordPolicyConfig.constraints.maxLength = 5000; + expect(() => { + ProjectConfig.buildServerRequest(tenantOptionsClientRequest); + }).to.throw('"PasswordPolicyConfig.constraints.maxLength"' + + ' must be greater than or equal to minLength and at max 4096.'); + }); + + it('should throw if minLength is greater than maxLength', () => { + const tenantOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + tenantOptionsClientRequest.passwordPolicyConfig.constraints.minLength = 20; + tenantOptionsClientRequest.passwordPolicyConfig.constraints.maxLength = 7; + expect(() => { + ProjectConfig.buildServerRequest(tenantOptionsClientRequest); + }).to.throw('"PasswordPolicyConfig.constraints.maxLength"' + + ' must be greater than or equal to minLength and at max 4096.'); + }); + + it('should throw on null EmailPrivacyConfig attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + configOptionsClientRequest.emailPrivacyConfig = null; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"EmailPrivacyConfig" must be a non-null object.'); + }); + + it('should throw on invalid EmailPrivacyConfig attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + configOptionsClientRequest.emailPrivacyConfig.invalidParameter = 'invalid'; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"invalidParameter" is not a valid "EmailPrivacyConfig" parameter.'); + }); + + it('should throw on invalid enableImprovedEmailPrivacy attribute', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest1) as any; + configOptionsClientRequest.emailPrivacyConfig.enableImprovedEmailPrivacy = []; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"EmailPrivacyConfig.enableImprovedEmailPrivacy" must be a valid boolean value.'); + }); + + const nonObjects = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; + nonObjects.forEach((request) => { + it('should throw on invalid UpdateProjectConfigRequest:' + JSON.stringify(request), () => { + expect(() => { + ProjectConfig.buildServerRequest(request as any); + }).to.throw('"UpdateProjectConfigRequest" must be a valid non-null object.'); + }); + }); + + it('should throw on unsupported attribute for update request', () => { + const configOptionsClientRequest = deepCopy(updateProjectConfigRequest) as any; + configOptionsClientRequest.unsupported = 'value'; + expect(() => { + ProjectConfig.buildServerRequest(configOptionsClientRequest); + }).to.throw('"unsupported" is not a valid UpdateProjectConfigRequest parameter.'); + }); + }); + }); + + describe('constructor', () => { + const serverResponseCopy: ProjectConfigServerResponse = deepCopy(serverResponse); + const projectConfig = new ProjectConfig(serverResponseCopy); + + it('should not throw on valid initialization', () => { + expect(() => new ProjectConfig(serverResponse)).not.to.throw(); + }); + + it('should set readonly property smsRegionConfig', () => { + const expectedSmsRegionConfig = { + allowByDefault: { + disallowedRegions: [ 'AC', 'AD' ], + }, + }; + expect(projectConfig.smsRegionConfig).to.deep.equal(expectedSmsRegionConfig); + }); + + it('should set readonly property multiFactorConfig', () => { + const expectedMultiFactorConfig = { + state: 'DISABLED', + providerConfigs: [ + { + state: 'ENABLED', + totpProviderConfig: { + adjacentIntervals: 5, + }, + }, + ], + }; + expect(projectConfig.multiFactorConfig).to.deep.equal(expectedMultiFactorConfig); + }); + + it('should set readonly property recaptchaConfig', () => { + const expectedRecaptchaConfig = new RecaptchaAuthConfig( + { + emailPasswordEnforcementState: 'AUDIT', + managedRules: [ { + endScore: 0.2, + action: 'BLOCK' + } ], + recaptchaKeys: [ { + type: 'WEB', + key: 'test-key-1' } + ], + useAccountDefender: true, + } + ); + expect(projectConfig.recaptchaConfig).to.deep.equal(expectedRecaptchaConfig); + }); + + it('should set readonly property passwordPolicyConfig', () => { + const expectedPasswordPolicyConfig = { + enforcementState: 'ENFORCE', + forceUpgradeOnSignin: true, + constraints: { + requireLowercase: true, + requireNonAlphanumeric: true, + requireNumeric: true, + requireUppercase: true, + minLength: 8, + maxLength: 30, + }, + }; + expect(projectConfig.passwordPolicyConfig).to.deep.equal(expectedPasswordPolicyConfig); + }); + + it('should set readonly property emailPrivacyConfig', () => { + const expectedEmailPrivacyConfig = { + enableImprovedEmailPrivacy: true, + }; + expect(projectConfig.emailPrivacyConfig).to.deep.equal(expectedEmailPrivacyConfig); + }); + }); + + describe('toJSON()', () => { + const serverResponseCopy: ProjectConfigServerResponse = deepCopy(serverResponse); + it('should return the expected object representation of project config', () => { + expect(new ProjectConfig(serverResponseCopy).toJSON()).to.deep.equal({ + smsRegionConfig: deepCopy(serverResponse.smsRegionConfig), + multiFactorConfig: deepCopy(serverResponse.mfa), + recaptchaConfig: deepCopy(serverResponse.recaptchaConfig), + passwordPolicyConfig: deepCopy(serverResponse.passwordPolicyConfig), + emailPrivacyConfig: deepCopy(serverResponse.emailPrivacyConfig), + }); + }); + + it('should not populate optional fields if not available', () => { + const serverResponseOptionalCopy: ProjectConfigServerResponse = deepCopy(serverResponse); + delete serverResponseOptionalCopy.smsRegionConfig; + delete serverResponseOptionalCopy.mfa; + delete serverResponseOptionalCopy.recaptchaConfig?.emailPasswordEnforcementState; + delete serverResponseOptionalCopy.recaptchaConfig?.managedRules; + delete serverResponseOptionalCopy.recaptchaConfig?.useAccountDefender; + delete serverResponseOptionalCopy.passwordPolicyConfig; + delete serverResponseOptionalCopy.passwordPolicyConfig; + delete serverResponseOptionalCopy.emailPrivacyConfig; + expect(new ProjectConfig(serverResponseOptionalCopy).toJSON()).to.deep.equal({ + recaptchaConfig: { + recaptchaKeys: deepCopy(serverResponse.recaptchaConfig?.recaptchaKeys), + } + }); + }); + }); +}); diff --git a/test/unit/auth/tenant-manager.spec.ts b/test/unit/auth/tenant-manager.spec.ts new file mode 100644 index 0000000000..8a8f0617f2 --- /dev/null +++ b/test/unit/auth/tenant-manager.spec.ts @@ -0,0 +1,583 @@ +/*! + * 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. + */ + +'use strict'; + +import * as _ from 'lodash'; +import * as chai from 'chai'; +import * as sinon from 'sinon'; +import * as sinonChai from 'sinon-chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +import * as mocks from '../../resources/mocks'; +import { FirebaseApp } from '../../../src/app/firebase-app'; +import { AuthRequestHandler } from '../../../src/auth/auth-api-request'; +import { TenantServerResponse } from '../../../src/auth/tenant'; +import { AuthClientErrorCode, FirebaseAuthError } from '../../../src/utils/error'; +import { + CreateTenantRequest, UpdateTenantRequest, ListTenantsResult, Tenant, TenantManager, +} from '../../../src/auth/index'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +describe('TenantManager', () => { + const TENANT_ID = 'tenant-id'; + let mockApp: FirebaseApp; + let tenantManager: TenantManager; + let nullAccessTokenTenantManager: TenantManager; + let malformedAccessTokenTenantManager: TenantManager; + let rejectedPromiseAccessTokenTenantManager: TenantManager; + const GET_TENANT_RESPONSE: TenantServerResponse = { + name: 'projects/project-id/tenants/tenant-id', + displayName: 'TENANT-DISPLAY-NAME', + allowPasswordSignup: true, + enableEmailLinkSignin: false, + enableAnonymousUser: true, + }; + + before(() => { + mockApp = mocks.app(); + tenantManager = new TenantManager(mockApp); + nullAccessTokenTenantManager = new TenantManager( + mocks.appReturningNullAccessToken()); + malformedAccessTokenTenantManager = new TenantManager( + mocks.appReturningMalformedAccessToken()); + rejectedPromiseAccessTokenTenantManager = new TenantManager( + mocks.appRejectedWhileFetchingAccessToken()); + + }); + + after(() => { + return mockApp.delete(); + }); + + describe('authForTenant()', () => { + const invalidTenantIds = [null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidTenantIds.forEach((invalidTenantId) => { + it('should throw given invalid tenant ID: ' + JSON.stringify(invalidTenantId), () => { + expect(() => { + return tenantManager.authForTenant(invalidTenantId as any); + }).to.throw('The tenant ID must be a valid non-empty string.'); + }); + }); + + it('should return a TenantAwareAuth with the expected tenant ID', () => { + expect(tenantManager.authForTenant(TENANT_ID).tenantId).to.equal(TENANT_ID); + }); + + it('should return a TenantAwareAuth with read-only tenant ID', () => { + expect(() => { + (tenantManager.authForTenant(TENANT_ID) as any).tenantId = 'OTHER-TENANT-ID'; + }).to.throw('Cannot assign to read only property \'tenantId\' of object \'#\''); + }); + + it('should cache the returned TenantAwareAuth', () => { + const tenantAwareAuth1 = tenantManager.authForTenant('tenantId1'); + const tenantAwareAuth2 = tenantManager.authForTenant('tenantId2'); + expect(tenantManager.authForTenant('tenantId1')).to.equal(tenantAwareAuth1); + expect(tenantManager.authForTenant('tenantId2')).to.equal(tenantAwareAuth2); + expect(tenantAwareAuth1).to.not.be.equal(tenantAwareAuth2); + expect(tenantAwareAuth1.tenantId).to.equal('tenantId1'); + expect(tenantAwareAuth2.tenantId).to.equal('tenantId2'); + }); + }); + + describe('getTenant()', () => { + const tenantId = 'tenant-id'; + const expectedTenant = new Tenant(GET_TENANT_RESPONSE); + const expectedError = new FirebaseAuthError(AuthClientErrorCode.TENANT_NOT_FOUND); + // Stubs used to simulate underlying API calls. + let stubs: sinon.SinonStub[] = []; + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + it('should be rejected given no tenant ID', () => { + return (tenantManager as any).getTenant() + .should.eventually.be.rejected.and.have.property('code', 'auth/invalid-tenant-id'); + }); + + it('should be rejected given an invalid tenant ID', () => { + const invalidTenantId = ''; + return tenantManager.getTenant(invalidTenantId) + .then(() => { + throw new Error('Unexpected success'); + }) + .catch((error) => { + expect(error).to.have.property('code', 'auth/invalid-tenant-id'); + }); + }); + + it('should be rejected given an app which returns null access tokens', () => { + return nullAccessTokenTenantManager.getTenant(tenantId) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should be rejected given an app which returns invalid access tokens', () => { + return malformedAccessTokenTenantManager.getTenant(tenantId) + .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 rejectedPromiseAccessTokenTenantManager.getTenant(tenantId) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should resolve with a Tenant on success', () => { + // Stub getTenant to return expected result. + const stub = sinon.stub(AuthRequestHandler.prototype, 'getTenant') + .returns(Promise.resolve(GET_TENANT_RESPONSE)); + stubs.push(stub); + return tenantManager.getTenant(tenantId) + .then((result) => { + // Confirm underlying API called with expected parameters. + expect(stub).to.have.been.calledOnce.and.calledWith(tenantId); + // Confirm expected tenant returned. + expect(result).to.deep.equal(expectedTenant); + }); + }); + + it('should throw an error when the backend returns an error', () => { + // Stub getTenant to throw a backend error. + const stub = sinon.stub(AuthRequestHandler.prototype, 'getTenant') + .returns(Promise.reject(expectedError)); + stubs.push(stub); + return tenantManager.getTenant(tenantId) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(stub).to.have.been.calledOnce.and.calledWith(tenantId); + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); + }); + + describe('listTenants()', () => { + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INTERNAL_ERROR); + const pageToken = 'PAGE_TOKEN'; + const maxResult = 500; + const listTenantsResponse: any = { + tenants : [ + { 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' }), + ], + pageToken: 'NEXT_PAGE_TOKEN', + }; + const emptyListTenantsResponse: any = { + tenants: [], + }; + const emptyExpectedResult: any = { + tenants: [], + }; + // Stubs used to simulate underlying API calls. + let stubs: sinon.SinonStub[] = []; + + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + it('should be rejected given an invalid page token', () => { + const invalidToken = {}; + return tenantManager.listTenants(undefined, invalidToken as any) + .then(() => { + throw new Error('Unexpected success'); + }) + .catch((error) => { + expect(error).to.have.property('code', 'auth/invalid-page-token'); + }); + }); + + it('should be rejected given a maxResults greater than the allowed max', () => { + const moreThanMax = 1000 + 1; + return tenantManager.listTenants(moreThanMax) + .then(() => { + throw new Error('Unexpected success'); + }) + .catch((error) => { + expect(error).to.have.property('code', 'auth/argument-error'); + }); + }); + + it('should be rejected given an app which returns null access tokens', () => { + return nullAccessTokenTenantManager.listTenants(maxResult) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should be rejected given an app which returns invalid access tokens', () => { + return malformedAccessTokenTenantManager.listTenants(maxResult) + .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 rejectedPromiseAccessTokenTenantManager.listTenants(maxResult) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should resolve on listTenants request success with tenants in response', () => { + // Stub listTenants to return expected response. + const listTenantsStub = sinon + .stub(AuthRequestHandler.prototype, 'listTenants') + .returns(Promise.resolve(listTenantsResponse)); + stubs.push(listTenantsStub); + return tenantManager.listTenants(maxResult, pageToken) + .then((response) => { + expect(response).to.deep.equal(expectedResult); + // Confirm underlying API called with expected parameters. + expect(listTenantsStub) + .to.have.been.calledOnce.and.calledWith(maxResult, pageToken); + }); + }); + + it('should resolve on listTenants request success with default options', () => { + // Stub listTenants to return expected response. + const listTenantsStub = sinon + .stub(AuthRequestHandler.prototype, 'listTenants') + .returns(Promise.resolve(listTenantsResponse)); + stubs.push(listTenantsStub); + return tenantManager.listTenants() + .then((response) => { + expect(response).to.deep.equal(expectedResult); + // Confirm underlying API called with expected parameters. + expect(listTenantsStub) + .to.have.been.calledOnce.and.calledWith(undefined, undefined); + }); + }); + + it('should resolve on listTenants request success with no tenants in response', () => { + // Stub listTenants to return expected response. + const listTenantsStub = sinon + .stub(AuthRequestHandler.prototype, 'listTenants') + .returns(Promise.resolve(emptyListTenantsResponse)); + stubs.push(listTenantsStub); + return tenantManager.listTenants(maxResult, pageToken) + .then((response) => { + expect(response).to.deep.equal(emptyExpectedResult); + // Confirm underlying API called with expected parameters. + expect(listTenantsStub) + .to.have.been.calledOnce.and.calledWith(maxResult, pageToken); + }); + }); + + it('should throw an error when listTenants returns an error', () => { + // Stub listTenants to throw a backend error. + const listTenantsStub = sinon + .stub(AuthRequestHandler.prototype, 'listTenants') + .returns(Promise.reject(expectedError)); + stubs.push(listTenantsStub); + return tenantManager.listTenants(maxResult, pageToken) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(listTenantsStub) + .to.have.been.calledOnce.and.calledWith(maxResult, pageToken); + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); + }); + + describe('deleteTenant()', () => { + const tenantId = 'tenant-id'; + const expectedError = new FirebaseAuthError(AuthClientErrorCode.TENANT_NOT_FOUND); + // Stubs used to simulate underlying API calls. + let stubs: sinon.SinonStub[] = []; + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + it('should be rejected given no tenant ID', () => { + return (tenantManager as any).deleteTenant() + .should.eventually.be.rejected.and.have.property('code', 'auth/invalid-tenant-id'); + }); + + const invalidTenantIds = [null, NaN, 0, 1, true, false, '', ['tenant-id'], [], {}, { a: 1 }, _.noop]; + invalidTenantIds.forEach((invalidTenantId) => { + it('should be rejected given an invalid tenant ID:' + JSON.stringify(invalidTenantId), () => { + return tenantManager.deleteTenant(invalidTenantId as any) + .then(() => { + throw new Error('Unexpected success'); + }) + .catch((error) => { + expect(error).to.have.property('code', 'auth/invalid-tenant-id'); + }); + }); + }); + + it('should be rejected given an app which returns null access tokens', () => { + return nullAccessTokenTenantManager.deleteTenant(tenantId) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should be rejected given an app which returns invalid access tokens', () => { + return malformedAccessTokenTenantManager.deleteTenant(tenantId) + .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 rejectedPromiseAccessTokenTenantManager.deleteTenant(tenantId) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should resolve with void on success', () => { + // Stub deleteTenant to return expected result. + const stub = sinon.stub(AuthRequestHandler.prototype, 'deleteTenant') + .returns(Promise.resolve()); + stubs.push(stub); + return tenantManager.deleteTenant(tenantId) + .then((result) => { + // Confirm underlying API called with expected parameters. + expect(stub).to.have.been.calledOnce.and.calledWith(tenantId); + // Confirm expected result is undefined. + expect(result).to.be.undefined; + }); + }); + + it('should throw an error when the backend returns an error', () => { + // Stub deleteTenant to throw a backend error. + const stub = sinon.stub(AuthRequestHandler.prototype, 'deleteTenant') + .returns(Promise.reject(expectedError)); + stubs.push(stub); + return tenantManager.deleteTenant(tenantId) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(stub).to.have.been.calledOnce.and.calledWith(tenantId); + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); + }); + + describe('createTenant()', () => { + const tenantOptions: CreateTenantRequest = { + displayName: 'TENANT-DISPLAY-NAME', + emailSignInConfig: { + enabled: true, + passwordRequired: true, + }, + anonymousSignInEnabled: true, + }; + const expectedTenant = new Tenant(GET_TENANT_RESPONSE); + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'Unable to create the tenant provided.'); + // Stubs used to simulate underlying API calls. + let stubs: sinon.SinonStub[] = []; + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + it('should be rejected given no properties', () => { + return (tenantManager as any).createTenant() + .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); + }); + + it('should be rejected given invalid TenantOptions', () => { + return tenantManager.createTenant(null as any) + .then(() => { + throw new Error('Unexpected success'); + }) + .catch((error) => { + expect(error).to.have.property('code', 'auth/argument-error'); + }); + }); + + 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) + .then(() => { + throw new Error('Unexpected success'); + }) + .catch((error) => { + expect(error).to.have.property('code', 'auth/argument-error'); + }); + }); + + it('should be rejected given an app which returns null access tokens', () => { + return nullAccessTokenTenantManager.createTenant(tenantOptions) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should be rejected given an app which returns invalid access tokens', () => { + return malformedAccessTokenTenantManager.createTenant(tenantOptions) + .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 rejectedPromiseAccessTokenTenantManager.createTenant(tenantOptions) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should resolve with a Tenant on createTenant request success', () => { + // Stub createTenant to return expected result. + const createTenantStub = sinon.stub(AuthRequestHandler.prototype, 'createTenant') + .returns(Promise.resolve(GET_TENANT_RESPONSE)); + stubs.push(createTenantStub); + return tenantManager.createTenant(tenantOptions) + .then((actualTenant) => { + // Confirm underlying API called with expected parameters. + expect(createTenantStub).to.have.been.calledOnce.and.calledWith(tenantOptions); + // Confirm expected Tenant object returned. + expect(actualTenant).to.deep.equal(expectedTenant); + }); + }); + + it('should throw an error when createTenant returns an error', () => { + // Stub createTenant to throw a backend error. + const createTenantStub = sinon.stub(AuthRequestHandler.prototype, 'createTenant') + .returns(Promise.reject(expectedError)); + stubs.push(createTenantStub); + return tenantManager.createTenant(tenantOptions) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(createTenantStub).to.have.been.calledOnce.and.calledWith(tenantOptions); + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); + }); + + describe('updateTenant()', () => { + const tenantId = 'tenant-id'; + const tenantOptions: UpdateTenantRequest = { + displayName: 'TENANT-DISPLAY-NAME', + emailSignInConfig: { + enabled: true, + passwordRequired: true, + }, + anonymousSignInEnabled: true, + }; + const expectedTenant = new Tenant(GET_TENANT_RESPONSE); + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'Unable to update the tenant provided.'); + // Stubs used to simulate underlying API calls. + let stubs: sinon.SinonStub[] = []; + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + it('should be rejected given no tenant ID', () => { + return (tenantManager as any).updateTenant(undefined, tenantOptions) + .should.eventually.be.rejected.and.have.property('code', 'auth/invalid-tenant-id'); + }); + + it('should be rejected given an invalid tenant ID', () => { + const invalidTenantId = ''; + return tenantManager.updateTenant(invalidTenantId, tenantOptions) + .then(() => { + throw new Error('Unexpected success'); + }) + .catch((error) => { + expect(error).to.have.property('code', 'auth/invalid-tenant-id'); + }); + }); + + it('should be rejected given no TenantOptions', () => { + return (tenantManager as any).updateTenant(tenantId) + .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); + }); + + it('should be rejected given invalid TenantOptions', () => { + return tenantManager.updateTenant(tenantId, null as unknown as UpdateTenantRequest) + .then(() => { + throw new Error('Unexpected success'); + }) + .catch((error) => { + expect(error).to.have.property('code', 'auth/argument-error'); + }); + }); + + 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) + .then(() => { + throw new Error('Unexpected success'); + }) + .catch((error) => { + expect(error).to.have.property('code', 'auth/argument-error'); + }); + }); + + it('should be rejected given an app which returns null access tokens', () => { + return nullAccessTokenTenantManager.updateTenant(tenantId, tenantOptions) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should be rejected given an app which returns invalid access tokens', () => { + return malformedAccessTokenTenantManager.updateTenant(tenantId, tenantOptions) + .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 rejectedPromiseAccessTokenTenantManager.updateTenant(tenantId, tenantOptions) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should resolve with a Tenant on updateTenant request success', () => { + // Stub updateTenant to return expected result. + const updateTenantStub = sinon.stub(AuthRequestHandler.prototype, 'updateTenant') + .returns(Promise.resolve(GET_TENANT_RESPONSE)); + stubs.push(updateTenantStub); + return tenantManager.updateTenant(tenantId, tenantOptions) + .then((actualTenant) => { + // Confirm underlying API called with expected parameters. + expect(updateTenantStub).to.have.been.calledOnce.and.calledWith(tenantId, tenantOptions); + // Confirm expected Tenant object returned. + expect(actualTenant).to.deep.equal(expectedTenant); + }); + }); + + it('should throw an error when updateTenant returns an error', () => { + // Stub updateTenant to throw a backend error. + const updateTenantStub = sinon.stub(AuthRequestHandler.prototype, 'updateTenant') + .returns(Promise.reject(expectedError)); + stubs.push(updateTenantStub); + return tenantManager.updateTenant(tenantId, tenantOptions) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(updateTenantStub).to.have.been.calledOnce.and.calledWith(tenantId, tenantOptions); + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); + }); +}); diff --git a/test/unit/auth/tenant.spec.ts b/test/unit/auth/tenant.spec.ts new file mode 100644 index 0000000000..e1006e47b7 --- /dev/null +++ b/test/unit/auth/tenant.spec.ts @@ -0,0 +1,1172 @@ +/*! + * 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 * as _ from 'lodash'; +import * as chai from 'chai'; +import * as sinonChai from 'sinon-chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +import { deepCopy } from '../../../src/utils/deep-copy'; +import { EmailSignInConfig, MultiFactorAuthConfig, RecaptchaAuthConfig, + PasswordPolicyAuthServerConfig, PasswordPolicyConfig, +} from '../../../src/auth/auth-config'; +import { TenantServerResponse } from '../../../src/auth/tenant'; +import { + CreateTenantRequest, UpdateTenantRequest, EmailSignInProviderConfig, Tenant, +} from '../../../src/auth/index'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +describe('Tenant', () => { + const smsAllowByDefault = { + allowByDefault: { + disallowedRegions: [ 'AC', 'AD' ], + }, + }; + + const smsAllowlistOnly = { + allowlistOnly: { + allowedRegions: [ 'AC', 'AD' ], + }, + }; + + const passwordPolicyClientConfig: PasswordPolicyConfig = { + enforcementState: 'ENFORCE', + forceUpgradeOnSignin: true, + constraints: { + requireLowercase: true, + requireUppercase: true, + requireNonAlphanumeric: true, + requireNumeric: true, + minLength: 8, + maxLength: 30, + } + }; + + const passwordPolicyServerConfig: PasswordPolicyAuthServerConfig = { + passwordPolicyEnforcementState: 'ENFORCE', + forceUpgradeOnSignin: true, + passwordPolicyVersions: [ + { + customStrengthOptions: { + containsLowercaseCharacter: true, + containsNonAlphanumericCharacter: true, + containsNumericCharacter: true, + containsUppercaseCharacter: true, + minPasswordLength: 8, + maxPasswordLength: 30, + }, + }, + ], + }; + + const serverRequest: TenantServerResponse = { + name: 'projects/project1/tenants/TENANT-ID', + displayName: 'TENANT-DISPLAY-NAME', + allowPasswordSignup: true, + enableEmailLinkSignin: true, + mfaConfig: { + state: 'ENABLED', + enabledProviders: ['PHONE_SMS'], + providerConfigs: [ + { + state: 'ENABLED', + totpProviderConfig: { + adjacentIntervals: 5, + }, + }, + ], + }, + testPhoneNumbers: { + '+16505551234': '019287', + '+16505550676': '985235', + }, + smsRegionConfig: smsAllowByDefault, + passwordPolicyConfig: passwordPolicyServerConfig, + emailPrivacyConfig: { + enableImprovedEmailPrivacy: true, + }, + }; + + const clientRequest: UpdateTenantRequest = { + displayName: 'TENANT-DISPLAY-NAME', + emailSignInConfig: { + enabled: true, + passwordRequired: false, + }, + multiFactorConfig: { + state: 'ENABLED', + factorIds: ['phone'], + providerConfigs: [ + { + state: 'ENABLED', + totpProviderConfig: { + adjacentIntervals: 5, + }, + }, + ], + }, + testPhoneNumbers: { + '+16505551234': '019287', + '+16505550676': '985235', + }, + smsRegionConfig: smsAllowByDefault, + passwordPolicyConfig: passwordPolicyClientConfig, + emailPrivacyConfig: { + enableImprovedEmailPrivacy: true, + }, + }; + + const serverRequestWithoutMfa: TenantServerResponse = { + name: 'projects/project1/tenants/TENANT-ID', + displayName: 'TENANT-DISPLAY-NAME', + allowPasswordSignup: true, + enableEmailLinkSignin: true, + passwordPolicyConfig: passwordPolicyServerConfig, + emailPrivacyConfig: { + enableImprovedEmailPrivacy: true, + }, + }; + + const clientRequestWithoutMfa: UpdateTenantRequest = { + displayName: 'TENANT-DISPLAY-NAME', + emailSignInConfig: { + enabled: true, + passwordRequired: false, + }, + passwordPolicyConfig: passwordPolicyClientConfig, + emailPrivacyConfig: { + enableImprovedEmailPrivacy: true, + }, + }; + + const clientRequestWithRecaptcha: UpdateTenantRequest = { + displayName: 'TENANT-DISPLAY-NAME', + emailSignInConfig: { + enabled: true, + passwordRequired: false, + }, + multiFactorConfig: { + state: 'ENABLED', + factorIds: ['phone'], + }, + testPhoneNumbers: { + '+16505551234': '019287', + '+16505550676': '985235', + }, + recaptchaConfig: { + managedRules: [{ + endScore: 0.2, + action: 'BLOCK' + }], + emailPasswordEnforcementState: 'AUDIT', + useAccountDefender: true, + }, + }; + + const serverResponseWithRecaptcha: TenantServerResponse = { + name: 'projects/project1/tenants/TENANT-ID', + displayName: 'TENANT-DISPLAY-NAME', + allowPasswordSignup: true, + enableEmailLinkSignin: true, + mfaConfig: { + state: 'ENABLED', + enabledProviders: ['PHONE_SMS'], + providerConfigs: [ + { + state: 'ENABLED', + totpProviderConfig: { + adjacentIntervals: 5, + }, + }, + ], + }, + testPhoneNumbers: { + '+16505551234': '019287', + '+16505550676': '985235', + }, + recaptchaConfig: { + emailPasswordEnforcementState: 'AUDIT', + managedRules: [ { + endScore: 0.2, + action: 'BLOCK' + } ], + recaptchaKeys: [ { + type: 'WEB', + key: 'test-key-1' } + ], + useAccountDefender: true, + }, + smsRegionConfig: smsAllowByDefault, + passwordPolicyConfig: passwordPolicyServerConfig, + emailPrivacyConfig: { + enableImprovedEmailPrivacy: true, + }, + }; + + describe('buildServerRequest()', () => { + const createRequest = true; + + describe('for an update request', () => { + it('should return the expected server request without multi-factor and phone config', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithoutMfa); + const tenantOptionsServerRequest = deepCopy(serverRequestWithoutMfa); + delete (tenantOptionsServerRequest as any).name; + expect(Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest)) + .to.deep.equal(tenantOptionsServerRequest); + }); + + it('should return the expected server request with multi-factor (SMS, TOTP) and testPhoneNumber config', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest); + const tenantOptionsServerRequest = deepCopy(serverRequest); + delete (tenantOptionsServerRequest as any).name; + expect(Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest)) + .to.deep.equal(tenantOptionsServerRequest); + }); + + it('should throw on invalid EmailSignInConfig object', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest); + tenantOptionsClientRequest.emailSignInConfig = null as unknown as EmailSignInProviderConfig; + expect(() => Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest)) + .to.throw('"EmailSignInConfig" must be a non-null object.'); + }); + + it('should throw on invalid EmailSignInConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.emailSignInConfig.enabled = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).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 null RecaptchaConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig = null; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"RecaptchaConfig" must be a non-null object.'); + }); + + it('should throw on invalid RecaptchaConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig.invalidParameter = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"invalidParameter" is not a valid RecaptchaConfig parameter.'); + }); + + it('should throw on null emailPasswordEnforcementState attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig.emailPasswordEnforcementState = null; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"RecaptchaConfig.emailPasswordEnforcementState" must be a valid non-empty string.'); + }); + + it('should throw on invalid emailPasswordEnforcementState attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig + .emailPasswordEnforcementState = 'INVALID'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"RecaptchaConfig.emailPasswordEnforcementState" must be either "OFF", "AUDIT" or "ENFORCE".'); + }); + + it('should throw on non-array managedRules attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig.managedRules = 'non-array'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"RecaptchaConfig.managedRules" must be an array of valid "RecaptchaManagedRule".'); + }); + + it('should throw on non-boolean useAccountDefender attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig.useAccountDefender = 'yes'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"RecaptchaConfig.useAccountDefender" must be a boolean value".'); + }); + + it('should throw on invalid managedRules attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig.managedRules = + [{ 'score': 0.1, 'action': 'BLOCK' }]; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"score" is not a valid RecaptchaManagedRule parameter.'); + }); + + it('should throw on invalid RecaptchaManagedRule.action attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig.managedRules = + [{ 'endScore': 0.1, 'action': 'ALLOW' }]; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"RecaptchaManagedRule.action" must be "BLOCK".'); + }); + + 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 as any).name; + tenantOptionsServerRequest.testPhoneNumbers = {}; + + expect(Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest)) + .to.deep.equal(tenantOptionsServerRequest); + }); + + it('should throw on null SmsRegionConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig = null; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"SmsRegionConfig" must be a non-null object.'); + }); + + it('should throw on invalid SmsRegionConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig.invalidParameter = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"invalidParameter" is not a valid SmsRegionConfig parameter.'); + }); + + it('should throw on invalid allowlistOnly attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig = deepCopy(smsAllowlistOnly); + tenantOptionsClientRequest.smsRegionConfig.allowlistOnly.disallowedRegions = [ 'AC', 'AD' ]; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"disallowedRegions" is not a valid SmsRegionConfig.allowlistOnly parameter.'); + }); + + it('should throw on invalid allowByDefault attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig.allowByDefault.allowedRegions = [ 'AC', 'AD' ]; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"allowedRegions" is not a valid SmsRegionConfig.allowByDefault parameter.'); + }); + + it('should throw on non-array disallowedRegions attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig.allowByDefault.disallowedRegions = 'non-array'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"SmsRegionConfig.allowByDefault.disallowedRegions" must be a valid string array.'); + }); + + it('should throw on non-array allowedRegions attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig = deepCopy(smsAllowlistOnly); + tenantOptionsClientRequest.smsRegionConfig.allowlistOnly.allowedRegions = 'non-array'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"SmsRegionConfig.allowlistOnly.allowedRegions" must be a valid string array.'); + }); + + it('should throw when both allowlistOnly and allowByDefault attributes are presented', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig = { ...smsAllowByDefault, ...smsAllowlistOnly }; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('SmsRegionConfig cannot have both "allowByDefault" and "allowlistOnly" parameters.'); + }); + + it('should throw on null PasswordPolicyConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.passwordPolicyConfig = null; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"PasswordPolicyConfig" must be a non-null object.'); + }); + + it('should throw on invalid PasswordPolicyConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.passwordPolicyConfig.invalidParameter = 'invalid', + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"invalidParameter" is not a valid PasswordPolicyConfig parameter.'); + }); + + it('should throw on missing enforcementState', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + delete tenantOptionsClientRequest.passwordPolicyConfig.enforcementState; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"PasswordPolicyConfig.enforcementState" must be either "ENFORCE" or "OFF".'); + }); + + it('should throw on invalid enforcementState', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.passwordPolicyConfig.enforcementState = 'INVALID_STATE'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"PasswordPolicyConfig.enforcementState" must be either "ENFORCE" or "OFF".'); + }); + + it('should throw on invalid forceUpgradeOnSignin', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.passwordPolicyConfig.forceUpgradeOnSignin = 'INVALID'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"PasswordPolicyConfig.forceUpgradeOnSignin" must be a boolean.'); + }); + + it('should throw on undefined constraints when state is enforced', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + delete tenantOptionsClientRequest.passwordPolicyConfig.constraints; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"PasswordPolicyConfig.constraints" must be defined.'); + }); + + it('should throw on invalid constraints attribute', ()=> { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.passwordPolicyConfig.constraints.invalidParameter = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"invalidParameter" is not a valid PasswordPolicyConfig.constraints parameter.'); + }); + + it('should throw on null constraints object', ()=> { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.passwordPolicyConfig.constraints = null; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"PasswordPolicyConfig.constraints" must be a non-empty object.'); + }); + + it('should throw on invalid constraints object', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.passwordPolicyConfig.constraints = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"PasswordPolicyConfig.constraints" must be a non-empty object.'); + }); + + it('should throw on invalid uppercase type', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.passwordPolicyConfig.constraints.requireUppercase = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"PasswordPolicyConfig.constraints.requireUppercase"' + + ' must be a boolean.'); + }); + + it('should throw on invalid lowercase type', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.passwordPolicyConfig.constraints.requireLowercase = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"PasswordPolicyConfig.constraints.requireLowercase"' + + ' must be a boolean.'); + }); + + it('should throw on invalid numeric type', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.passwordPolicyConfig.constraints.requireNumeric = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"PasswordPolicyConfig.constraints.requireNumeric"' + + ' must be a boolean.'); + }); + + it('should throw on invalid non-alphanumeric type', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.passwordPolicyConfig.constraints.requireNonAlphanumeric = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"PasswordPolicyConfig.constraints.requireNonAlphanumeric"' + + ' must be a boolean.'); + }); + + it('should throw on invalid minLength type', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.passwordPolicyConfig.constraints.minLength = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"PasswordPolicyConfig.constraints.minLength" must be a number.'); + }); + + it('should throw on invalid maxLength type', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.passwordPolicyConfig.constraints.maxLength = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"PasswordPolicyConfig.constraints.maxLength" must be a number.'); + }); + + it('should throw on invalid minLength range', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.passwordPolicyConfig.constraints.minLength = 45; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"PasswordPolicyConfig.constraints.minLength"' + + ' must be an integer between 6 and 30, inclusive.'); + }); + + it('should throw on invalid maxLength range', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.passwordPolicyConfig.constraints.maxLength = 5000; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"PasswordPolicyConfig.constraints.maxLength"' + + ' must be greater than or equal to minLength and at max 4096.'); + }); + + it('should throw if minLength is greater than maxLength', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.passwordPolicyConfig.constraints.minLength = 20; + tenantOptionsClientRequest.passwordPolicyConfig.constraints.maxLength = 7; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"PasswordPolicyConfig.constraints.maxLength"' + + ' must be greater than or equal to minLength and at max 4096.'); + }); + + it('should throw on null EmailPrivacyConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.emailPrivacyConfig = null; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"EmailPrivacyConfig" must be a non-null object.'); + }); + + it('should throw on invalid EmailPrivacyConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.emailPrivacyConfig.invalidParameter = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"invalidParameter" is not a valid "EmailPrivacyConfig" parameter.'); + }); + + it('should throw on invalid enableImprovedEmailPrivacy attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.emailPrivacyConfig.enableImprovedEmailPrivacy = []; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"EmailPrivacyConfig.enableImprovedEmailPrivacy" must be a valid boolean value.'); + }); + + it('should not throw on valid client request object', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha); + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).not.to.throw; + }); + + const nonObjects = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; + nonObjects.forEach((request) => { + it('should throw on invalid UpdateTenantRequest:' + JSON.stringify(request), () => { + expect(() => { + Tenant.buildServerRequest(request as any, !createRequest); + }).to.throw('"UpdateTenantRequest" must be a valid non-null object.'); + }); + }); + + it('should throw on unsupported attribute for update request', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.unsupported = 'value'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"unsupported" is not a valid UpdateTenantRequest parameter.'); + }); + + const invalidTenantNames = [null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidTenantNames.forEach((displayName) => { + it('should throw on invalid UpdateTenantRequest displayName:' + JSON.stringify(displayName), () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.displayName = displayName; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"UpdateTenantRequest.displayName" must be a valid non-empty string.'); + }); + }); + }); + + describe('for a create request', () => { + it('should return the expected server request without multi-factor and phone config', () => { + const tenantOptionsClientRequest: CreateTenantRequest = deepCopy(clientRequestWithoutMfa); + const tenantOptionsServerRequest: TenantServerResponse = deepCopy(serverRequestWithoutMfa); + delete (tenantOptionsServerRequest as any).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: CreateTenantRequest = deepCopy(clientRequest); + const tenantOptionsServerRequest: TenantServerResponse = deepCopy(serverRequest); + delete (tenantOptionsServerRequest as any).name; + + expect(Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest)) + .to.deep.equal(tenantOptionsServerRequest); + }); + + it('should throw on invalid EmailSignInConfig', () => { + const tenantOptionsClientRequest: CreateTenantRequest = deepCopy(clientRequest); + tenantOptionsClientRequest.emailSignInConfig = null as unknown as EmailSignInProviderConfig; + + expect(() => Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest)) + .to.throw('"EmailSignInConfig" must be a non-null object.'); + }); + + it('should throw on invalid MultiFactorConfig.factorIds 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 null RecaptchaConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig = null; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"RecaptchaConfig" must be a non-null object.'); + }); + + it('should throw on invalid RecaptchaConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig.invalidParameter = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"invalidParameter" is not a valid RecaptchaConfig parameter.'); + }); + + it('should throw on null emailPasswordEnforcementState attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig.emailPasswordEnforcementState = null; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"RecaptchaConfig.emailPasswordEnforcementState" must be a valid non-empty string.'); + }); + + it('should throw on invalid emailPasswordEnforcementState attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig + .emailPasswordEnforcementState = 'INVALID'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"RecaptchaConfig.emailPasswordEnforcementState" must be either "OFF", "AUDIT" or "ENFORCE".'); + }); + + it('should throw on non-array managedRules attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig.managedRules = 'non-array'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"RecaptchaConfig.managedRules" must be an array of valid "RecaptchaManagedRule".'); + }); + + const invalidUseAccountDefender = [null, NaN, 0, 1, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidUseAccountDefender.forEach((useAccountDefender) => { + it('should throw on non-boolean useAccountDefender attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig.useAccountDefender = useAccountDefender; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"RecaptchaConfig.useAccountDefender" must be a boolean value".'); + }); + }); + + it('should throw on invalid managedRules attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig.managedRules = + [{ 'score': 0.1, 'action': 'BLOCK' }]; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"score" is not a valid RecaptchaManagedRule parameter.'); + }); + + it('should throw on invalid RecaptchaManagedRule.action attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithRecaptcha) as any; + tenantOptionsClientRequest.recaptchaConfig.managedRules = + [{ 'endScore': 0.1, 'action': 'ALLOW' }]; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"RecaptchaManagedRule.action" must be "BLOCK".'); + }); + + 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 as any).name; + tenantOptionsServerRequest.testPhoneNumbers = {}; + + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"CreateTenantRequest.testPhoneNumbers" must be a non-null object.'); + }); + + it('should throw on null SmsRegionConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig = null; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"SmsRegionConfig" must be a non-null object.'); + }); + + it('should throw on invalid SmsRegionConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig.invalidParameter = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"invalidParameter" is not a valid SmsRegionConfig parameter.'); + }); + + it('should throw on invalid allowlistOnly attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig = deepCopy(smsAllowlistOnly); + tenantOptionsClientRequest.smsRegionConfig.allowlistOnly.disallowedRegions = [ 'AC', 'AD' ]; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"disallowedRegions" is not a valid SmsRegionConfig.allowlistOnly parameter.'); + }); + + it('should throw on invalid allowByDefault attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig.allowByDefault.allowedRegions = [ 'AC', 'AD' ]; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"allowedRegions" is not a valid SmsRegionConfig.allowByDefault parameter.'); + }); + + it('should throw on non-array disallowedRegions attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig.allowByDefault.disallowedRegions = 'non-array'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"SmsRegionConfig.allowByDefault.disallowedRegions" must be a valid string array.'); + }); + + it('should throw on non-array allowedRegions attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig = deepCopy(smsAllowlistOnly); + tenantOptionsClientRequest.smsRegionConfig.allowlistOnly.allowedRegions = 'non-array'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"SmsRegionConfig.allowlistOnly.allowedRegions" must be a valid string array.'); + }); + + it('should throw when both allowlistOnly and allowByDefault attributes are presented', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.smsRegionConfig = { ...smsAllowByDefault, ...smsAllowlistOnly }; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('SmsRegionConfig cannot have both "allowByDefault" and "allowlistOnly" parameters.'); + }); + + it('should throw on null PasswordPolicyConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.passwordPolicyConfig = null; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"PasswordPolicyConfig" must be a non-null object.'); + }); + + it('should throw on invalid PasswordPolicyConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.passwordPolicyConfig.invalidParameter = 'invalid', + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"invalidParameter" is not a valid PasswordPolicyConfig parameter.'); + }); + + it('should throw on missing enforcementState', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + delete tenantOptionsClientRequest.passwordPolicyConfig.enforcementState; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"PasswordPolicyConfig.enforcementState" must be either "ENFORCE" or "OFF".'); + }); + + it('should throw on invalid enforcementState', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.passwordPolicyConfig.enforcementState = 'INVALID_STATE'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"PasswordPolicyConfig.enforcementState" must be either "ENFORCE" or "OFF".'); + }); + + it('should throw on invalid forceUpgradeOnSignin', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.passwordPolicyConfig.forceUpgradeOnSignin = 'INVALID'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"PasswordPolicyConfig.forceUpgradeOnSignin" must be a boolean.'); + }); + + it('should throw on undefined constraints when state is enforced', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + delete tenantOptionsClientRequest.passwordPolicyConfig.constraints; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"PasswordPolicyConfig.constraints" must be defined.'); + }); + + it('should throw on invalid constraints attribute', ()=> { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.passwordPolicyConfig.constraints.invalidParameter = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"invalidParameter" is not a valid PasswordPolicyConfig.constraints parameter.'); + }); + + it('should throw on null constraints object', ()=> { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.passwordPolicyConfig.constraints = null; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"PasswordPolicyConfig.constraints" must be a non-empty object.'); + }); + + it('should throw on invalid constraints object', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.passwordPolicyConfig.constraints = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"PasswordPolicyConfig.constraints" must be a non-empty object.'); + }); + + it('should throw on invalid uppercase type', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.passwordPolicyConfig.constraints.requireUppercase = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"PasswordPolicyConfig.constraints.requireUppercase"' + + ' must be a boolean.'); + }); + + it('should throw on invalid lowercase type', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.passwordPolicyConfig.constraints.requireLowercase = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"PasswordPolicyConfig.constraints.requireLowercase"' + + ' must be a boolean.'); + }); + + it('should throw on invalid numeric type', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.passwordPolicyConfig.constraints.requireNumeric = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"PasswordPolicyConfig.constraints.requireNumeric"' + + ' must be a boolean.'); + }); + + it('should throw on invalid non-alphanumeric type', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.passwordPolicyConfig.constraints.requireNonAlphanumeric = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"PasswordPolicyConfig.constraints.requireNonAlphanumeric"' + + ' must be a boolean.'); + }); + + it('should throw on invalid minLength type', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.passwordPolicyConfig.constraints.minLength = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"PasswordPolicyConfig.constraints.minLength" must be a number.'); + }); + + it('should throw on invalid maxLength type', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.passwordPolicyConfig.constraints.maxLength = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"PasswordPolicyConfig.constraints.maxLength" must be a number.'); + }); + + it('should throw on invalid minLength range', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.passwordPolicyConfig.constraints.minLength = 45; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"PasswordPolicyConfig.constraints.minLength"' + + ' must be an integer between 6 and 30, inclusive.'); + }); + + it('should throw on invalid maxLength range', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.passwordPolicyConfig.constraints.maxLength = 5000; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"PasswordPolicyConfig.constraints.maxLength"' + + ' must be greater than or equal to minLength and at max 4096.'); + }); + + it('should throw if minLength is greater than maxLength', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.passwordPolicyConfig.constraints.minLength = 20; + tenantOptionsClientRequest.passwordPolicyConfig.constraints.maxLength = 7; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"PasswordPolicyConfig.constraints.maxLength"' + + ' must be greater than or equal to minLength and at max 4096.'); + }); + + it('should throw on null EmailPrivacyConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.emailPrivacyConfig = null; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"EmailPrivacyConfig" must be a non-null object.'); + }); + + it('should throw on invalid EmailPrivacyConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.emailPrivacyConfig.invalidParameter = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"invalidParameter" is not a valid "EmailPrivacyConfig" parameter.'); + }); + + it('should throw on invalid enableImprovedEmailPrivacy attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.emailPrivacyConfig.enableImprovedEmailPrivacy = []; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"EmailPrivacyConfig.enableImprovedEmailPrivacy" must be a valid boolean value.'); + }); + + const nonObjects = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; + nonObjects.forEach((request) => { + it('should throw on invalid CreateTenantRequest:' + JSON.stringify(request), () => { + expect(() => { + Tenant.buildServerRequest(request as any, createRequest); + }).to.throw('"CreateTenantRequest" must be a valid non-null object.'); + }); + }); + + it('should throw on unsupported attribute for create request', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.unsupported = 'value'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"unsupported" is not a valid CreateTenantRequest parameter.'); + }); + + const invalidTenantNames = [null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidTenantNames.forEach((displayName) => { + it('should throw on invalid CreateTenantRequest displayName:' + JSON.stringify(displayName), () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.displayName = displayName; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"CreateTenantRequest.displayName" must be a valid non-empty string.'); + }); + }); + }); + }); + + describe('getTenantIdFromResourceName()', () => { + it('should return the expected tenant ID from resource name', () => { + expect(Tenant.getTenantIdFromResourceName('projects/project1/tenants/TENANT-ID')) + .to.equal('TENANT-ID'); + }); + + it('should return the expected tenant ID from resource name whose project ID contains "tenants" substring', () => { + expect(Tenant.getTenantIdFromResourceName('projects/projecttenants/tenants/TENANT-ID')) + .to.equal('TENANT-ID'); + }); + + it('should return null when no tenant ID is found', () => { + expect(Tenant.getTenantIdFromResourceName('projects/project1')).to.be.null; + }); + }); + + describe('constructor', () => { + const serverRequestCopy: TenantServerResponse = deepCopy(serverRequest); + const tenant = new Tenant(serverRequestCopy); + it('should not throw on valid initialization', () => { + expect(() => new Tenant(serverRequest)).not.to.throw(); + }); + + it('should set readonly property tenantId', () => { + expect(tenant.tenantId).to.equal('TENANT-ID'); + }); + + it('should set readonly property displayName', () => { + expect(tenant.displayName).to.equal('TENANT-DISPLAY-NAME'); + }); + + it('should set readonly property emailSignInConfig', () => { + const expectedEmailSignInConfig = new EmailSignInConfig({ + allowPasswordSignup: true, + enableEmailLinkSignin: true, + }); + expect(tenant.emailSignInConfig).to.deep.equal(expectedEmailSignInConfig); + }); + + it('should set readonly property multiFactorConfig', () => { + const expectedMultiFactorConfig = new MultiFactorAuthConfig({ + state: 'ENABLED', + enabledProviders: ['PHONE_SMS'], + providerConfigs: [ + { + state: 'ENABLED', + totpProviderConfig: { + adjacentIntervals: 5, + }, + }, + ], + }); + expect(tenant.multiFactorConfig).to.deep.equal(expectedMultiFactorConfig); + }); + + it('should set readonly property recaptchaConfig', () => { + const serverRequestWithRecaptchaCopy: TenantServerResponse = + deepCopy(serverResponseWithRecaptcha); + const tenantWithRecaptcha = new Tenant(serverRequestWithRecaptchaCopy); + const expectedRecaptchaConfig = new RecaptchaAuthConfig({ + emailPasswordEnforcementState: 'AUDIT', + managedRules: [{ + endScore: 0.2, + action: 'BLOCK' + }], + recaptchaKeys: [ { + type: 'WEB', + key: 'test-key-1' } + ], + useAccountDefender: true, + }); + expect(tenantWithRecaptcha.recaptchaConfig).to.deep.equal(expectedRecaptchaConfig); + }); + + it('should set readonly property testPhoneNumbers', () => { + expect(tenant.testPhoneNumbers).to.deep.equal( + deepCopy(clientRequest.testPhoneNumbers)); + }); + + it('should set readonly property smsRegionConfig', () => { + expect(tenant.smsRegionConfig).to.deep.equal( + deepCopy(clientRequest.smsRegionConfig)); + }); + + it('should set readonly property passwordPolicyConfig', () => { + expect(tenant.passwordPolicyConfig).to.deep.equal( + deepCopy(clientRequest.passwordPolicyConfig)); + }); + + it('should set readonly property emailPrivacyConfig', () => { + const expectedEmailPrivacyConfig = { + enableImprovedEmailPrivacy: true, + }; + expect(clientRequest.emailPrivacyConfig).to.deep.equal(expectedEmailPrivacyConfig); + }); + + it('should throw when no tenant ID is provided', () => { + const invalidOptions = deepCopy(serverRequest); + // Use resource name that does not include a tenant ID. + invalidOptions.name = 'projects/project1'; + expect(() => new Tenant(invalidOptions)) + .to.throw('INTERNAL ASSERT FAILED: Invalid tenant response'); + }); + + it('should set default EmailSignInConfig when allowPasswordSignup is undefined', () => { + const serverResponse: TenantServerResponse = { + name: 'projects/project1/tenants/TENANT-ID', + displayName: 'TENANT-DISPLAY-NAME', + }; + expect(() => { + const tenantWithoutAllowPasswordSignup = new Tenant(serverResponse); + + expect(tenantWithoutAllowPasswordSignup.displayName).to.equal(serverResponse.displayName); + expect(tenantWithoutAllowPasswordSignup.tenantId).to.equal('TENANT-ID'); + expect(tenantWithoutAllowPasswordSignup.emailSignInConfig).to.exist; + expect(tenantWithoutAllowPasswordSignup.emailSignInConfig!.enabled).to.be.false; + expect(tenantWithoutAllowPasswordSignup.emailSignInConfig!.passwordRequired).to.be.true; + }).not.to.throw(); + }); + }); + + describe('toJSON()', () => { + const serverRequestCopy: TenantServerResponse = deepCopy(serverResponseWithRecaptcha); + it('should return the expected object representation of a tenant', () => { + expect(new Tenant(serverRequestCopy).toJSON()).to.deep.equal({ + tenantId: 'TENANT-ID', + displayName: 'TENANT-DISPLAY-NAME', + emailSignInConfig: { + enabled: true, + passwordRequired: false, + }, + anonymousSignInEnabled: false, + multiFactorConfig: deepCopy(clientRequest.multiFactorConfig), + testPhoneNumbers: deepCopy(clientRequest.testPhoneNumbers), + smsRegionConfig: deepCopy(clientRequest.smsRegionConfig), + recaptchaConfig: deepCopy(serverResponseWithRecaptcha.recaptchaConfig), + passwordPolicyConfig: deepCopy(clientRequest.passwordPolicyConfig), + emailPrivacyConfig: deepCopy(clientRequest.emailPrivacyConfig), + }); + }); + + it('should not populate optional fields if not available', () => { + const serverRequestCopyWithoutMfa: TenantServerResponse = deepCopy(serverResponseWithRecaptcha); + delete serverRequestCopyWithoutMfa.mfaConfig; + delete serverRequestCopyWithoutMfa.testPhoneNumbers; + delete serverRequestCopyWithoutMfa.smsRegionConfig; + delete serverRequestCopyWithoutMfa.recaptchaConfig; + delete serverRequestCopyWithoutMfa.passwordPolicyConfig; + delete serverRequestCopyWithoutMfa.emailPrivacyConfig; + expect(new Tenant(serverRequestCopyWithoutMfa).toJSON()).to.deep.equal({ + tenantId: 'TENANT-ID', + displayName: 'TENANT-DISPLAY-NAME', + emailSignInConfig: { + enabled: true, + passwordRequired: false, + }, + anonymousSignInEnabled: false, + }); + }); + }); +}); diff --git a/test/unit/auth/token-generator.spec.ts b/test/unit/auth/token-generator.spec.ts index 15d9bed73e..acb89b13e8 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"); @@ -16,21 +17,22 @@ 'use strict'; -// Use untyped import syntax for Node built-ins -import https = require('https'); - import * as _ from 'lodash'; import * as jwt from 'jsonwebtoken'; 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 LegacyFirebaseTokenGenerator = require('firebase-token-generator'); import * as mocks from '../../resources/mocks'; -import {FirebaseTokenGenerator} from '../../../src/auth/token-generator'; -import {Certificate} from '../../../src/auth/credential'; +import { + BLACKLISTED_CLAIMS, FirebaseTokenGenerator, EmulatedSigner, handleCryptoSignerError +} from '../../../src/auth/token-generator'; +import { CryptoSignerError, CryptoSignerErrorCode, ServiceAccountSigner } from '../../../src/utils/crypto-signer'; + +import { ServiceAccountCredential } from '../../../src/app/credential-internal'; +import { FirebaseAuthError } from '../../../src/utils/error'; +import * as utils from '../utils'; chai.should(); chai.use(sinonChai); @@ -38,74 +40,9 @@ chai.use(chaiAsPromised); const expect = chai.expect; - const ALGORITHM = 'RS256'; const ONE_HOUR_IN_SECONDS = 60 * 60; const FIREBASE_AUDIENCE = 'https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit'; -const BLACKLISTED_CLAIMS = [ - 'acr', 'amr', 'at_hash', 'aud', 'auth_time', 'azp', 'cnf', 'c_hash', 'exp', 'iat', 'iss', 'jti', - 'nbf', 'nonce', -]; - - -/** - * Returns a mocked out success response from the URL containing the public keys for the Google certs. - * - * @return {Object} A nock response object. - */ -function mockFetchPublicKeys(): nock.Scope { - const mockedResponse = {}; - mockedResponse[mocks.certificateObject.private_key_id] = mocks.keyPairs[0].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 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 = {}; - 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', - }); -} - -/** - * 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'); -} - /** * Verifies a token is signed with the private key corresponding to the provided public key. @@ -122,572 +59,322 @@ function verifyToken(token: string, publicKey: string): Promise { if (err) { reject(err); } else { - resolve(res); + resolve(res as object); } }); }); } - describe('FirebaseTokenGenerator', () => { - let tokenGenerator: FirebaseTokenGenerator; - - let clock: sinon.SinonFakeTimers; - let httpsSpy: sinon.SinonSpy; - beforeEach(() => { - tokenGenerator = new FirebaseTokenGenerator(new Certificate(mocks.certificateObject)); - httpsSpy = sinon.spy(https, 'get'); - }); + const tenantId = 'tenantId1'; + const cert = new ServiceAccountCredential(mocks.certificateObject); + let clock: sinon.SinonFakeTimers | undefined; afterEach(() => { if (clock) { clock.restore(); clock = undefined; } - httpsSpy.restore(); - }); - - after(() => { - nock.cleanAll(); }); describe('Constructor', () => { - it('should throw given no service account', () => { + it('should throw given no arguments', () => { expect(() => { // Need to overcome the type system to allow a call with no parameter const anyFirebaseTokenGenerator: any = FirebaseTokenGenerator; return new anyFirebaseTokenGenerator(); - }).to.throw('Must provide a certificate to use FirebaseTokenGenerator'); + }).to.throw('Must provide a CryptoSigner to use FirebaseTokenGenerator'); }); - const invalidCredentials = [null, NaN, 0, 1, true, false, '', 'a', [], {}, { a: 1 }, _.noop]; - invalidCredentials.forEach((invalidCredential) => { - it('should throw given invalid Credential: ' + JSON.stringify(invalidCredential), () => { + 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 FirebaseTokenGenerator(new Certificate(invalidCredential as any)); - }).to.throw(Error); + return new FirebaseTokenGenerator(invalidSigner as any); + }).to.throw('Must provide a CryptoSigner to use FirebaseTokenGenerator'); }); }); - it('should throw given an object without a "private_key" property', () => { - const invalidCertificate = _.omit(mocks.certificateObject, 'private_key'); - expect(() => { - return new FirebaseTokenGenerator(new Certificate(invalidCertificate as any)); - }).to.throw('Certificate object must contain a string "private_key" property'); + const invalidTenantIds = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop]; + invalidTenantIds.forEach((invalidTenantId) => { + it('should throw given a non-string tenantId', () => { + expect(() => { + return new FirebaseTokenGenerator(new ServiceAccountSigner(cert), invalidTenantId as any); + }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); + }); }); - it('should throw given an object with an empty string "private_key" property', () => { - const invalidCertificate = _.clone(mocks.certificateObject); - invalidCertificate.private_key = ''; + it('should throw given an empty string tenantId', () => { expect(() => { - return new FirebaseTokenGenerator(new Certificate(invalidCertificate as any)); - }).to.throw('Certificate object must contain a string "private_key" property'); + return new FirebaseTokenGenerator(new ServiceAccountSigner(cert), ''); + }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); }); + }); - it('should throw given an object without a "client_email" property', () => { - const invalidCertificate = _.omit(mocks.certificateObject, 'client_email'); - expect(() => { - return new FirebaseTokenGenerator(new Certificate(invalidCertificate as any)); - }).to.throw('Certificate object must contain a string "client_email" property'); - }); + const tokenGeneratorConfigs = [{ + name: 'createCustomToken()', + tokenGenerator: new FirebaseTokenGenerator(new ServiceAccountSigner(cert)), + }, { + name: 'createCustomToken() (tenant-aware)', + tokenGenerator: new FirebaseTokenGenerator(new ServiceAccountSigner(cert), tenantId), + }]; - it('should throw given an object without an empty string "client_email" property', () => { - const invalidCertificate = _.clone(mocks.certificateObject); - invalidCertificate.client_email = ''; - expect(() => { - return new FirebaseTokenGenerator(new Certificate(invalidCertificate as any)); - }).to.throw('Certificate object must contain a string "client_email" property'); - }); + describe('Emulator', () => { + const signer = new EmulatedSigner(); + const tokenGenerator = new FirebaseTokenGenerator(signer); - it('should not throw given a valid certificate', () => { - expect(() => { - return new FirebaseTokenGenerator(new Certificate(mocks.certificateObject)); - }).not.to.throw(); - }); + it('should generate a valid unsigned token', async () => { + const uid = 'uid123'; + const claims = { foo: 'bar' }; + const token = await tokenGenerator.createCustomToken(uid, claims); - it('should not throw given an object representing a certificate key', () => { - expect(() => { - return new FirebaseTokenGenerator(mocks.certificateObject); - }).not.to.throw(); + // Check that verify doesn't throw + // Note: the types for jsonwebtoken are wrong so we have to disguise the 'null' + jwt.verify(token, undefined as any, { 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(''); }); - }); + }); - describe('createCustomToken()', () => { - it('should throw given no uid', () => { - expect(() => { - (tokenGenerator as any).createCustomToken(); - }).to.throw('First argument to createCustomToken() must be a non-empty string uid'); - }); + tokenGeneratorConfigs.forEach((tokenGeneratorConfig) => { + describe(tokenGeneratorConfig.name, () => { + const tokenGenerator = tokenGeneratorConfig.tokenGenerator; - const invalidUids = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop]; - invalidUids.forEach((invalidUid) => { - it('should throw given a non-string uid: ' + JSON.stringify(invalidUid), () => { + it('should throw given no uid', () => { expect(() => { - tokenGenerator.createCustomToken(invalidUid as any); - }).to.throw('First argument to createCustomToken() must be a non-empty string uid'); + (tokenGenerator as any).createCustomToken(); + }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); }); - }); - - it('should throw given an empty string uid', () => { - expect(() => { - tokenGenerator.createCustomToken(''); - }).to.throw('First argument to createCustomToken() must be a non-empty string uid'); - }); - - it('should throw given a uid with a length greater than 128 characters', () => { - // uid of length 128 should be allowed - let uid = Array(129).join('a'); - expect(uid).to.have.length(128); - expect(() => { - tokenGenerator.createCustomToken(uid); - }).not.to.throw(); - // uid of length 129 should throw - uid = Array(130).join('a'); - expect(uid).to.have.length(129); - expect(() => { - tokenGenerator.createCustomToken(uid); - }).to.throw('First argument to createCustomToken() must a uid with less than or equal to 128 characters'); - }); - - it('should throw given a non-object developer claims', () => { - const invalidDeveloperClaims = [null, NaN, [], true, false, '', 'a', 0, 1, Infinity, _.noop]; - invalidDeveloperClaims.forEach((invalidDevClaims: any) => { - expect(() => { - tokenGenerator.createCustomToken(mocks.uid, invalidDevClaims); - }).to.throw('Second argument to createCustomToken() must be an object containing the developer claims'); + const invalidUids = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop]; + invalidUids.forEach((invalidUid) => { + it('should throw given a non-string uid: ' + JSON.stringify(invalidUid), () => { + expect(() => { + tokenGenerator.createCustomToken(invalidUid as any); + }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); + }); }); - }); - BLACKLISTED_CLAIMS.forEach((blacklistedClaim) => { - it('should throw given a developer claims object with a blacklisted claim: ' + blacklistedClaim, () => { - const blacklistedDeveloperClaims = _.clone(mocks.developerClaims); - blacklistedDeveloperClaims[blacklistedClaim] = true; + it('should throw given an empty string uid', () => { expect(() => { - tokenGenerator.createCustomToken(mocks.uid, blacklistedDeveloperClaims); - }).to.throw('Developer claim "' + blacklistedClaim + '" is reserved and cannot be specified'); + tokenGenerator.createCustomToken(''); + }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); }); - }); - - it('should throw if the token generator was initialized with no "private_key"', () => { - const certificateObjectWithNoPrivateKey: any = _.omit(mocks.certificateObject, 'private_key'); - certificateObjectWithNoPrivateKey.clientEmail = certificateObjectWithNoPrivateKey.client_email; - const tokenGeneratorWithNoPrivateKey = new FirebaseTokenGenerator(certificateObjectWithNoPrivateKey); - - expect(() => { - tokenGeneratorWithNoPrivateKey.createCustomToken(mocks.uid); - }).to.throw('createCustomToken() requires a certificate with "private_key" set'); - }); - - it('should throw if the token generator was initialized with no "client_email"', () => { - const certificateObjectWithNoClientEmail: any = _.omit(mocks.certificateObject, 'client_email'); - certificateObjectWithNoClientEmail.privateKey = certificateObjectWithNoClientEmail.private_key; - const tokenGeneratorWithNoClientEmail = new FirebaseTokenGenerator(certificateObjectWithNoClientEmail); - - expect(() => { - tokenGeneratorWithNoClientEmail.createCustomToken(mocks.uid); - }).to.throw('createCustomToken() requires a certificate with "client_email" set'); - }); - - it('should be fulfilled given a valid uid and no developer claims', () => { - return tokenGenerator.createCustomToken(mocks.uid); - }); - - it('should be fulfilled given a valid uid and empty object developer claims', () => { - return tokenGenerator.createCustomToken(mocks.uid, {}); - }); - - it('should be fulfilled given a valid uid and valid developer claims', () => { - return tokenGenerator.createCustomToken(mocks.uid, mocks.developerClaims); - }); - - it('should be fulfilled with a Firebase Custom JWT', () => { - return tokenGenerator.createCustomToken(mocks.uid) - .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(mocks.uid) - .then((token) => { - const decoded = jwt.decode(token); - - expect(decoded).to.deep.equal({ - uid: mocks.uid, - iat: 1, - exp: ONE_HOUR_IN_SECONDS + 1, - aud: FIREBASE_AUDIENCE, - iss: mocks.certificateObject.client_email, - sub: mocks.certificateObject.client_email, - }); - }); - }); - - it('should be fulfilled with a JWT with the developer claims in its decoded payload', () => { - clock = sinon.useFakeTimers(1000); - - return tokenGenerator.createCustomToken(mocks.uid, mocks.developerClaims) - .then((token) => { - const decoded = jwt.decode(token); - - expect(decoded).to.deep.equal({ - uid: mocks.uid, - iat: 1, - exp: ONE_HOUR_IN_SECONDS + 1, - aud: FIREBASE_AUDIENCE, - iss: mocks.certificateObject.client_email, - sub: mocks.certificateObject.client_email, - claims: { - one: 'uno', - two: 'dos', - }, - }); - }); - }); - - it('should be fulfilled with a JWT with the correct header', () => { - clock = sinon.useFakeTimers(1000); - - return tokenGenerator.createCustomToken(mocks.uid) - .then((token) => { - const decoded: any = jwt.decode(token, { - complete: true, - }); - expect(decoded.header).to.deep.equal({ - typ: 'JWT', - alg: ALGORITHM, - }); - }); - }); - - it('should be fulfilled with a JWT which can be verified by the service account public key', () => { - return tokenGenerator.createCustomToken(mocks.uid) - .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(mocks.uid) - .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; - return tokenGenerator.createCustomToken(mocks.uid) - .then((result) => { - token = result; - - clock.tick((ONE_HOUR_IN_SECONDS * 1000) - 1); + it('should throw given a uid with a length greater than 128 characters', () => { + // uid of length 128 should be allowed + let uid = Array(129).join('a'); + expect(uid).to.have.length(128); + expect(() => { + tokenGenerator.createCustomToken(uid); + }).not.to.throw(); - // Token should still be valid - return verifyToken(token, mocks.keyPairs[0].public); - }) - .then(() => { - clock.tick(1); + // uid of length 129 should throw + uid = Array(130).join('a'); + expect(uid).to.have.length(129); + expect(() => { + tokenGenerator.createCustomToken(uid); + }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); + }); - // Token should now be invalid - return verifyToken(token, mocks.keyPairs[0].public) - .should.eventually.be.rejectedWith('jwt expired'); + it('should throw given a non-object developer claims', () => { + const invalidDeveloperClaims: any[] = [null, NaN, [], true, false, '', 'a', 0, 1, Infinity, _.noop]; + invalidDeveloperClaims.forEach((invalidDevClaims) => { + expect(() => { + tokenGenerator.createCustomToken(mocks.uid, invalidDevClaims); + }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); }); - }); + }); - it('should not mutate the passed in developer claims', () => { - const originalClaims = { - foo: 'bar', - }; - const clonedClaims = _.clone(originalClaims); - return tokenGenerator.createCustomToken(mocks.uid, clonedClaims) - .then(() => { - expect(originalClaims).to.deep.equal(clonedClaims); + 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); + blacklistedDeveloperClaims[blacklistedClaim] = true; + expect(() => { + tokenGenerator.createCustomToken(mocks.uid, blacklistedDeveloperClaims); + }).to.throw(FirebaseAuthError, blacklistedClaim).with.property('code', 'auth/argument-error'); }); - }); - }); - - - describe('verifyIdToken()', () => { - let mockedRequests: nock.Scope[] = []; - - afterEach(() => { - _.forEach(mockedRequests, (mockedRequest) => mockedRequest.done()); - mockedRequests = []; - }); - - it('should throw given no ID token', () => { - expect(() => { - (tokenGenerator as any).verifyIdToken(); - }).to.throw('First argument to verifyIdToken() must be a Firebase ID token'); - }); - - const invalidIdTokens = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop]; - invalidIdTokens.forEach((invalidIdToken) => { - it('should throw given a non-string ID token: ' + JSON.stringify(invalidIdToken), () => { - expect(() => { - tokenGenerator.verifyIdToken(invalidIdToken as any); - }).to.throw('First argument to verifyIdToken() must be a Firebase ID token'); }); - }); - - it('should throw given an empty string ID token', () => { - return tokenGenerator.verifyIdToken('') - .should.eventually.be.rejectedWith('Decoding Firebase ID token failed'); - }); - - it('should be rejected given an invalid ID token', () => { - return tokenGenerator.verifyIdToken('invalid-token') - .should.eventually.be.rejectedWith('Decoding Firebase ID token failed'); - }); - it('should throw if the token generator was initialized with no "project_id"', () => { - const certificateObjectWithNoProjectId: any = _.omit(mocks.certificateObject, 'project_id'); - const tokenGeneratorWithNoProjectId = new FirebaseTokenGenerator(certificateObjectWithNoProjectId); - - const mockIdToken = mocks.generateIdToken(); - - expect(() => { - tokenGeneratorWithNoProjectId.verifyIdToken(mockIdToken); - }).to.throw('verifyIdToken() requires a certificate with "project_id" set'); - }); - - it('should be rejected given an ID token with no kid', () => { - const mockIdToken = mocks.generateIdToken({ - header: {foo: 'bar'}, + it('should be fulfilled given a valid uid and no developer claims', () => { + return tokenGenerator.createCustomToken(mocks.uid); }); - return tokenGenerator.verifyIdToken(mockIdToken) - .should.eventually.be.rejectedWith('Firebase ID token has no "kid" claim'); - }); - it('should be rejected given an ID token with a kid which does not match any of the actual public keys', () => { - mockedRequests.push(mockFetchPublicKeys()); - - const mockIdToken = mocks.generateIdToken({ - header: { - kid: 'wrongkid', - }, + it('should be fulfilled given a valid uid and empty object developer claims', () => { + return tokenGenerator.createCustomToken(mocks.uid, {}); }); - return tokenGenerator.verifyIdToken(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 an ID token with an incorrect algorithm', () => { - const mockIdToken = mocks.generateIdToken({ - algorithm: 'HS256', + it('should be fulfilled given a valid uid and valid developer claims', () => { + return tokenGenerator.createCustomToken(mocks.uid, mocks.developerClaims); }); - return tokenGenerator.verifyIdToken(mockIdToken) - .should.eventually.be.rejectedWith('Firebase ID token has incorrect algorithm'); - }); - it('should be rejected given an ID token with an incorrect audience', () => { - const mockIdToken = mocks.generateIdToken({ - audience: 'incorrectAudience', + it('should be fulfilled with a Firebase Custom JWT', () => { + return tokenGenerator.createCustomToken(mocks.uid) + .should.eventually.be.a('string').and.not.be.empty; }); - return tokenGenerator.verifyIdToken(mockIdToken) - .should.eventually.be.rejectedWith('Firebase ID token has incorrect "aud" (audience) claim'); - }); - - it('should be rejected given an ID token with an incorrect issuer', () => { - const mockIdToken = mocks.generateIdToken({ - issuer: 'incorrectIssuer', + it('should be fulfilled with a JWT with the correct decoded payload', () => { + clock = sinon.useFakeTimers(1000); + + return tokenGenerator.createCustomToken(mocks.uid) + .then((token) => { + const decoded = jwt.decode(token); + const expected: { [key: string]: any } = { + uid: mocks.uid, + iat: 1, + exp: ONE_HOUR_IN_SECONDS + 1, + aud: FIREBASE_AUDIENCE, + iss: mocks.certificateObject.client_email, + sub: mocks.certificateObject.client_email, + }; + + if (tokenGenerator.tenantId) { + expected.tenant_id = tokenGenerator.tenantId; + } + + expect(decoded).to.deep.equal(expected); + }); }); - return tokenGenerator.verifyIdToken(mockIdToken) - .should.eventually.be.rejectedWith('Firebase ID token has incorrect "iss" (issuer) claim'); - }); - - // TODO(jwenger): jsonwebtoken no longer allows the subject to be empty, so we need to find a - // new way to test this - xit('should be rejected given an ID token with an empty string subject', () => { - const mockIdToken = mocks.generateIdToken({ - subject: '', + it('should be fulfilled with a JWT with the developer claims in its decoded payload', () => { + clock = sinon.useFakeTimers(1000); + + return tokenGenerator.createCustomToken(mocks.uid, mocks.developerClaims) + .then((token) => { + const decoded = jwt.decode(token); + + const expected: { [key: string]: any } = { + uid: mocks.uid, + iat: 1, + exp: ONE_HOUR_IN_SECONDS + 1, + aud: FIREBASE_AUDIENCE, + iss: mocks.certificateObject.client_email, + sub: mocks.certificateObject.client_email, + claims: { + one: 'uno', + two: 'dos', + }, + }; + + if (tokenGenerator.tenantId) { + expected.tenant_id = tokenGenerator.tenantId; + } + + expect(decoded).to.deep.equal(expected); + }); }); - return tokenGenerator.verifyIdToken(mockIdToken) - .should.eventually.be.rejectedWith('Firebase ID token has an empty string "sub" (subject) claim'); - }); - - // TODO(jwenger): jsonwebtoken no longer allows the subject to be a non-string, so we need to - // find a new way to test this - xit('should be rejected given an ID token with a non-string subject', () => { - const mockIdToken = mocks.generateIdToken({ - subject: 100, + it('should be fulfilled with a JWT with the correct header', () => { + clock = sinon.useFakeTimers(1000); + + return tokenGenerator.createCustomToken(mocks.uid) + .then((token) => { + const decoded: any = jwt.decode(token, { + complete: true, + }); + expect(decoded.header).to.deep.equal({ + alg: ALGORITHM, + typ: 'JWT', + }); + }); }); - return tokenGenerator.verifyIdToken(mockIdToken) - .should.eventually.be.rejectedWith('Firebase ID token has no "sub" (subject) claim'); - }); - - it('should be rejected given an ID token with a subject with greater than 128 characters', () => { - mockedRequests.push(mockFetchPublicKeys()); - - // uid of length 128 should be fulfilled - let uid = Array(129).join('a'); - expect(uid).to.have.length(128); - let mockIdToken = mocks.generateIdToken({ - subject: uid, + it('should be fulfilled with a JWT which can be verified by the service account public key', () => { + return tokenGenerator.createCustomToken(mocks.uid) + .then((token) => { + return verifyToken(token, mocks.keyPairs[0].public); + }); }); - return tokenGenerator.verifyIdToken(mockIdToken).then(() => { - // uid of length 129 should be rejected - uid = Array(130).join('a'); - expect(uid).to.have.length(129); - mockIdToken = mocks.generateIdToken({ - subject: uid, - }); - return tokenGenerator.verifyIdToken(mockIdToken) - .should.eventually.be.rejectedWith('Firebase ID token has "sub" (subject) claim longer than 128 characters'); + it('should be fulfilled with a JWT which cannot be verified by a random public key', () => { + return tokenGenerator.createCustomToken(mocks.uid) + .then((token) => { + return verifyToken(token, mocks.keyPairs[1].public) + .should.eventually.be.rejectedWith('invalid signature'); + }); }); - }); - - it('should be rejected given an expired ID token', () => { - mockedRequests.push(mockFetchPublicKeys()); - clock = sinon.useFakeTimers(1000); + it('should be fulfilled with a JWT which expires after one hour', () => { + clock = sinon.useFakeTimers(1000); - const mockIdToken = mocks.generateIdToken(); + let token: string; + return tokenGenerator.createCustomToken(mocks.uid) + .then((result) => { + token = result; - clock.tick((ONE_HOUR_IN_SECONDS * 1000) - 1); + clock!.tick((ONE_HOUR_IN_SECONDS * 1000) - 1); - // Token should still be valid - return tokenGenerator.verifyIdToken(mockIdToken).then(() => { - clock.tick(1); + // Token should still be valid + return verifyToken(token, mocks.keyPairs[0].public); + }) + .then(() => { + clock!.tick(1); - // Token should now be invalid - return tokenGenerator.verifyIdToken(mockIdToken) - .should.eventually.be.rejectedWith('Firebase ID token has expired. Get a fresh token from your client ' + - 'app and try again (auth/id-token-expired)'); + // Token should now be invalid + return verifyToken(token, mocks.keyPairs[0].public) + .should.eventually.be.rejectedWith('jwt expired'); + }); }); - }); - - it('should be rejected given an ID token which was not signed with the kid it specifies', () => { - mockedRequests.push(mockFetchWrongPublicKeys()); - - const mockIdToken = mocks.generateIdToken(); - - return tokenGenerator.verifyIdToken(mockIdToken) - .should.eventually.be.rejectedWith('Firebase ID token has invalid signature'); - }); - - it('should be rejected given a custom token', () => { - return tokenGenerator.createCustomToken(mocks.uid) - .then((customToken) => { - return tokenGenerator.verifyIdToken(customToken) - .should.eventually.be.rejectedWith('verifyIdToken() expects an ID token, but was given a custom token'); - }); - }); - it('should be rejected given a legacy custom token', () => { - const legacyTokenGenerator = new LegacyFirebaseTokenGenerator('foo'); - const legacyCustomToken = legacyTokenGenerator.createToken({ - uid: mocks.uid, + it('should not mutate the passed in developer claims', () => { + const originalClaims = { + foo: 'bar', + }; + const clonedClaims = _.clone(originalClaims); + return tokenGenerator.createCustomToken(mocks.uid, clonedClaims) + .then(() => { + expect(originalClaims).to.deep.equal(clonedClaims); + }); }); - - return tokenGenerator.verifyIdToken(legacyCustomToken) - .should.eventually.be.rejectedWith('verifyIdToken() expects an ID token, but was given a legacy custom token'); - }); - - it('should be fulfilled with decoded claims given a valid ID token', () => { - mockedRequests.push(mockFetchPublicKeys()); - - clock = sinon.useFakeTimers(1000); - - const mockIdToken = mocks.generateIdToken(); - - return tokenGenerator.verifyIdToken(mockIdToken) - .should.eventually.be.fulfilled.and.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 fetch the Google cert public keys until the first time verifyIdToken() is called', () => { - mockedRequests.push(mockFetchPublicKeys()); - - const anotherTokenGenerator = new FirebaseTokenGenerator(new Certificate(mocks.certificateObject)); - expect(https.get).not.to.have.been.called; - - const mockIdToken = mocks.generateIdToken(); - - return anotherTokenGenerator.verifyIdToken(mockIdToken) - .then(() => expect(https.get).to.have.been.calledOnce); - }); - - it('should not re-fetch the Google cert public keys every time verifyIdToken() is called', () => { - mockedRequests.push(mockFetchPublicKeys()); - - const mockIdToken = mocks.generateIdToken(); - - return tokenGenerator.verifyIdToken(mockIdToken).then(() => { - expect(https.get).to.have.been.calledOnce; - return tokenGenerator.verifyIdToken(mockIdToken); - }).then(() => expect(https.get).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 tokenGenerator.verifyIdToken(mockIdToken).then(() => { - expect(https.get).to.have.been.calledOnce; - clock.tick(999); - return tokenGenerator.verifyIdToken(mockIdToken); - }).then(() => { - expect(https.get).to.have.been.calledOnce; - clock.tick(1); - return tokenGenerator.verifyIdToken(mockIdToken); - }).then(() => { - // One second has passed - expect(https.get).to.have.been.calledTwice; - clock.tick(999); - return tokenGenerator.verifyIdToken(mockIdToken); - }).then(() => { - expect(https.get).to.have.been.calledTwice; - clock.tick(1); - return tokenGenerator.verifyIdToken(mockIdToken); - }).then(() => { - // Two seconds have passed - expect(https.get).to.have.been.calledThrice; + 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 be rejected if fetching the Google public keys fails', () => { - mockedRequests.push(mockFailedFetchPublicKeys()); - - const mockIdToken = mocks.generateIdToken(); - - return tokenGenerator.verifyIdToken(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 tokenGenerator.verifyIdToken(mockIdToken) - .should.eventually.be.rejectedWith('Error fetching public keys for Google certs: message (description)'); + 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 new file mode 100644 index 0000000000..2d01678ef7 --- /dev/null +++ b/test/unit/auth/token-verifier.spec.ts @@ -0,0 +1,821 @@ +/*! + * Copyright 2018 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 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'; +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/app/credential-internal'; +import { FirebaseApp } from '../../../src/app/firebase-app'; +import { AuthClientErrorCode } from '../../../src/utils/error'; +import { JwtError, JwtErrorCode, PublicKeySignatureVerifier } from '../../../src/utils/jwt'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +const ONE_HOUR_IN_SECONDS = 60 * 60; +const TEN_MINUTES_IN_SECONDS = 10 * 60; + +function createTokenVerifier( + app: FirebaseApp +): verifier.FirebaseTokenVerifier { + return new verifier.FirebaseTokenVerifier( + 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com', + 'https://securetoken.google.com/', + verifier.ID_TOKEN_INFO, + app + ); +} + +function createAuthBlockingTokenVerifier( + app: FirebaseApp +): verifier.FirebaseTokenVerifier { + return new verifier.FirebaseTokenVerifier( + 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com', + 'https://securetoken.google.com/', + verifier.AUTH_BLOCKING_TOKEN_INFO, + app + ); +} + + +describe('FirebaseTokenVerifier', () => { + let app: FirebaseApp; + let tokenVerifier: verifier.FirebaseTokenVerifier; + let authBlockingTokenVerifier: verifier.FirebaseTokenVerifier; + let tokenGenerator: FirebaseTokenGenerator; + let clock: sinon.SinonFakeTimers | undefined; + 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); + authBlockingTokenVerifier = createAuthBlockingTokenVerifier(app); + }); + + afterEach(() => { + if (clock) { + clock.restore(); + clock = undefined; + } + }); + + after(() => { + nock.cleanAll(); + }); + + describe('Constructor', () => { + it('should not throw when valid arguments are provided', () => { + expect(() => { + tokenVerifier = new verifier.FirebaseTokenVerifier( + 'https://www.example.com/publicKeys', + 'https://www.example.com/issuer/', + { + url: 'https://docs.example.com/verify-tokens', + verifyApiName: 'verifyToken()', + jwtName: 'Important Token', + shortName: 'token', + expiredErrorCode: AuthClientErrorCode.INVALID_ARGUMENT, + }, + app, + ); + }).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 verifier.FirebaseTokenVerifier( + invalidCertUrl as any, + 'https://www.example.com/issuer/', + verifier.ID_TOKEN_INFO, + app, + ); + }).to.throw('The provided public client certificate URL is an invalid URL.'); + }); + }); + + 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', + invalidIssuer as any, + verifier.ID_TOKEN_INFO, + app, + ); + }).to.throw('The provided JWT issuer is an invalid URL.'); + }); + }); + + const invalidVerifyApiNames = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop, '']; + invalidVerifyApiNames.forEach((invalidVerifyApiName) => { + it('should throw given an invalid verify API name: ' + JSON.stringify(invalidVerifyApiName), () => { + expect(() => { + new verifier.FirebaseTokenVerifier( + 'https://www.example.com/publicKeys', + 'https://www.example.com/issuer/', + { + url: 'https://docs.example.com/verify-tokens', + verifyApiName: invalidVerifyApiName as any, + jwtName: 'Important Token', + shortName: 'token', + expiredErrorCode: AuthClientErrorCode.INVALID_ARGUMENT, + }, + app, + ); + }).to.throw('The JWT verify API name must be a non-empty string.'); + }); + }); + + const invalidJwtNames = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop, '']; + invalidJwtNames.forEach((invalidJwtName) => { + it('should throw given an invalid JWT full name: ' + JSON.stringify(invalidJwtName), () => { + expect(() => { + new verifier.FirebaseTokenVerifier( + 'https://www.example.com/publicKeys', + 'https://www.example.com/issuer/', + { + url: 'https://docs.example.com/verify-tokens', + verifyApiName: 'verifyToken()', + jwtName: invalidJwtName as any, + shortName: 'token', + expiredErrorCode: AuthClientErrorCode.INVALID_ARGUMENT, + }, + app, + ); + }).to.throw('The JWT public full name must be a non-empty string.'); + }); + }); + + const invalidShortNames = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop, '']; + invalidShortNames.forEach((invalidShortName) => { + it('should throw given an invalid JWT short name: ' + JSON.stringify(invalidShortName), () => { + expect(() => { + new verifier.FirebaseTokenVerifier( + 'https://www.example.com/publicKeys', + 'https://www.example.com/issuer/', + { + url: 'https://docs.example.com/verify-tokens', + verifyApiName: 'verifyToken()', + jwtName: 'Important Token', + shortName: invalidShortName as any, + expiredErrorCode: AuthClientErrorCode.INVALID_ARGUMENT, + }, + app, + ); + }).to.throw('The JWT public short name must be a non-empty string.'); + }); + }); + + const invalidExpiredErrorCodes = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop, '', 'test']; + invalidExpiredErrorCodes.forEach((invalidExpiredErrorCode) => { + it('should throw given an invalid expiration error code: ' + JSON.stringify(invalidExpiredErrorCode), () => { + expect(() => { + new verifier.FirebaseTokenVerifier( + 'https://www.example.com/publicKeys', + 'https://www.example.com/issuer/', + { + url: 'https://docs.example.com/verify-tokens', + verifyApiName: 'verifyToken()', + jwtName: 'Important Token', + shortName: 'token', + expiredErrorCode: invalidExpiredErrorCode as any, + }, + app, + ); + }).to.throw('The JWT expiration error code must be a non-null ErrorInfo object.'); + }); + }); + }); + + 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', () => { + expect(() => { + (tokenVerifier as any).verifyJWT(); + }).to.throw('First argument to verifyIdToken() must be a Firebase ID token'); + }); + + const invalidIdTokens = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop]; + invalidIdTokens.forEach((invalidIdToken) => { + it('should throw given a non-string Firebase JWT token: ' + JSON.stringify(invalidIdToken), () => { + expect(() => { + tokenVerifier.verifyJWT(invalidIdToken as any); + }).to.throw('First argument to verifyIdToken() must be a Firebase ID token'); + }); + }); + + it('should throw given an empty string Firebase JWT token', () => { + return tokenVerifier.verifyJWT('') + .should.eventually.be.rejectedWith('Decoding Firebase ID token failed'); + }); + + it('should be rejected given an invalid Firebase JWT token', () => { + return tokenVerifier.verifyJWT('invalid-token') + .should.eventually.be.rejectedWith('Decoding Firebase ID token failed'); + }); + + 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', + 'https://securetoken.google.com/', + verifier.ID_TOKEN_INFO, + mocks.mockCredentialApp(), + ); + const mockIdToken = mocks.generateIdToken(); + const expected = 'Must initialize app with a cert credential or set your Firebase project ID as ' + + 'the GOOGLE_CLOUD_PROJECT environment variable to call verifyIdToken().'; + return tokenVerifierWithNoProjectId.verifyJWT(mockIdToken) + .should.eventually.be.rejectedWith(expected); + }); + + it('should be rejected given a Firebase JWT token with no kid', () => { + const mockIdToken = mocks.generateIdToken({ + header: { foo: 'bar' }, + }); + return tokenVerifier.verifyJWT(mockIdToken) + .should.eventually.be.rejectedWith('Firebase ID token has no "kid" claim'); + }); + + it('should be rejected given a Firebase JWT token with an incorrect algorithm', () => { + const mockIdToken = mocks.generateIdToken({ + algorithm: 'PS256', + }); + return tokenVerifier.verifyJWT(mockIdToken) + .should.eventually.be.rejectedWith('Firebase ID token has incorrect algorithm'); + }); + + it('should be rejected given a Firebase JWT token with an incorrect audience', () => { + const mockIdToken = mocks.generateIdToken({ + audience: 'incorrectAudience', + }); + + return tokenVerifier.verifyJWT(mockIdToken) + .should.eventually.be.rejectedWith('Firebase ID token has incorrect "aud" (audience) claim'); + }); + + it('should be rejected given a Firebase JWT token with an incorrect issuer', () => { + const mockIdToken = mocks.generateIdToken({ + issuer: 'incorrectIssuer', + }); + + return tokenVerifier.verifyJWT(mockIdToken) + .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', () => { + const verifierStub = sinon.stub(PublicKeySignatureVerifier.prototype, 'verify') + .resolves(); + stubs.push(verifierStub); + + // uid of length 128 should be fulfilled + let uid = Array(129).join('a'); + expect(uid).to.have.length(128); + let mockIdToken = mocks.generateIdToken({ + subject: uid, + }); + return tokenVerifier.verifyJWT(mockIdToken).then(() => { + // uid of length 129 should be rejected + uid = Array(130).join('a'); + expect(uid).to.have.length(129); + mockIdToken = mocks.generateIdToken({ + subject: uid, + }); + + return tokenVerifier.verifyJWT(mockIdToken) + .should.eventually.be.rejectedWith('Firebase ID token has a "sub" (subject) claim longer than 128 ' + + 'characters'); + }); + }); + + 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(); + + 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 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', + 'https://session.firebase.google.com/', + verifier.SESSION_COOKIE_INFO, + app, + ); + + const mockSessionCookie = mocks.generateSessionCookie(); + + 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'); + }); + + 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); + + const mockIdToken = mocks.generateIdToken(); + + return tokenVerifier.verifyJWT(mockIdToken) + .should.eventually.be.rejectedWith('Firebase ID 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 public keys.')); + stubs.push(verifierStub); + + const mockIdToken = mocks.generateIdToken(); + + return tokenVerifier.verifyJWT(mockIdToken) + .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', () => { + return tokenGenerator.createCustomToken(mocks.uid) + .then((customToken) => { + return tokenVerifier.verifyJWT(customToken) + .should.eventually.be.rejectedWith('verifyIdToken() expects an ID token, but was given a custom token'); + }); + }); + + 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', + 'https://session.firebase.google.com/', + verifier.SESSION_COOKIE_INFO, + app, + ); + return tokenGenerator.createCustomToken(mocks.uid) + .then((customToken) => { + return tokenVerifierSessionCookie.verifyJWT(customToken) + .should.eventually.be.rejectedWith( + 'verifySessionCookie() expects a session cookie, but was given a custom token'); + }); + }); + + it('should be rejected given a legacy custom token with error using article "an" before JWT short name', () => { + const legacyTokenGenerator = new LegacyFirebaseTokenGenerator('foo'); + const legacyCustomToken = legacyTokenGenerator.createToken({ + uid: mocks.uid, + }); + + return tokenVerifier.verifyJWT(legacyCustomToken) + .should.eventually.be.rejectedWith('verifyIdToken() expects an ID token, but was given a legacy custom token'); + }); + + 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', + 'https://session.firebase.google.com/', + verifier.SESSION_COOKIE_INFO, + app, + ); + const legacyTokenGenerator = new LegacyFirebaseTokenGenerator('foo'); + const legacyCustomToken = legacyTokenGenerator.createToken({ + uid: mocks.uid, + }); + + return tokenVerifierSessionCookie.verifyJWT(legacyCustomToken) + .should.eventually.be.rejectedWith( + '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', () => { + const verifierStub = sinon.stub(PublicKeySignatureVerifier.prototype, 'verify') + .resolves(); + stubs.push(verifierStub); + + clock = sinon.useFakeTimers(1000); + + const mockIdToken = mocks.generateIdToken(); + + return tokenVerifier.verifyJWT(mockIdToken) + .should.eventually.be.fulfilled.and.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 decode an unsigned token if isEmulator=true', async () => { + clock = sinon.useFakeTimers(1000); + + const emulatorVerifier = createTokenVerifier(app); + const mockIdToken = mocks.generateIdToken({ + algorithm: 'none', + header: {} + }, undefined, 'secret'); + + const isEmulator = true; + const decoded = await emulatorVerifier.verifyJWT(mockIdToken, isEmulator); + 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 an unsigned token when the algorithm is not overridden (emulator)', async () => { + clock = sinon.useFakeTimers(1000); + + const idTokenNoAlg = mocks.generateIdToken({ + algorithm: 'none', + }, undefined, 'secret'); + await tokenVerifier.verifyJWT(idTokenNoAlg) + .should.eventually.be.rejectedWith('Firebase ID token has incorrect algorithm.'); + + const idTokenNoHeader = mocks.generateIdToken({ + algorithm: 'none', + header: {} + }, undefined, 'secret'); + await tokenVerifier.verifyJWT(idTokenNoHeader) + .should.eventually.be.rejectedWith('Firebase ID token has no "kid" claim.'); + }); + }); + + describe('_verifyAuthBlockingToken()', () => { + 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 Auth Blocking JWT token', () => { + expect(() => { + (authBlockingTokenVerifier as any)._verifyAuthBlockingToken(); + }).to.throw('First argument to _verifyAuthBlockingToken() must be a Firebase Auth Blocking token'); + }); + + const invalidAuthBlockingTokens = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop]; + invalidAuthBlockingTokens.forEach((invalidAuthBlockingToken) => { + it('should throw given a non-string Auth Blocking JWT token: ' + JSON.stringify(invalidAuthBlockingToken), () => { + expect(() => { + authBlockingTokenVerifier._verifyAuthBlockingToken(invalidAuthBlockingToken as any, false, undefined); + }).to.throw('First argument to _verifyAuthBlockingToken() must be a Firebase Auth Blocking token'); + }); + }); + + it('should throw given an empty string Auth Blocking JWT token', () => { + return authBlockingTokenVerifier._verifyAuthBlockingToken('', false, undefined) + .should.eventually.be.rejectedWith('Decoding Firebase Auth Blocking token failed'); + }); + + it('should be rejected given an invalid Auth Blocking JWT token', () => { + return authBlockingTokenVerifier._verifyAuthBlockingToken('invalid-token', false, undefined) + .should.eventually.be.rejectedWith('Decoding Firebase Auth Blocking token failed'); + }); + + 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', + 'https://securetoken.google.com/', + verifier.AUTH_BLOCKING_TOKEN_INFO, + mocks.mockCredentialApp(), + ); + const mockAuthBlockingToken = mocks.generateAuthBlockingToken(); + const expected = 'Must initialize app with a cert credential or set your Firebase project ID as ' + + 'the GOOGLE_CLOUD_PROJECT environment variable to call _verifyAuthBlockingToken().'; + return tokenVerifierWithNoProjectId._verifyAuthBlockingToken(mockAuthBlockingToken, false, undefined) + .should.eventually.be.rejectedWith(expected); + }); + + it('should be rejected given a Auth Blocking JWT token with no kid', () => { + const mockAuthBlockingToken = mocks.generateAuthBlockingToken({ + header: { foo: 'bar' }, + }); + return authBlockingTokenVerifier._verifyAuthBlockingToken(mockAuthBlockingToken, false, undefined) + .should.eventually.be.rejectedWith('Firebase Auth Blocking token has no "kid" claim'); + }); + + it('should be rejected given a Auth Blocking JWT token with an incorrect algorithm', () => { + const mockAuthBlockingToken = mocks.generateAuthBlockingToken({ + algorithm: 'PS256', + }); + return authBlockingTokenVerifier._verifyAuthBlockingToken(mockAuthBlockingToken, false, undefined) + .should.eventually.be.rejectedWith('Firebase Auth Blocking token has incorrect algorithm'); + }); + + it('should be rejected given an Auth Blocking JWT token that is not a cloud functions url', () => { + const mockAuthBlockingToken = mocks.generateIdToken({ + audience: 'incorrectAudience', + }); + + return authBlockingTokenVerifier._verifyAuthBlockingToken(mockAuthBlockingToken, false, undefined) + .should.eventually.be.rejectedWith('Firebase Auth Blocking token has incorrect "aud" (audience) claim'); + }); + + it('should be rejected given a Auth Blocking JWT token with an incorrect audience', () => { + const mockAuthBlockingToken = mocks.generateIdToken({ + audience: 'https://resource-someotherurl.net/', + }); + + return authBlockingTokenVerifier._verifyAuthBlockingToken(mockAuthBlockingToken, false, 'someurl.net/') + .should.eventually.be.rejectedWith('Firebase Auth Blocking token has incorrect "aud" (audience) claim'); + }); + + it('should be rejected given a Auth Blocking JWT token with an incorrect issuer', () => { + const mockAuthBlockingToken = mocks.generateAuthBlockingToken({ + issuer: 'incorrectIssuer', + }); + + return authBlockingTokenVerifier._verifyAuthBlockingToken(mockAuthBlockingToken, false, undefined) + .should.eventually.be.rejectedWith('Firebase Auth Blocking 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 Auth Blocking.')); + stubs.push(verifierStub); + + const mockAuthBlockingToken = mocks.generateAuthBlockingToken({ + header: { + kid: 'wrongkid', + }, + }); + + return authBlockingTokenVerifier._verifyAuthBlockingToken(mockAuthBlockingToken, false, undefined) + .should.eventually.be.rejectedWith('Firebase Auth Blocking token has "kid" claim which does not ' + + 'correspond to a known public key'); + }); + + it('should be rejected given a Auth Blocking JWT token with a subject with greater than 128 characters', () => { + const verifierStub = sinon.stub(PublicKeySignatureVerifier.prototype, 'verify') + .resolves(); + stubs.push(verifierStub); + + // uid of length 128 should be fulfilled + let uid = Array(129).join('a'); + expect(uid).to.have.length(128); + let mockAuthBlockingToken = mocks.generateAuthBlockingToken({ + subject: uid, + }); + return authBlockingTokenVerifier._verifyAuthBlockingToken(mockAuthBlockingToken, false, undefined).then(() => { + // uid of length 129 should be rejected + uid = Array(130).join('a'); + expect(uid).to.have.length(129); + mockAuthBlockingToken = mocks.generateAuthBlockingToken({ + subject: uid, + }); + + return authBlockingTokenVerifier._verifyAuthBlockingToken(mockAuthBlockingToken, false, undefined) + .should.eventually.be.rejectedWith( + 'Firebase Auth Blocking token has a "sub" (subject) claim longer than 128 characters'); + }); + }); + + it('should be rejected when the verifier throws for expired Auth Blocking JWT token', () => { + const verifierStub = sinon.stub(PublicKeySignatureVerifier.prototype, 'verify') + .rejects(new JwtError(JwtErrorCode.TOKEN_EXPIRED, 'Expired token.')); + stubs.push(verifierStub); + + const mockAuthBlockingToken = mocks.generateAuthBlockingToken(); + + return authBlockingTokenVerifier._verifyAuthBlockingToken(mockAuthBlockingToken, false, undefined) + .should.eventually.be.rejectedWith( + 'Firebase Auth Blocking token has expired. Get a fresh Auth Blocking token from your client ' + + 'app and try again (auth/auth-blocking-token-expired)') + .and.have.property('code', 'auth/auth-blocking-token-expired'); + }); + + it('should be rejected when the verifier throws invalid signature for a Auth Blocking JWT token.', () => { + const verifierStub = sinon.stub(PublicKeySignatureVerifier.prototype, 'verify') + .rejects(new JwtError(JwtErrorCode.INVALID_SIGNATURE, 'invalid signature.')); + stubs.push(verifierStub); + + const mockAuthBlockingToken = mocks.generateAuthBlockingToken(); + + return authBlockingTokenVerifier._verifyAuthBlockingToken(mockAuthBlockingToken, false, undefined) + .should.eventually.be.rejectedWith('Firebase Auth Blocking 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 public keys.')); + stubs.push(verifierStub); + + const mockAuthBlockingToken = mocks.generateAuthBlockingToken(); + + return authBlockingTokenVerifier._verifyAuthBlockingToken(mockAuthBlockingToken, false, undefined) + .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', () => { + return tokenGenerator.createCustomToken(mocks.uid) + .then((customToken) => { + return authBlockingTokenVerifier._verifyAuthBlockingToken(customToken, false, undefined) + .should.eventually.be.rejectedWith( + '_verifyAuthBlockingToken() expects an Auth Blocking token, but was given a custom token'); + }); + }); + + it('should be rejected given a legacy custom token with error using article "an" before JWT short name', () => { + const legacyTokenGenerator = new LegacyFirebaseTokenGenerator('foo'); + const legacyCustomToken = legacyTokenGenerator.createToken({ + uid: mocks.uid, + }); + + return authBlockingTokenVerifier._verifyAuthBlockingToken(legacyCustomToken, false, undefined) + .should.eventually.be.rejectedWith( + '_verifyAuthBlockingToken() expects an Auth Blocking token, but was given a legacy custom token'); + }); + + it('should be fulfilled with decoded claims given a valid Auth Blocking JWT token', () => { + const verifierStub = sinon.stub(PublicKeySignatureVerifier.prototype, 'verify') + .resolves(); + stubs.push(verifierStub); + + clock = sinon.useFakeTimers(1000); + + const mockAuthBlockingToken = mocks.generateAuthBlockingToken(); + + return authBlockingTokenVerifier._verifyAuthBlockingToken(mockAuthBlockingToken, false, undefined) + .should.eventually.be.fulfilled.and.deep.equal({ + one: 'uno', + two: 'dos', + iat: 1, + exp: TEN_MINUTES_IN_SECONDS + 1, + aud: `https://us-central1-${mocks.projectId}.cloudfunctions.net/functionName`, + iss: 'https://securetoken.google.com/' + mocks.projectId, + sub: mocks.uid, + uid: mocks.uid, + }); + }); + + it('should decode an unsigned token if isEmulator=true', async () => { + clock = sinon.useFakeTimers(1000); + + const emulatorVerifier = createTokenVerifier(app); + const mockAuthBlockingToken = mocks.generateAuthBlockingToken({ + algorithm: 'none', + header: {} + }, undefined, 'secret'); + + const isEmulator = true; + const decoded = await emulatorVerifier._verifyAuthBlockingToken(mockAuthBlockingToken, isEmulator, undefined); + expect(decoded).to.deep.equal({ + one: 'uno', + two: 'dos', + iat: 1, + exp: TEN_MINUTES_IN_SECONDS + 1, + aud: `https://us-central1-${mocks.projectId}.cloudfunctions.net/functionName`, + iss: 'https://securetoken.google.com/' + mocks.projectId, + sub: mocks.uid, + uid: mocks.uid, + }); + }); + + it('should not decode an unsigned token when the algorithm is not overridden (emulator)', async () => { + clock = sinon.useFakeTimers(1000); + + const idTokenNoAlg = mocks.generateAuthBlockingToken({ + algorithm: 'none', + }, undefined, 'secret'); + await authBlockingTokenVerifier._verifyAuthBlockingToken(idTokenNoAlg, false, undefined) + .should.eventually.be.rejectedWith('Firebase Auth Blocking token has incorrect algorithm.'); + + const idTokenNoHeader = mocks.generateAuthBlockingToken({ + algorithm: 'none', + header: {} + }, undefined, 'secret'); + await authBlockingTokenVerifier._verifyAuthBlockingToken(idTokenNoHeader, false, undefined) + .should.eventually.be.rejectedWith('Firebase Auth Blocking token has no "kid" claim.'); + }); + + const eventTypesWithoutUid = ['beforeSendSms', 'beforeSendEmail']; + eventTypesWithoutUid.forEach((eventType) => { + it('should not throw error on invalid `sub` when event_type is "' + eventType + '"' , async () => { + const verifierStub = sinon.stub(PublicKeySignatureVerifier.prototype, 'verify') + .resolves(); + stubs.push(verifierStub); + + const mockAuthBlockingToken = mocks.generateAuthBlockingToken({ + subject: '' + }, { + event_type: eventType, + }); + return authBlockingTokenVerifier._verifyAuthBlockingToken(mockAuthBlockingToken, false, undefined) + .should.eventually.be.fulfilled; + }); + }); + + const eventTypesWithUid = ['beforeCreate', 'beforeSignIn', undefined]; + eventTypesWithUid.forEach((eventType) => { + it('should not throw error on invalid `sub` when event_type is "' + eventType + '"', async () => { + const verifierStub = sinon.stub(PublicKeySignatureVerifier.prototype, 'verify') + .resolves(); + stubs.push(verifierStub); + + const mockAuthBlockingToken = mocks.generateAuthBlockingToken({ + subject: '' + }, { + event_type: eventType, + }); + return authBlockingTokenVerifier._verifyAuthBlockingToken(mockAuthBlockingToken, false, undefined) + .should.eventually.be.rejectedWith('Firebase Auth Blocking token has an empty "sub" (subject) claim.' + + ' See https://cloud.google.com/identity-platform/docs/blocking-functions for details on how to retrieve an' + + ' Auth Blocking token.'); + }); + }); + }); +}); diff --git a/test/unit/auth/user-import-builder.spec.ts b/test/unit/auth/user-import-builder.spec.ts new file mode 100644 index 0000000000..859265a03a --- /dev/null +++ b/test/unit/auth/user-import-builder.spec.ts @@ -0,0 +1,883 @@ +/*! + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as chai from 'chai'; +import * as sinonChai from 'sinon-chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +import { deepCopy } from '../../../src/utils/deep-copy'; +import { + UserImportBuilder, ValidatorFunction, UploadAccountRequest, +} from '../../../src/auth/user-import-builder'; +import { AuthClientErrorCode, FirebaseAuthError } from '../../../src/utils/error'; +import { toWebSafeBase64 } from '../../../src/utils'; +import { + UpdatePhoneMultiFactorInfoRequest, UserImportResult, UserImportRecord, +} from '../../../src/auth'; + +chai.should(); +chai.use(sinonChai); +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(); + const userRequestValidator: ValidatorFunction = () => { + // Do not throw an error. + }; + const userRequestValidatorWithError: ValidatorFunction = (request) => { + // Simulate a validation error is thrown for a specific user. + if (request.localId === '5678') { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_PHONE_NUMBER, + ); + } + }; + const users = [ + { + uid: '1234', + email: 'user@example.com', + passwordHash: Buffer.from('password'), + passwordSalt: Buffer.from('salt'), + displayName: 'Test User', + photoURL: 'https://www.example.com/1234/photo.png', + disabled: true, + metadata: { + lastSignInTime: nowString, + creationTime: nowString, + }, + providerData: [ + { + uid: 'google1234', + email: 'user@example.com', + photoURL: 'https://www.google.com/1234/photo.png', + displayName: 'Google User', + providerId: 'google.com', + }, + ], + customClaims: { admin: true }, + tenantId: 'TENANT-ID', + }, + { + uid: '9012', + email: 'johndoe@example.com', + passwordHash: Buffer.from('userpass'), + passwordSalt: Buffer.from('NaCl'), + }, + { uid: '5678', phoneNumber: '+16505550101' }, + { + uid: '3456', + email: 'janedoe@example.com', + passwordHash: Buffer.from('password'), + passwordSalt: Buffer.from('NaCl'), + multiFactor: { + enrolledFactors: [ + { + uid: 'enrolledSecondFactor1', + phoneNumber: '+16505557348', + displayName: 'Spouse\'s phone number', + factorId: 'phone', + enrollmentTime: now.toUTCString(), + }, + { + uid: 'enrolledSecondFactor2', + phoneNumber: '+16505551000', + factorId: 'phone', + }, + ], + }, + }, + ]; + const expectedUsersRequest = [ + { + localId: '1234', + email: 'user@example.com', + passwordHash: toWebSafeBase64(Buffer.from('password')), + salt: toWebSafeBase64(Buffer.from('salt')), + displayName: 'Test User', + photoUrl: 'https://www.example.com/1234/photo.png', + disabled: true, + lastLoginAt: new Date(nowString).getTime(), + createdAt: new Date(nowString).getTime(), + providerUserInfo: [ + { + rawId: 'google1234', + email: 'user@example.com', + photoUrl: 'https://www.google.com/1234/photo.png', + displayName: 'Google User', + providerId: 'google.com', + }, + ], + customAttributes: JSON.stringify({ admin: true }), + tenantId: 'TENANT-ID', + }, + { + localId: '9012', + email: 'johndoe@example.com', + passwordHash: toWebSafeBase64(Buffer.from('userpass')), + salt: toWebSafeBase64(Buffer.from('NaCl')), + }, + { + localId: '5678', + phoneNumber: '+16505550101', + }, + { + localId: '3456', + email: 'janedoe@example.com', + passwordHash: toWebSafeBase64(Buffer.from('password')), + salt: toWebSafeBase64(Buffer.from('NaCl')), + mfaInfo: [ + { + mfaEnrollmentId: 'enrolledSecondFactor1', + phoneInfo: '+16505557348', + displayName: 'Spouse\'s phone number', + enrolledAt: now.toISOString(), + }, + { + mfaEnrollmentId: 'enrolledSecondFactor2', + phoneInfo: '+16505551000', + }, + ], + }, + ]; + + const hmacAlgorithms = ['HMAC_SHA512', 'HMAC_SHA256', 'HMAC_SHA1', 'HMAC_MD5']; + const md5ShaPbkdfAlgorithms = [ + 'MD5', 'SHA1', 'SHA256', 'SHA512', 'PBKDF_SHA1', 'PBKDF2_SHA256', + ]; + describe('constructor', () => { + const invalidUserImportOptions = [10, 'invalid', undefined, null, true, ['a']]; + invalidUserImportOptions.forEach((invalidOption) => { + it(`should throw when non-object ${JSON.stringify(invalidOption)} UserImportOptions is provided`, () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"UserImportOptions" are required when importing users with passwords.', + ); + expect(() => { + return new UserImportBuilder(users, invalidOption as any, userRequestValidator); + }).to.throw(expectedError.message); + }); + }); + + it('should throw when an empty hash algorithm is provided', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.MISSING_HASH_ALGORITHM, + '"hash.algorithm" is missing from the provided "UserImportOptions".', + ); + expect(() => { + return new UserImportBuilder(users, {} as any, userRequestValidator); + }).to.throw(expectedError.message); + }); + + it('should throw when an invalid hash algorithm is provided', () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_HASH_ALGORITHM, + 'Unsupported hash algorithm provider "invalid".', + ); + const invalidOptions = { + hash: { + algorithm: 'invalid', + }, + }; + expect(() => { + return new UserImportBuilder(users, invalidOptions as any, userRequestValidator); + }).to.throw(expectedError.message); + }); + + 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' }, + ]; + expect(() => { + return new UserImportBuilder(noHashUsers, undefined, userRequestValidator); + }).not.to.throw(); + }); + + hmacAlgorithms.forEach((algorithm) => { + describe(`${algorithm}`, () => { + const invalidKeys = [10, 'invalid', undefined, null]; + invalidKeys.forEach((key) => { + 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 ' + + `hash algorithm ${algorithm}.`, + ); + const invalidOptions = { + hash: { + algorithm, + key, + }, + }; + expect(() => { + return new UserImportBuilder(users, invalidOptions as any, userRequestValidator); + }).to.throw(expectedError.message); + }); + }); + + it('should not throw with valid options and should generate expected request', () => { + const validOptions = { + hash: { + algorithm, + key: Buffer.from('secret'), + }, + }; + const expectedRequest = { + hashAlgorithm: algorithm, + signerKey: toWebSafeBase64(Buffer.from('secret')), + users: expectedUsersRequest, + }; + const userImportBuilder = + new UserImportBuilder(users, validOptions as any, userRequestValidator); + expect(userImportBuilder.buildRequest()).to.deep.equal(expectedRequest); + }); + }); + }); + + md5ShaPbkdfAlgorithms.forEach((algorithm) => { + describe(`${algorithm}`, () => { + let minRounds: number; + let maxRounds: number; + switch (algorithm) { + case 'MD5': + minRounds = 0; + maxRounds = 8192; + break; + case 'SHA1': + case 'SHA256': + case 'SHA512': + minRounds = 1; + maxRounds = 8192; + break; + case 'PBKDF_SHA1': + case 'PBKDF2_SHA256': + minRounds = 0; + maxRounds = 120000; + break; + default: + throw new Error('Unexpected algorithm: ' + algorithm); + } + const invalidRounds = [minRounds - 1, maxRounds + 1, 'invalid', undefined, null]; + + invalidRounds.forEach((rounds) => { + it(`should throw when ${JSON.stringify(rounds)} rounds provided`, () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_HASH_ROUNDS, + `A valid "hash.rounds" number between ${minRounds} and ${maxRounds} must be provided for ` + + `hash algorithm ${algorithm}.`, + ); + const invalidOptions = { + hash: { + algorithm, + rounds, + }, + }; + expect(() => { + return new UserImportBuilder(users, invalidOptions as any, userRequestValidator); + }).to.throw(expectedError.message); + }); + }); + + it('should not throw with valid options and should generate expected request', () => { + const validOptions = { + hash: { + algorithm, + rounds: maxRounds, + }, + }; + const expectedRequest = { + hashAlgorithm: algorithm, + rounds: maxRounds, + users: expectedUsersRequest, + }; + const userImportBuilder = + new UserImportBuilder(users, validOptions as any, userRequestValidator); + expect(userImportBuilder.buildRequest()).to.deep.equal(expectedRequest); + }); + + }); + }); + + describe('SCRYPT', () => { + const algorithm = 'SCRYPT'; + const invalidKeys = [10, 'invalid', undefined, null]; + const invalidRounds = [0, 9, 'invalid', undefined, null]; + const invalidMemoryCost = [0, 15, 'invalid', undefined, null]; + const invalidSaltSeparator = [10, 'invalid']; + invalidKeys.forEach((key) => { + 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 ' + + `hash algorithm ${algorithm}.`, + ); + const invalidOptions = { + hash: { + algorithm, + key, + rounds: 5, + memoryCost: 12, + }, + }; + expect(() => { + return new UserImportBuilder(users, invalidOptions as any, userRequestValidator); + }).to.throw(expectedError.message); + }); + }); + + invalidRounds.forEach((rounds) => { + it(`should throw when ${JSON.stringify(rounds)} rounds provided`, () => { + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_HASH_ROUNDS, + 'A valid "hash.rounds" number between 1 and 8 must be provided for ' + + `hash algorithm ${algorithm}.`, + ); + const invalidOptions = { + hash: { + algorithm, + key: Buffer.from('secret'), + rounds, + memoryCost: 12, + }, + }; + expect(() => { + return new UserImportBuilder(users, invalidOptions as any, userRequestValidator); + }).to.throw(expectedError.message); + }); + }); + + invalidMemoryCost.forEach((memoryCost) => { + 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 ' + + `hash algorithm ${algorithm}.`, + ); + const invalidOptions = { + hash: { + algorithm, + key: Buffer.from('secret'), + rounds: 4, + memoryCost, + }, + }; + expect(() => { + return new UserImportBuilder(users, invalidOptions as any, userRequestValidator); + }).to.throw(expectedError.message); + }); + }); + + invalidSaltSeparator.forEach((saltSeparator) => { + 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.', + ); + const invalidOptions = { + hash: { + algorithm, + key: Buffer.from('secret'), + rounds: 4, + memoryCost: 12, + saltSeparator, + }, + }; + expect(() => { + return new UserImportBuilder(users, invalidOptions as any, userRequestValidator); + }).to.throw(expectedError.message); + }); + }); + + it('should not throw with valid options and should generate expected request', () => { + const validOptions = { + hash: { + algorithm, + key: Buffer.from('secret'), + rounds: 4, + memoryCost: 12, + }, + }; + const expectedRequest = { + hashAlgorithm: algorithm, + signerKey: toWebSafeBase64(Buffer.from('secret')), + rounds: 4, + memoryCost: 12, + users: expectedUsersRequest, + saltSeparator: '', + }; + const userImportBuilder = + new UserImportBuilder(users, validOptions as any, userRequestValidator); + expect(userImportBuilder.buildRequest()).to.deep.equal(expectedRequest); + }); + }); + + describe('BCRYPT', () => { + const algorithm = 'BCRYPT'; + it('should not throw with valid options and should generate expected request', () => { + const validOptions = { + hash: { + algorithm, + }, + }; + const expectedRequest = { + hashAlgorithm: algorithm, + users: expectedUsersRequest, + }; + const userImportBuilder = + new UserImportBuilder(users, validOptions as any, userRequestValidator); + expect(userImportBuilder.buildRequest()).to.deep.equal(expectedRequest); + }); + }); + + describe('STANDARD_SCRYPT', () => { + const algorithm = 'STANDARD_SCRYPT'; + const invalidMemoryCost = [false, {}, 'invalid', undefined, null]; + const invalidParallelization = [false, {}, 'invalid', undefined, null]; + const invalidBlockSize = [false, {}, 'invalid', undefined, null]; + const invalidDerivedKeyLength = [false, {}, 'invalid', undefined, null]; + invalidMemoryCost.forEach((memoryCost) => { + 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 ' + + `hash algorithm ${algorithm}.`, + ); + const invalidOptions = { + hash: { + algorithm, + memoryCost, + parallelization: 16, + blockSize: 8, + derivedKeyLength: 64, + }, + }; + expect(() => { + return new UserImportBuilder(users, invalidOptions as any, userRequestValidator); + }).to.throw(expectedError.message); + }); + }); + + invalidParallelization.forEach((parallelization) => { + 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 ' + + `hash algorithm ${algorithm}.`, + ); + const invalidOptions = { + hash: { + algorithm, + memoryCost : 1024, + parallelization, + blockSize: 8, + derivedKeyLength: 64, + }, + }; + expect(() => { + return new UserImportBuilder(users, invalidOptions as any, userRequestValidator); + }).to.throw(expectedError.message); + }); + }); + + invalidBlockSize.forEach((blockSize) => { + 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 ' + + `hash algorithm ${algorithm}.`, + ); + const invalidOptions = { + hash: { + algorithm, + memoryCost : 1024, + parallelization: 16, + blockSize, + derivedKeyLength: 64, + }, + }; + expect(() => { + return new UserImportBuilder(users, invalidOptions as any, userRequestValidator); + }).to.throw(expectedError.message); + }); + }); + + invalidDerivedKeyLength.forEach((derivedKeyLength) => { + 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 ' + + `hash algorithm ${algorithm}.`, + ); + const invalidOptions = { + hash: { + algorithm, + memoryCost : 1024, + parallelization: 16, + blockSize: 8, + derivedKeyLength, + }, + }; + expect(() => { + return new UserImportBuilder(users, invalidOptions as any, userRequestValidator); + }).to.throw(expectedError.message); + }); + }); + + it('should not throw with valid options and should generate expected request', () => { + const validOptions = { + hash: { + algorithm, + memoryCost : 1024, + parallelization: 16, + blockSize: 8, + derivedKeyLength: 64, + }, + }; + const expectedRequest = { + hashAlgorithm: algorithm, + cpuMemCost: 1024, + parallelization: 16, + blockSize: 8, + dkLen: 64, + users: expectedUsersRequest, + }; + const userImportBuilder = + new UserImportBuilder(users, validOptions as any, userRequestValidator); + expect(userImportBuilder.buildRequest()).to.deep.equal(expectedRequest); + }); + + }); + }); + + describe('buildRequest()', () => { + const algorithm = 'BCRYPT'; + const validOptions = { + hash: { + algorithm, + }, + }; + + it('should return the expected request when no client side error is detected', () => { + const expectedRequest = { + hashAlgorithm: algorithm, + users: expectedUsersRequest, + }; + const userImportBuilder = + new UserImportBuilder(users, validOptions as any, userRequestValidator); + expect(userImportBuilder.buildRequest()).to.deep.equal(expectedRequest); + }); + + it('should return the expected request when client side errors are detected', () => { + const testUsers = deepCopy(users); + // Pass 2 more users with invalid passwordHash and invalid passwordSalt. + testUsers.push( + { + uid: 'INVALID1', + email: 'johndoe@example.com', + passwordHash: Buffer.from('password'), + passwordSalt: 'not a buffer', + } as any, + ); + testUsers.push( + { uid: 'INVALID2', email: 'other@domain.com', passwordHash: 'not a buffer' } as any, + ); + const expectedRequest = { + hashAlgorithm: algorithm, + // The third user will be removed due to client side error. + users: [ + expectedUsersRequest[0], expectedUsersRequest[1], expectedUsersRequest[3], + ], + }; + const userImportBuilder = + new UserImportBuilder(users, validOptions as any, userRequestValidatorWithError); + expect(userImportBuilder.buildRequest()).to.deep.equal(expectedRequest); + }); + + it('should return expected request with no hash options when not required', () => { + const noHashUsers = [ + { uid: '1234', email: 'user@example.com' }, + { uid: '5678', phoneNumber: '+16505550101' }, + ]; + const expectedRequest = { + users: [ + { localId: '1234', email: 'user@example.com' }, + { localId: '5678', phoneNumber: '+16505550101' }, + ], + }; + const userImportBuilder = + new UserImportBuilder(noHashUsers, validOptions as any, userRequestValidator); + expect(userImportBuilder.buildRequest()).to.deep.equal(expectedRequest); + }); + + 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: [] } }, + ]; + const expectedRequest = { + users: [ + { localId: '1234', email: 'user@example.com' }, + { localId: '5678', phoneNumber: '+16505550101' }, + ], + }; + const userImportBuilder = + new UserImportBuilder(noMultiFactorUsers, validOptions as any, userRequestValidator); + expect(userImportBuilder.buildRequest()).to.deep.equal(expectedRequest); + }); + + 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: [ phoneFactor ], + }, + }, + { uid: '5678', phoneNumber: '+16505550102' }, + ]; + const expectedRequest: UploadAccountRequest = { + users: [ + { localId: '5678', phoneNumber: '+16505550102' }, + ], + }; + const userImportBuilder = + new UserImportBuilder(invalidMultiFactorUsers, validOptions as any, userRequestValidator); + expect(userImportBuilder.buildRequest()).to.deep.equal(expectedRequest); + }); + + it('should ignore users with unsupported second factors', () => { + const invalidMultiFactorUsers: any = [ + { + uid: '1234', + multiFactor: { + enrolledFactors: [ + { + uid: 'enrolledSecondFactor1', + secret: 'SECRET', + displayName: 'Google Authenticator on personal phone', + factorId: 'totp', + }, + ], + }, + }, + { uid: '5678', phoneNumber: '+16505550102' }, + ]; + const expectedRequest: UploadAccountRequest = { + users: [ + { localId: '5678', phoneNumber: '+16505550102' }, + ], + }; + const userImportBuilder = + new UserImportBuilder(invalidMultiFactorUsers, validOptions as any, userRequestValidator); + expect(userImportBuilder.buildRequest()).to.deep.equal(expectedRequest); + }); + }); + + describe('buildResponse()', () => { + const algorithm = 'BCRYPT'; + const validOptions = { + hash: { + algorithm, + }, + }; + + it('should return the expected response for successful import', () => { + const successfulServerResponse: any = []; + const successfulUserImportResponse: UserImportResult = { + successCount: 4, + failureCount: 0, + errors: [], + }; + const userImportBuilder = + new UserImportBuilder(users, validOptions as any, userRequestValidator); + expectUserImportResult( + userImportBuilder.buildResponse(successfulServerResponse), + successfulUserImportResponse); + }); + + it('should return the expected response for import with server side errors', () => { + const failingServerResponse = [ + { index: 1, message: 'Some error occurred!' }, + ]; + const serverErrorUserImportResponse = { + successCount: 3, + failureCount: 1, + errors: [ + { + // Index should match server error index. + index: 1, + error: new FirebaseAuthError( + AuthClientErrorCode.INVALID_USER_IMPORT, + 'Some error occurred!', + ), + }, + ], + }; + const userImportBuilder = + new UserImportBuilder(users, validOptions as any, userRequestValidator); + expectUserImportResult( + userImportBuilder.buildResponse(failingServerResponse), + serverErrorUserImportResponse); + }); + + it('should return the expected response for import with client side errors', () => { + const successfulServerResponse: any = []; + const clientErrorUserImportResponse = { + successCount: 3, + failureCount: 1, + errors: [ + { index: 2, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER) }, + ], + }; + // userRequestValidatorWithError will throw on the 3rd user (index = 2). + const userImportBuilder = + new UserImportBuilder(users, validOptions as any, userRequestValidatorWithError); + expectUserImportResult( + userImportBuilder.buildResponse(successfulServerResponse), + clientErrorUserImportResponse); + }); + + 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!' }, + ]; + const userRequestValidatorWithMultipleErrors: ValidatorFunction = (request) => { + // Simulate a validation error is thrown for specific users. + if (request.localId === 'USER2') { + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL); + } else if (request.localId === 'USER4') { + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER); + } + }; + + // The second and fourth users will throw a client side error. + // The third and sixth user will throw a server side error. + // 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: 'USER8', + email: 'other@domain.com', + passwordHash: Buffer.from('password'), + passwordSalt: 'not a buffer' as any, + }, + { + uid: 'USER9', + multiFactor: { + enrolledFactors: [ + { + uid: 'enrollmentId1', + phoneNumber: '+16505551111', + factorId: 'phone', + enrollmentTime: 'invalid', + }, + ], + }, + }, + { + uid: 'USER10', + multiFactor: { + enrolledFactors: [ + { + uid: 'enrollmentId2', + secret: 'SECRET', + displayName: 'Google Authenticator on personal phone', + factorId: 'totp', + } as any, + ], + }, + }, + ]; + const mixedErrorUserImportResponse = { + successCount: 2, + failureCount: 8, + errors: [ + // Client side detected error. + { index: 1, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL) }, + // Server side detected error. + { + index: 2, + error: new FirebaseAuthError( + AuthClientErrorCode.INVALID_USER_IMPORT, + 'Some error occurred in USER3!', + ), + }, + // Client side detected error. + { index: 3, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER) }, + // Server side detected error. + { + index: 5, + error: new FirebaseAuthError( + AuthClientErrorCode.INVALID_USER_IMPORT, + 'Another error occurred in USER6!', + ), + }, + // Client side errors. + { index: 6, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD_HASH) }, + { index: 7, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD_SALT) }, + { + index: 8, + error: new FirebaseAuthError( + AuthClientErrorCode.INVALID_ENROLLMENT_TIME, + 'The second factor "enrollmentTime" for "enrollmentId1" must be a valid ' + + 'UTC date string.'), + }, + { + index: 9, + error: new FirebaseAuthError( + AuthClientErrorCode.UNSUPPORTED_SECOND_FACTOR, + `Unsupported second factor "${JSON.stringify(testUsers[9].multiFactor!.enrolledFactors[0])}" provided.`), + }, + ], + }; + const userImportBuilder = new UserImportBuilder( + testUsers, validOptions as any, userRequestValidatorWithMultipleErrors); + expectUserImportResult( + userImportBuilder.buildResponse(failingServerResponse), + mixedErrorUserImportResponse); + }); + }); +}); diff --git a/test/unit/auth/user-record.spec.ts b/test/unit/auth/user-record.spec.ts index 1de0745857..dc332c13b9 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"); @@ -18,8 +19,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 {UserInfo, UserMetadata, UserRecord} from '../../../src/auth/user-record'; +import { deepCopy } from '../../../src/utils/deep-copy'; +import { + GetAccountInfoUserResponse, ProviderUserInfoResponse, MultiFactorInfoResponse, TotpMultiFactorInfo, +} from '../../../src/auth/user-record'; +import { + UserInfo, UserMetadata, UserRecord, MultiFactorSettings, MultiFactorInfo, PhoneMultiFactorInfo, +} from '../../../src/auth/index'; chai.should(); @@ -27,13 +33,15 @@ chai.use(sinonChai); chai.use(chaiAsPromised); const expect = chai.expect; +const now = new Date(); /** - * @return {object} A sample valid user response as returned from getAccountInfo + * @param tenantId The optional tenant ID to add to the response. + * @return A sample valid user response as returned from getAccountInfo * endpoint. */ -function getValidUserResponse(): object { - return { +function getValidUserResponse(tenantId?: string): GetAccountInfoUserResponse { + const response: any = { localId: 'abcdefghijklmnopqrstuvwxyz', email: 'user@gmail.com', emailVerified: true, @@ -75,17 +83,36 @@ function getValidUserResponse(): object { validSince: '1476136676', lastLoginAt: '1476235905000', createdAt: '1476136676000', + lastRefreshAt: '2016-10-12T01:31:45.000Z', customAttributes: JSON.stringify({ admin: true, }), + mfaInfo: [ + { + mfaEnrollmentId: 'enrollmentId1', + displayName: 'displayName1', + enrolledAt: now.toISOString(), + phoneInfo: '+16505551234', + }, + { + mfaEnrollmentId: 'enrollmentId2', + enrolledAt: now.toISOString(), + phoneInfo: '+16505556789', + }, + ], }; + if (typeof tenantId !== 'undefined') { + response.tenantId = tenantId; + } + return response; } /** - * @return {object} The expected user JSON representation for the above user + * @param tenantId The optional tenant ID to add to the user. + * @return The expected user JSON representation for the above user * server response. */ -function getUserJSON(): object { +function getUserJSON(tenantId?: string): object { return { uid: 'abcdefghijklmnopqrstuvwxyz', email: 'user@gmail.com', @@ -133,19 +160,39 @@ function getUserJSON(): object { metadata: { lastSignInTime: new Date(1476235905000).toUTCString(), creationTime: new Date(1476136676000).toUTCString(), + lastRefreshTime: new Date(1476235905000).toUTCString(), }, customClaims: { admin: true, }, tokensValidAfterTime: new Date(1476136676000).toUTCString(), + tenantId, + multiFactor: { + enrolledFactors: [ + { + uid: 'enrollmentId1', + displayName: 'displayName1', + enrollmentTime: now.toUTCString(), + phoneNumber: '+16505551234', + factorId: 'phone', + }, + { + uid: 'enrollmentId2', + displayName: undefined, + enrollmentTime: now.toUTCString(), + phoneNumber: '+16505556789', + factorId: 'phone', + }, + ], + }, }; } /** - * @return {object} A sample user info response as returned from getAccountInfo + * @return A sample user info response as returned from getAccountInfo * endpoint. */ -function getUserInfoResponse(): object { +function getUserInfoResponse(): ProviderUserInfoResponse { return { providerId: 'google.com', displayName: 'John Doe', @@ -157,7 +204,7 @@ function getUserInfoResponse(): object { } /** - * @return {object} The JSON representation of the above user info response. + * @return The JSON representation of the above user info response. */ function getUserInfoJSON(): object { return { @@ -171,10 +218,10 @@ function getUserInfoJSON(): object { } /** - * @return {object} A sample user info response with phone number as returned + * @return A sample user info response with phone number as returned * from getAccountInfo endpoint. */ -function getUserInfoWithPhoneNumberResponse(): object { +function getUserInfoWithPhoneNumberResponse(): ProviderUserInfoResponse { return { providerId: 'phone', phoneNumber: '+11234567890', @@ -183,7 +230,7 @@ function getUserInfoWithPhoneNumberResponse(): object { } /** - * @return {object} The JSON representation of the above user info response + * @return The JSON representation of the above user info response * with a phone number. */ function getUserInfoWithPhoneNumberJSON(): object { @@ -197,29 +244,448 @@ function getUserInfoWithPhoneNumberJSON(): object { }; } +describe('PhoneMultiFactorInfo', () => { + const serverResponse: MultiFactorInfoResponse = { + mfaEnrollmentId: 'enrollmentId1', + displayName: 'displayName1', + enrolledAt: now.toISOString(), + phoneInfo: '+16505551234', + }; + const phoneMultiFactorInfo = new PhoneMultiFactorInfo(serverResponse); + const phoneMultiFactorInfoMissingFields = new PhoneMultiFactorInfo({ + mfaEnrollmentId: serverResponse.mfaEnrollmentId, + phoneInfo: serverResponse.phoneInfo, + }); + + describe('constructor', () => { + it('should throw when an empty object is provided', () => { + expect(() => { + return new PhoneMultiFactorInfo({} as any); + }).to.throw('INTERNAL ASSERT FAILED: Invalid multi-factor info response'); + }); + + it('should throw when an undefined response is provided', () => { + expect(() => { + return new PhoneMultiFactorInfo(undefined as any); + }).to.throw('INTERNAL ASSERT FAILED: Invalid multi-factor info response'); + }); + + it('should succeed when mfaEnrollmentId and phoneInfo are both provided', () => { + expect(() => { + return new PhoneMultiFactorInfo({ + mfaEnrollmentId: 'enrollmentId1', + phoneInfo: '+16505551234', + }); + }).not.to.throw(Error); + }); + + it('should throw when only mfaEnrollmentId is provided', () => { + expect(() => { + return new PhoneMultiFactorInfo({ + mfaEnrollmentId: 'enrollmentId1', + } as any); + }).to.throw('INTERNAL ASSERT FAILED: Invalid multi-factor info response'); + }); + + it('should throw when only phoneInfo is provided', () => { + expect(() => { + return new PhoneMultiFactorInfo({ + phoneInfo: '+16505551234', + } as any); + }).to.throw('INTERNAL ASSERT FAILED: Invalid multi-factor info response'); + }); + }); + + describe('getters', () => { + it('should set missing optional fields to null', () => { + expect(phoneMultiFactorInfoMissingFields.uid).to.equal(serverResponse.mfaEnrollmentId); + 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'); + }); + + it('should return expected factorId', () => { + expect(phoneMultiFactorInfo.factorId).to.equal('phone'); + }); + + it('should throw when modifying readonly factorId property', () => { + expect(() => { + (phoneMultiFactorInfo as any).factorId = 'other'; + }).to.throw(Error); + }); + + it('should return expected displayName', () => { + expect(phoneMultiFactorInfo.displayName).to.equal(serverResponse.displayName); + }); + + it('should throw when modifying readonly displayName property', () => { + expect(() => { + (phoneMultiFactorInfo as any).displayName = 'Modified'; + }).to.throw(Error); + }); + + it('should return expected phoneNumber', () => { + expect(phoneMultiFactorInfo.phoneNumber).to.equal(serverResponse.phoneInfo); + }); + + it('should throw when modifying readonly phoneNumber property', () => { + expect(() => { + (phoneMultiFactorInfo as any).phoneNumber = '+16505551111'; + }).to.throw(Error); + }); + + it('should return expected uid', () => { + expect(phoneMultiFactorInfo.uid).to.equal(serverResponse.mfaEnrollmentId); + }); + + it('should throw when modifying readonly uid property', () => { + expect(() => { + (phoneMultiFactorInfo as any).uid = 'modifiedEnrollmentId'; + }).to.throw(Error); + }); + + it('should return expected enrollmentTime', () => { + expect(phoneMultiFactorInfo.enrollmentTime).to.equal(now.toUTCString()); + }); + + it('should throw when modifying readonly uid property', () => { + expect(() => { + (phoneMultiFactorInfo as any).enrollmentTime = new Date().toISOString(); + }).to.throw(Error); + }); + }); + + describe('toJSON', () => { + it('should return expected JSON object', () => { + expect(phoneMultiFactorInfo.toJSON()).to.deep.equal({ + uid: 'enrollmentId1', + displayName: 'displayName1', + enrollmentTime: now.toUTCString(), + phoneNumber: '+16505551234', + factorId: 'phone', + }); + }); + + it('should return expected JSON object with missing fields set to null', () => { + expect(phoneMultiFactorInfoMissingFields.toJSON()).to.deep.equal({ + uid: 'enrollmentId1', + displayName: undefined, + enrollmentTime: null, + phoneNumber: '+16505551234', + factorId: 'phone', + }); + }); + }); +}); + +describe('TotpMultiFactorInfo', () => { + const serverResponse: MultiFactorInfoResponse = { + mfaEnrollmentId: 'enrollmentId1', + displayName: 'displayName1', + enrolledAt: now.toISOString(), + totpInfo: {}, + }; + const totpMultiFactorInfo = new TotpMultiFactorInfo(serverResponse); + const totpMultiFactorInfoMissingFields = new TotpMultiFactorInfo({ + mfaEnrollmentId: serverResponse.mfaEnrollmentId, + totpInfo: serverResponse.totpInfo, + }); + + describe('constructor', () => { + it('should throw when an empty object is provided', () => { + expect(() => { + return new TotpMultiFactorInfo({} as any); + }).to.throw('INTERNAL ASSERT FAILED: Invalid multi-factor info response'); + }); + + it('should throw when an undefined response is provided', () => { + expect(() => { + return new TotpMultiFactorInfo(undefined as any); + }).to.throw('INTERNAL ASSERT FAILED: Invalid multi-factor info response'); + }); + + it('should succeed when mfaEnrollmentId and totpInfo are both provided', () => { + expect(() => { + return new TotpMultiFactorInfo({ + mfaEnrollmentId: 'enrollmentId1', + totpInfo: {}, + }); + }).not.to.throw(Error); + }); + + it('should throw when only mfaEnrollmentId is provided', () => { + expect(() => { + return new TotpMultiFactorInfo({ + mfaEnrollmentId: 'enrollmentId1', + } as any); + }).to.throw('INTERNAL ASSERT FAILED: Invalid multi-factor info response'); + }); + + it('should throw when only totpInfo is provided', () => { + expect(() => { + return new TotpMultiFactorInfo({ + totpInfo: {}, + } as any); + }).to.throw('INTERNAL ASSERT FAILED: Invalid multi-factor info response'); + }); + }); + + describe('getters', () => { + it('should set missing optional fields to null', () => { + expect(totpMultiFactorInfoMissingFields.uid).to.equal(serverResponse.mfaEnrollmentId); + expect(totpMultiFactorInfoMissingFields.displayName).to.be.undefined; + expect(totpMultiFactorInfoMissingFields.totpInfo).to.equal(serverResponse.totpInfo); + expect(totpMultiFactorInfoMissingFields.enrollmentTime).to.be.null; + expect(totpMultiFactorInfoMissingFields.factorId).to.equal('totp'); + }); + + it('should return expected factorId', () => { + expect(totpMultiFactorInfo.factorId).to.equal('totp'); + }); + + it('should throw when modifying readonly factorId property', () => { + expect(() => { + (totpMultiFactorInfo as any).factorId = 'other'; + }).to.throw(Error); + }); + + it('should return expected displayName', () => { + expect(totpMultiFactorInfo.displayName).to.equal(serverResponse.displayName); + }); + + it('should throw when modifying readonly displayName property', () => { + expect(() => { + (totpMultiFactorInfo as any).displayName = 'Modified'; + }).to.throw(Error); + }); + + it('should return expected totpInfo object', () => { + expect(totpMultiFactorInfo.totpInfo).to.equal(serverResponse.totpInfo); + }); + + it('should return expected uid', () => { + expect(totpMultiFactorInfo.uid).to.equal(serverResponse.mfaEnrollmentId); + }); + + it('should throw when modifying readonly uid property', () => { + expect(() => { + (totpMultiFactorInfo as any).uid = 'modifiedEnrollmentId'; + }).to.throw(Error); + }); + + it('should return expected enrollmentTime', () => { + expect(totpMultiFactorInfo.enrollmentTime).to.equal(now.toUTCString()); + }); + + it('should throw when modifying readonly uid property', () => { + expect(() => { + (totpMultiFactorInfo as any).enrollmentTime = new Date().toISOString(); + }).to.throw(Error); + }); + }); + + describe('toJSON', () => { + it('should return expected JSON object', () => { + expect(totpMultiFactorInfo.toJSON()).to.deep.equal({ + uid: 'enrollmentId1', + displayName: 'displayName1', + enrollmentTime: now.toUTCString(), + totpInfo: {}, + factorId: 'totp', + }); + }); + + it('should return expected JSON object with missing fields set to null', () => { + expect(totpMultiFactorInfoMissingFields.toJSON()).to.deep.equal({ + uid: 'enrollmentId1', + displayName: undefined, + enrollmentTime: null, + totpInfo: {}, + factorId: 'totp', + }); + }); + }); +}); + +describe('MultiFactorInfo', () => { + const phoneServerResponse: MultiFactorInfoResponse = { + mfaEnrollmentId: 'enrollmentId1', + displayName: 'displayName1', + enrolledAt: now.toISOString(), + phoneInfo: '+16505551234', + }; + const phoneMultiFactorInfo = new PhoneMultiFactorInfo(phoneServerResponse); + const totpServerResponse: MultiFactorInfoResponse = { + mfaEnrollmentId: 'enrollmentId1', + displayName: 'displayName1', + enrolledAt: now.toISOString(), + totpInfo: {}, + }; + const totpMultiFactorInfo = new TotpMultiFactorInfo(totpServerResponse); + + describe('initMultiFactorInfo', () => { + it('should return expected PhoneMultiFactorInfo', () => { + expect(MultiFactorInfo.initMultiFactorInfo(phoneServerResponse)).to.deep.equal(phoneMultiFactorInfo); + }); + it('should return expected TotpMultiFactorInfo', () => { + expect(MultiFactorInfo.initMultiFactorInfo(totpServerResponse)).to.deep.equal(totpMultiFactorInfo); + }); + + it('should return null for invalid MultiFactorInfo', () => { + expect(MultiFactorInfo.initMultiFactorInfo(undefined as any)).to.be.null; + }); + }); +}); + +describe('MultiFactorSettings', () => { + const serverResponse = { + localId: 'uid123', + mfaInfo: [ + { + mfaEnrollmentId: 'enrollmentId1', + displayName: 'displayName1', + enrolledAt: now.toISOString(), + phoneInfo: '+16505551234', + }, + { + mfaEnrollmentId: 'enrollmentId2', + enrolledAt: now.toISOString(), + phoneInfo: '+16505556789', + }, + { + // Invalid factor. + mfaEnrollmentId: 'enrollmentId3', + }, + { + // Unsupported factor. + mfaEnrollmentId: 'enrollmentId4', + displayName: 'Backup second factor', + enrolledAt: now.toISOString(), + secretKey: 'SECRET_KEY', + }, + { + mfaEnrollmentId: 'enrollmentId5', + displayName: 'displayName1', + enrolledAt: now.toISOString(), + totpInfo: {}, + }, + ], + }; + const expectedMultiFactorInfo = [ + new PhoneMultiFactorInfo({ + mfaEnrollmentId: 'enrollmentId1', + displayName: 'displayName1', + enrolledAt: now.toISOString(), + phoneInfo: '+16505551234', + }), + new PhoneMultiFactorInfo({ + mfaEnrollmentId: 'enrollmentId2', + enrolledAt: now.toISOString(), + phoneInfo: '+16505556789', + }), + new TotpMultiFactorInfo({ + mfaEnrollmentId: 'enrollmentId5', + displayName: 'displayName1', + enrolledAt: now.toISOString(), + totpInfo: {}, + }) + ]; + + describe('constructor', () => { + it('should throw when a non object is provided', () => { + expect(() => { + return new MultiFactorSettings(undefined as any); + }).to.throw('INTERNAL ASSERT FAILED: Invalid multi-factor response'); + }); + + it('should populate an empty enrolledFactors array when given an empty object', () => { + const multiFactor = new MultiFactorSettings({} as any); + + expect(multiFactor.enrolledFactors.length).to.equal(0); + }); + + it('should populate expected enrolledFactors', () => { + const multiFactor = new MultiFactorSettings(serverResponse); + + expect(multiFactor.enrolledFactors.length).to.equal(3); + expect(multiFactor.enrolledFactors[0]).to.deep.equal(expectedMultiFactorInfo[0]); + expect(multiFactor.enrolledFactors[1]).to.deep.equal(expectedMultiFactorInfo[1]); + expect(multiFactor.enrolledFactors[2]).to.deep.equal(expectedMultiFactorInfo[2]); + }); + }); + + describe('getter', () => { + it('should throw when modifying readonly enrolledFactors property', () => { + const multiFactor = new MultiFactorSettings(serverResponse); + + expect(() => { + (multiFactor as any).enrolledFactors = [ + expectedMultiFactorInfo[0], + ]; + }).to.throw(Error); + }); + + it('should throw when modifying readonly enrolledFactors internals', () => { + const multiFactor = new MultiFactorSettings(serverResponse); + + expect(() => { + (multiFactor.enrolledFactors as any)[0] = new PhoneMultiFactorInfo({ + mfaEnrollmentId: 'enrollmentId3', + displayName: 'displayName3', + enrolledAt: now.toISOString(), + phoneInfo: '+16505559999', + }); + }).to.throw(Error); + }); + }); + + describe('toJSON', () => { + it('should return expected JSON object when given an empty response', () => { + const multiFactor = new MultiFactorSettings({} as any); + + expect(multiFactor.toJSON()).to.deep.equal({ + enrolledFactors: [], + }); + }); + + it('should return expected JSON object when given a populated response', () => { + const multiFactor = new MultiFactorSettings(serverResponse); + + expect(multiFactor.toJSON()).to.deep.equal({ + enrolledFactors: [ + expectedMultiFactorInfo[0].toJSON(), + expectedMultiFactorInfo[1].toJSON(), + expectedMultiFactorInfo[2].toJSON(), + ], + }); + }); + }); +}); + describe('UserInfo', () => { describe('constructor', () => { it('should throw when an empty object is provided', () => { expect(() => { - return new UserInfo({}); + return new UserInfo({} as any); }).to.throw(Error); }); 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'}); + 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'}); + return new UserInfo({ providerId: 'google.com' } as any); }).to.throw(Error); }); }); @@ -308,24 +774,28 @@ describe('UserInfo', () => { describe('UserMetadata', () => { const expectedLastLoginAt = 1476235905000; const expectedCreatedAt = 1476136676000; + const expectedLastRefreshAt = '2016-10-12T01:31:45.000Z'; const actualMetadata: UserMetadata = new UserMetadata({ - lastLoginAt: expectedLastLoginAt, - createdAt: expectedCreatedAt, + localId: 'uid123', + lastLoginAt: expectedLastLoginAt.toString(), + createdAt: expectedCreatedAt.toString(), + lastRefreshAt: expectedLastRefreshAt, }); const expectedMetadataJSON = { lastSignInTime: new Date(expectedLastLoginAt).toUTCString(), creationTime: new Date(expectedCreatedAt).toUTCString(), + lastRefreshTime: new Date(expectedLastRefreshAt).toUTCString(), }; describe('constructor', () => { it('should initialize as expected when a valid creationTime is provided', () => { expect(() => { - return new UserMetadata({createdAt: '1476136676000'}); + return new UserMetadata({ createdAt: '1476136676000' } as any); }).not.to.throw(Error); }); it('should set creationTime and lastSignInTime to null when not provided', () => { - const metadata = new UserMetadata({}); + const metadata = new UserMetadata({} as any); expect(metadata.creationTime).to.be.null; expect(metadata.lastSignInTime).to.be.null; }); @@ -333,7 +803,7 @@ describe('UserMetadata', () => { it('should set creationTime to null when creationTime value is invalid', () => { const metadata = new UserMetadata({ createdAt: 'invalid', - }); + } as any); expect(metadata.creationTime).to.be.null; expect(metadata.lastSignInTime).to.be.null; }); @@ -342,7 +812,7 @@ describe('UserMetadata', () => { const metadata = new UserMetadata({ createdAt: '1476235905000', lastLoginAt: 'invalid', - }); + } as any); expect(metadata.lastSignInTime).to.be.null; }); }); @@ -367,6 +837,19 @@ describe('UserMetadata', () => { (actualMetadata as any).creationTime = new Date(); }).to.throw(Error); }); + + it('should return expected lastRefreshTime', () => { + expect(actualMetadata.lastRefreshTime).to.equal(new Date(expectedLastRefreshAt).toUTCString()) + }); + + it('should return null when lastRefreshTime is not available', () => { + const metadata: UserMetadata = new UserMetadata({ + localId: 'uid123', + lastLoginAt: expectedLastLoginAt.toString(), + createdAt: expectedCreatedAt.toString(), + }); + expect(metadata.lastRefreshTime).to.be.null; + }); }); describe('toJSON', () => { @@ -380,13 +863,13 @@ describe('UserRecord', () => { describe('constructor', () => { it('should throw when no localId is provided', () => { expect(() => { - return new UserRecord({}); + return new UserRecord({} as any); }).to.throw(Error); }); it('should succeed when only localId is provided', () => { expect(() => { - return new UserRecord({localId: '123456789'}); + return new UserRecord({ localId: '123456789' }); }).not.to.throw(Error); }); }); @@ -440,7 +923,7 @@ describe('UserRecord', () => { it('should return expected photoURL', () => { expect(userRecord.photoURL).to.equal( - 'https://lh3.googleusercontent.com/1234567890/photo.jpg'); + 'https://lh3.googleusercontent.com/1234567890/photo.jpg'); }); it('should throw when modifying readonly photoURL property', () => { @@ -479,6 +962,15 @@ describe('UserRecord', () => { expect((new UserRecord(resp)).passwordHash).to.be.undefined; }); + it('should clear REDACTED passwordHash', () => { + const user = new UserRecord({ + localId: 'uid1', + passwordHash: Buffer.from('REDACTED').toString('base64'), + }); + + expect(user.passwordHash).to.be.undefined; + }); + it('should return expected empty string passwordHash', () => { // This happens for users that were migrated from other Auth systems // using different hashing algorithms. @@ -525,7 +1017,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); }); @@ -546,15 +1038,16 @@ describe('UserRecord', () => { }).to.throw(Error); }); - it('should return null tokensValidAfterTime when not available', () => { - expect(userRecordNoValidSince.tokensValidAfterTime).to.be.null; + it('should return undefined tokensValidAfterTime when not available', () => { + expect(userRecordNoValidSince.tokensValidAfterTime).to.be.undefined; }); it('should return expected metadata', () => { const metadata = new UserMetadata({ createdAt: '1476136676000', lastLoginAt: '1476235905000', - }); + lastRefreshAt: '2016-10-12T01:31:45.000Z', + } as any); expect(userRecord.metadata).to.deep.equal(metadata); }); @@ -563,7 +1056,7 @@ describe('UserRecord', () => { (userRecord as any).metadata = new UserMetadata({ createdAt: new Date().toUTCString(), lastLoginAt: new Date().toUTCString(), - }); + } as any); }).to.throw(Error); }); @@ -610,13 +1103,13 @@ describe('UserRecord', () => { it('should throw when modifying readonly providerData property', () => { expect(() => { (userRecord as any).providerData = [ - new UserInfo({ - providerId: 'google.com', - displayName: 'Jane Doe', - photoUrl: 'https://lh3.googleusercontent.com/00000000/photo.jpg', - email: 'janedoe@gmail.com', - rawId: '00000000', - }), + new UserInfo({ + providerId: 'google.com', + displayName: 'Jane Doe', + photoUrl: 'https://lh3.googleusercontent.com/00000000/photo.jpg', + email: 'janedoe@gmail.com', + rawId: '00000000', + }), ]; }).to.throw(Error); }); @@ -626,6 +1119,86 @@ describe('UserRecord', () => { (userRecord.providerData[0] as any).displayName = 'John Smith'; }).to.throw(Error); }); + + it('should return undefined tenantId when not available', () => { + expect(userRecord.tenantId).to.be.undefined; + }); + + it('should return expected tenantId', () => { + const resp = deepCopy(getValidUserResponse('TENANT-ID')) as GetAccountInfoUserResponse; + const tenantUserRecord = new UserRecord(resp); + expect(tenantUserRecord.tenantId).to.equal('TENANT-ID'); + }); + + it('should throw when modifying readonly tenantId property', () => { + expect(() => { + const resp = deepCopy(getValidUserResponse('TENANT-ID')) as GetAccountInfoUserResponse; + const tenantUserRecord = new UserRecord(resp); + (tenantUserRecord as any).tenantId = 'OTHER-TENANT-ID'; + }).to.throw(Error); + }); + + it('should return expected multiFactor', () => { + const multiFactor = new MultiFactorSettings({ + localId: 'uid123', + mfaInfo: [ + { + mfaEnrollmentId: 'enrollmentId1', + displayName: 'displayName1', + enrolledAt: now.toISOString(), + phoneInfo: '+16505551234', + }, + { + mfaEnrollmentId: 'enrollmentId2', + enrolledAt: now.toISOString(), + phoneInfo: '+16505556789', + }, + ], + }); + expect(userRecord.multiFactor).to.deep.equal(multiFactor); + expect(userRecord.multiFactor!.enrolledFactors.length).to.equal(2); + }); + + it('should return undefined multiFactor when not available', () => { + const validUserResponseWithoutMultiFactor = deepCopy(validUserResponse); + delete validUserResponseWithoutMultiFactor.mfaInfo; + const userRecordWithoutMultiFactor = new UserRecord(validUserResponseWithoutMultiFactor); + + expect(userRecordWithoutMultiFactor.multiFactor).to.be.undefined; + }); + + it('should throw when modifying readonly multiFactor property', () => { + expect(() => { + (userRecord as any).multiFactor = new MultiFactorSettings({ + localId: 'uid123', + mfaInfo: [{ + mfaEnrollmentId: 'enrollmentId3', + displayName: 'displayName3', + enrolledAt: now.toISOString(), + phoneInfo: '+16505550000', + }], + }); + }).to.throw(Error); + }); + + it('should throw when modifying readonly multiFactor internals', () => { + expect(() => { + (userRecord.multiFactor!.enrolledFactors[0] as any).displayName = 'Modified'; + }).to.throw(Error); + + expect(() => { + (userRecord.multiFactor!.enrolledFactors as any)[0] = new PhoneMultiFactorInfo({ + mfaEnrollmentId: 'enrollmentId3', + displayName: 'displayName3', + enrolledAt: now.toISOString(), + phoneInfo: '+16505550000', + }); + }).to.throw(Error); + + expect(() => { + (userRecord.multiFactor as any).enrolledFactors = []; + }).to.throw(Error); + }); }); describe('toJSON', () => { @@ -637,8 +1210,14 @@ describe('UserRecord', () => { expect(userRecord.toJSON()).to.deep.equal(getUserJSON()); }); - it('should return null tokensValidAfterTime when not available', () => { - expect((userRecordNoValidSince.toJSON() as any).tokensValidAfterTime).to.be.null; + it('should return undefined tokensValidAfterTime when not available', () => { + expect((userRecordNoValidSince.toJSON() as any).tokensValidAfterTime).to.be.undefined; + }); + + it('should return expected JSON object with tenant ID when available', () => { + const resp = deepCopy(getValidUserResponse('TENANT-ID') as GetAccountInfoUserResponse); + const tenantUserRecord = new UserRecord(resp); + expect(tenantUserRecord.toJSON()).to.deep.equal(getUserJSON('TENANT-ID')); }); }); }); diff --git a/test/unit/database/database.spec.ts b/test/unit/database/database.spec.ts index f6be61e68f..ce86082842 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"); @@ -17,12 +18,15 @@ '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/app/firebase-app'; +import { Database, DatabaseService } from '../../../src/database/database'; +import { ServiceAccountCredential } from '../../../src/app/credential-internal'; +import * as utils from '../utils'; +import { HttpClient, HttpRequestConfig } from '../../../src/utils/api-request'; describe('Database', () => { let mockApp: FirebaseApp; @@ -34,7 +38,7 @@ describe('Database', () => { }); afterEach(() => { - return database.INTERNAL.delete().then(() => { + return database.delete().then(() => { return mockApp.delete(); }); }); @@ -42,7 +46,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); @@ -111,4 +115,563 @@ 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', + expires_in: options?.expiresIn || 3600, + }); + } + + it('should refresh the token 5 minutes before expiration', () => { + database.getDatabase(); + expect(getTokenStub).to.have.not.been.called; + return 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(); + database.getDatabase('https://other-database.firebaseio.com'); + expect(getTokenStub).to.have.not.been.called; + return 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(); + return 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(); + return 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.calledOnce; + + const newExpiryTimeInMillis = token2.expirationTime - Date.now(); + clock.tick(newExpiryTimeInMillis); + expect(getTokenStub).to.have.been.calledOnce; + + getTokenStub.restore(); + getTokenStub = stubCredentials({ expiresIn: 60 * 60 }); + // Force a token refresh + return mockApp.INTERNAL.getToken(true); + }) + .then((token3) => { + expect(getTokenStub).to.have.been.calledOnce; + + const newExpiryTimeInMillis = token3.expirationTime - Date.now(); + clock.tick(newExpiryTimeInMillis - (5 * MINUTE_IN_MILLIS)); + expect(getTokenStub).to.have.been.calledTwice; + }); + }); + + it('should gracefully handle errors during token refresh', () => { + database.getDatabase(); + return 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(); + return 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; + let stubs: sinon.SinonStub[] = []; + + before(() => { + getTokenStub = utils.stubGetAccessToken(mockAccessToken); + }); + + after(() => { + getTokenStub.restore(); + }); + + beforeEach(() => { + return mockApp.INTERNAL.getToken(); + }); + + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + const rules = { + rules: { + '.read': true, + }, + }; + const rulesString = JSON.stringify(rules); + const rulesWithComments = `{ + // Some comments + rules: { + '.read': true, + }, + }`; + const rulesPath = '.settings/rules.json'; + + function callParamsForGet(options?: { strict?: boolean; url?: string }): HttpRequestConfig { + const url = options?.url || `https://databasename.firebaseio.com/${rulesPath}`; + const params: HttpRequestConfig = { + method: 'GET', + url, + headers: { + Authorization: 'Bearer ' + mockAccessToken, + }, + }; + + if (options?.strict) { + params.data = { format: 'strict' }; + } + + return params; + } + + function stubSuccessfulResponse(payload: string | object): sinon.SinonStub { + const expectedResult = utils.responseFrom(payload); + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + return stub; + } + + function stubErrorResponse(payload: string | object): sinon.SinonStub { + const expectedResult = utils.errorFrom(payload); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedResult); + stubs.push(stub); + return stub; + } + + describe('getRules', () => { + it('should return the rules fetched from the database', () => { + const db: Database = database.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()); + }); + }); + + it('should return the rules fetched from the database including comments', () => { + const db: Database = database.getDatabase(); + const stub = stubSuccessfulResponse(rulesWithComments); + return db.getRules().then((result) => { + expect(result).to.equal(rulesWithComments); + return expect(stub).to.have.been.calledOnce.and.calledWith( + callParamsForGet()); + }); + }); + + it('should return the rules fetched from the explicitly specified database', () => { + const db: Database = database.getDatabase('https://custom.firebaseio.com'); + 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: `https://custom.firebaseio.com/${rulesPath}` })); + }); + }); + + it('should return the rules fetched from the custom URL with query params', () => { + const db: Database = database.getDatabase('http://localhost:9000?ns=foo'); + 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: `http://localhost:9000/${rulesPath}?ns=foo` })); + }); + }); + + it('should throw if the server responds with a well-formed error', () => { + const db: Database = database.getDatabase(); + stubErrorResponse({ error: 'test error' }); + return db.getRules().should.eventually.be.rejectedWith( + 'Error while accessing security rules: test error'); + }); + + it('should throw if the server responds with an error', () => { + const db: Database = database.getDatabase(); + stubErrorResponse('error text'); + return db.getRules().should.eventually.be.rejectedWith( + 'Error while accessing security rules: error text'); + }); + + it('should throw in the event of an I/O error', () => { + const db: Database = database.getDatabase(); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects( + new Error('network error')); + stubs.push(stub); + return db.getRules().should.eventually.be.rejectedWith('network error'); + }); + }); + + describe('getRulesWithJSON', () => { + it('should return the rules fetched from the database', () => { + const db: Database = database.getDatabase(); + const stub = stubSuccessfulResponse(rules); + return db.getRulesJSON().then((result) => { + expect(result).to.deep.equal(rules); + return expect(stub).to.have.been.calledOnce.and.calledWith( + callParamsForGet({ strict: true })); + }); + }); + + it('should return the rules fetched from the explicitly specified database', () => { + const db: Database = database.getDatabase('https://custom.firebaseio.com'); + const stub = stubSuccessfulResponse(rules); + return db.getRulesJSON().then((result) => { + expect(result).to.deep.equal(rules); + return expect(stub).to.have.been.calledOnce.and.calledWith( + callParamsForGet({ strict: true, url: `https://custom.firebaseio.com/${rulesPath}` })); + }); + }); + + it('should return the rules fetched from the custom URL with query params', () => { + const db: Database = database.getDatabase('http://localhost:9000?ns=foo'); + const stub = stubSuccessfulResponse(rules); + return db.getRulesJSON().then((result) => { + expect(result).to.deep.equal(rules); + return expect(stub).to.have.been.calledOnce.and.calledWith( + callParamsForGet({ strict: true, url: `http://localhost:9000/${rulesPath}?ns=foo` })); + }); + }); + + it('should throw if the server responds with a well-formed error', () => { + const db: Database = database.getDatabase(); + stubErrorResponse({ error: 'test error' }); + return db.getRulesJSON().should.eventually.be.rejectedWith( + 'Error while accessing security rules: test error'); + }); + + it('should throw if the server responds with an error', () => { + const db: Database = database.getDatabase(); + stubErrorResponse('error text'); + return db.getRulesJSON().should.eventually.be.rejectedWith( + 'Error while accessing security rules: error text'); + }); + + it('should throw in the event of an I/O error', () => { + const db: Database = database.getDatabase(); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects( + new Error('network error')); + stubs.push(stub); + return db.getRulesJSON().should.eventually.be.rejectedWith('network error'); + }); + }); + + function callParamsForPut( + data: string | Buffer | object, + url = `https://databasename.firebaseio.com/${rulesPath}`, + ): HttpRequestConfig { + + return { + method: 'PUT', + url, + headers: { + 'Authorization': 'Bearer ' + mockAccessToken, + 'content-type': 'application/json; charset=utf-8', + }, + data, + }; + } + + describe('setRules', () => { + it('should set the rules when specified as a string', () => { + const db: Database = database.getDatabase(); + const stub = stubSuccessfulResponse({}); + return db.setRules(rulesString).then(() => { + return expect(stub).to.have.been.calledOnce.and.calledWith( + callParamsForPut(rulesString)); + }); + }); + + it('should set the rules when specified as a Buffer', () => { + const db: Database = database.getDatabase(); + const stub = stubSuccessfulResponse({}); + const buffer = Buffer.from(rulesString); + return db.setRules(buffer).then(() => { + return expect(stub).to.have.been.calledOnce.and.calledWith( + callParamsForPut(buffer)); + }); + }); + + it('should set the rules when specified as an object', () => { + const db: Database = database.getDatabase(); + const stub = stubSuccessfulResponse({}); + return db.setRules(rules).then(() => { + return expect(stub).to.have.been.calledOnce.and.calledWith( + callParamsForPut(rules)); + }); + }); + + it('should set the rules with comments when specified as a string', () => { + const db: Database = database.getDatabase(); + const stub = stubSuccessfulResponse({}); + return db.setRules(rulesWithComments).then(() => { + return expect(stub).to.have.been.calledOnce.and.calledWith( + callParamsForPut(rulesWithComments)); + }); + }); + + it('should set the rules in the explicitly specified database', () => { + const db: Database = database.getDatabase('https://custom.firebaseio.com'); + const stub = stubSuccessfulResponse({}); + return db.setRules(rulesString).then(() => { + return expect(stub).to.have.been.calledOnce.and.calledWith( + callParamsForPut(rulesString, `https://custom.firebaseio.com/${rulesPath}`)); + }); + }); + + it('should set the rules using the custom URL with query params', () => { + const db: Database = database.getDatabase('http://localhost:9000?ns=foo'); + const stub = stubSuccessfulResponse({}); + return db.setRules(rulesString).then(() => { + return expect(stub).to.have.been.calledOnce.and.calledWith( + callParamsForPut(rulesString, `http://localhost:9000/${rulesPath}?ns=foo`)); + }); + }); + + const invalidSources: any[] = [null, '', undefined, true, false, 1]; + invalidSources.forEach((invalidSource) => { + it(`should throw if the source is ${JSON.stringify(invalidSource)}`, () => { + const db: Database = database.getDatabase(); + return db.setRules(invalidSource).should.eventually.be.rejectedWith( + 'Source must be a non-empty string, Buffer or an object.'); + }); + }); + + it('should throw if the server responds with a well-formed error', () => { + const db: Database = database.getDatabase(); + stubErrorResponse({ error: 'test error' }); + return db.setRules(rules).should.eventually.be.rejectedWith( + 'Error while accessing security rules: test error'); + }); + + it('should throw if the server responds with an error', () => { + const db: Database = database.getDatabase(); + stubErrorResponse('error text'); + return db.setRules(rules).should.eventually.be.rejectedWith( + 'Error while accessing security rules: error text'); + }); + + it('should throw in the event of an I/O error', () => { + const db: Database = database.getDatabase(); + const stub = sinon.stub(HttpClient.prototype, 'send').rejects( + new Error('network error')); + stubs.push(stub); + 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)); + }); + }); + }); + }); + }); + }); }); diff --git a/test/unit/database/index.spec.ts b/test/unit/database/index.spec.ts new file mode 100644 index 0000000000..382a1d96d3 --- /dev/null +++ b/test/unit/database/index.spec.ts @@ -0,0 +1,100 @@ +/*! + * @license + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +import * as chai from 'chai'; +import * as sinonChai from 'sinon-chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +import * as mocks from '../../resources/mocks'; +import { App } from '../../../src/app/index'; +import { + getDatabase, getDatabaseWithUrl, Database, ServerValue, enableLogging , +} from '../../../src/database/index'; +import { FirebaseApp } from '../../../src/app/firebase-app'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +describe('Database', () => { + let mockApp: App; + + beforeEach(() => { + mockApp = mocks.app(); + }); + + afterEach(() => { + return (mockApp as FirebaseApp).delete(); + }); + + describe('getDatabase()', () => { + it('should throw when default app is not available', () => { + expect(() => { + return getDatabase(); + }).to.throw('The default Firebase app does not exist.'); + }); + + it('should not throw given a valid app', () => { + expect(() => { + return getDatabase(mockApp); + }).not.to.throw(); + }); + + it('should return the same instance for a given app instance', () => { + const db1: Database = getDatabase(mockApp); + const db2: Database = getDatabase(mockApp); + expect(db1).to.equal(db2); + }); + }); + + describe('getDatabaseWithUrl()', () => { + it('should throw when default app is not available', () => { + expect(() => { + return getDatabaseWithUrl('https://test.firebaseio.com'); + }).to.throw('The default Firebase app does not exist.'); + }); + + it('should not throw given a valid app', () => { + expect(() => { + return getDatabaseWithUrl('https://test.firebaseio.com', mockApp); + }).not.to.throw(); + }); + + it('should return the same instance for a given app instance', () => { + const db1: Database = getDatabaseWithUrl('https://test.firebaseio.com', mockApp); + const db2: Database = getDatabaseWithUrl('https://test.firebaseio.com', mockApp); + const db3: Database = getDatabaseWithUrl('https://other.firebaseio.com', mockApp); + expect(db1).to.equal(db2); + expect(db1).to.not.equal(db3); + }); + }); + + it('should expose ServerValue sentinel', () => { + expect(() => ServerValue.increment(1)).to.not.throw(); + }); + + it('should expose enableLogging global function', () => { + expect(() => { + enableLogging(console.log); + enableLogging(false); + }).to.not.throw(); + }); +}); diff --git a/test/unit/eventarc/eventarc-utils.spec.ts b/test/unit/eventarc/eventarc-utils.spec.ts new file mode 100644 index 0000000000..d2f18b7d96 --- /dev/null +++ b/test/unit/eventarc/eventarc-utils.spec.ts @@ -0,0 +1,193 @@ +/*! + * @license + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +import * as sinon from 'sinon'; +import * as utils from '../../../src/eventarc/eventarc-utils'; +import * as chai from 'chai'; +import chaiExclude from 'chai-exclude'; + +const expect = chai.expect; +chai.use(chaiExclude); + +describe('eventarc-utils', () => { + before(() => { + sinon + .stub(Date.prototype, 'toISOString') + .returns('2022-03-16T20:20:42.212Z'); + }); + + after(() => { + sinon.restore(); + }); + + afterEach(() => { + delete process.env.EVENTARC_CLOUD_EVENT_SOURCE; + }); + + describe('toCloudEventProtoFormat', () => { + it('converts cloud event to proto format', () => { + expect(utils.toCloudEventProtoFormat({ + type: 'some.custom.event', + specversion: '1.0', + subject: 'context', + datacontenttype: 'application/json', + id: 'user-provided-id', + data: { + hello: 'world' + }, + source: '/my/functions', + time: new Date().toISOString(), + customattr: 'custom value', + })).to.deep.eq({ + '@type': 'type.googleapis.com/io.cloudevents.v1.CloudEvent', + 'attributes': { + 'customattr': { + 'ceString': 'custom value' + }, + 'datacontenttype': { + 'ceString': 'application/json' + }, + 'time': { + 'ceTimestamp': '2022-03-16T20:20:42.212Z' + }, + 'subject': { + 'ceString': 'context' + } + }, + 'id': 'user-provided-id', + 'source': '/my/functions', + 'specVersion': '1.0', + 'textData': '{"hello":"world"}', + 'type': 'some.custom.event', + }); + }); + + it('populates specversion if not provided', () => { + const got = utils.toCloudEventProtoFormat({ + type: 'some.custom.event', + datacontenttype: 'application/json', + data: { + hello: 'world' + }, + source: '/my/functions', + time: new Date().toISOString(), + }); + expect(got['specVersion']).to.eq('1.0'); + }); + + it('populates time if not provided', () => { + const got = utils.toCloudEventProtoFormat({ + specversion: '1.0', + type: 'some.custom.event', + datacontenttype: 'application/json', + data: { + hello: 'world' + }, + source: '/my/functions', + }); + expect(got['attributes']['time']).to.deep.eq({ + 'ceTimestamp': '2022-03-16T20:20:42.212Z' + }); + }); + + it('populates id if not provided', () => { + const got = utils.toCloudEventProtoFormat({ + type: 'some.custom.event', + id: 'user-provided-id', + datacontenttype: 'application/json', + data: { + hello: 'world' + }, + source: '/my/functions', + time: new Date().toISOString(), + }); + // Couldn't figure out how to stub uuid, so just checking for presense. + expect(got).to.haveOwnProperty('id'); + }); + + it('populates source from EVENTARC_CLOUD_EVENT_SOURCE env var if not set', () => { + process.env.EVENTARC_CLOUD_EVENT_SOURCE = '//source/from/env/var'; + const got = utils.toCloudEventProtoFormat({ + specversion: '1.0', + type: 'some.custom.event', + datacontenttype: 'application/json', + data: { + hello: 'world' + }, + }); + expect(got['source']).to.eq('//source/from/env/var'); + }); + + it('throws invalid argument when source not set', () => { + expect(() => utils.toCloudEventProtoFormat({ + type: 'some.custom.event', + datacontenttype: 'application/json', + data: { + hello: 'world' + }, + time: new Date().toISOString(), + })).throws("CloudEvent 'source' is required."); + }); + + it('throws invalid argument when custom attr not string', () => { + expect(() => utils.toCloudEventProtoFormat({ + type: 'some.custom.event', + datacontenttype: 'application/json', + data: { + hello: 'world' + }, + source: '/my/functions', + time: new Date().toISOString(), + customattr: 123, + })).throws("CloudEvent extension attributes ('customattr') must be string"); + }); + + it('populates converts object data to JSON and sets datacontenttype', () => { + const got = utils.toCloudEventProtoFormat({ + type: 'some.custom.event', + id: 'user-provided-id', + data: { + hello: 'world' + }, + source: '/my/functions', + time: new Date().toISOString(), + }); + // Couldn't figure out how to stub uuid, so just checking for presense. + expect(got['textData']).to.eq('{"hello":"world"}'); + expect(got['attributes']['datacontenttype']).to.deep.eq({ + 'ceString': 'application/json' + }); + }); + + it('populates string data and sets datacontenttype', () => { + const got = utils.toCloudEventProtoFormat({ + type: 'some.custom.event', + id: 'user-provided-id', + data: 'hello world', + source: '/my/functions', + time: new Date().toISOString(), + }); + // Couldn't figure out how to stub uuid, so just checking for presense. + expect(got['textData']).to.eq('hello world'); + expect(got['attributes']['datacontenttype']).to.deep.eq({ + 'ceString': 'text/plain' + }); + }); + }); +}); diff --git a/test/unit/eventarc/eventarc.spec.ts b/test/unit/eventarc/eventarc.spec.ts new file mode 100644 index 0000000000..1bbddd5a4d --- /dev/null +++ b/test/unit/eventarc/eventarc.spec.ts @@ -0,0 +1,572 @@ +/*! + * @license + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +import * as sinon from 'sinon'; +import { Channel, Eventarc } from '../../../src/eventarc'; +import { toCloudEventProtoFormat } from '../../../src/eventarc/eventarc-utils'; +import { CloudEvent } from '../../../src/eventarc/cloudevent'; +import { HttpClient } from '../../../src/utils/api-request'; +import { FirebaseApp } from '../../../src/app/firebase-app'; +import * as mocks from '../../resources/mocks'; +import * as utils from '../utils'; +import * as chai from 'chai'; +import chaiExclude from 'chai-exclude'; +import { getSdkVersion } from '../../../src/utils/index'; + +const expect = chai.expect; +chai.use(chaiExclude); + +const TEST_EVENT1 : CloudEvent = { + type: 'some.custom.event1', + specversion: '1.0', + id: 'user-provided-id-1', + data: 'hello world', + source: '/my/functions', + time: '2011-11-11T11:11:11.111Z', +}; +const TEST_EVENT1_SERIALIZED = JSON.stringify(toCloudEventProtoFormat(TEST_EVENT1)); + +const TEST_EVENT2 : CloudEvent = { + type: 'some.custom.event2', + specversion: '1.0', + id: 'user-provided-id-2', + data: 'hello world', + source: '/my/functions', + time: '2011-11-11T11:11:11.111Z', +}; +const TEST_EVENT2_SERIALIZED = JSON.stringify(toCloudEventProtoFormat(TEST_EVENT2)); + +describe('eventarc', () => { + let mockApp: FirebaseApp; + let eventarc: Eventarc; + + before(() => { + mockApp = mocks.app(); + eventarc = new Eventarc(mockApp); + }); + + after(() => { + sinon.restore(); + }); + + afterEach(() => { + delete process.env.EVENTARC_CLOUD_EVENT_SOURCE; + }); + + describe('Eventarc', () => { + it('inintializes Eventarc object', () => { + expect(eventarc.app).eq(mockApp); + }); + }); + + it('throws invalid argument with creating channel with invalid name', () => { + expect(() => eventarc.channel('foo/bar')) + .throws('Invalid channel name format.'); + expect(() => eventarc.channel('foo/bar/baz')) + .throws('Invalid channel name format.'); + expect(() => eventarc.channel('channels/foo')) + .throws('Invalid channel name format.'); + expect(() => eventarc.channel('us-central1/channels/foo')) + .throws('Invalid channel name format.'); + expect(() => eventarc.channel('projectid/locations/us-central1/channels/foo')) + .throws('Invalid channel name format.'); + expect(() => eventarc.channel('v1/projects/projectid/locations/us-central1/channels/foo')) + .throws('Invalid channel name format.'); + expect(() => eventarc.channel('projects/projectid/channels/foo')) + .throws('Invalid channel name format.'); + expect(() => eventarc.channel('projects/projectid/locations/us-central1')) + .throws('Invalid channel name format.'); + expect(() => eventarc.channel('projects/projectid/locations_us-central1/channels/foo')) + .throws('Invalid channel name format.'); + }); + + describe('default Channel', () => { + let channel : Channel; + let mockAccessToken: string; + let httpStub: sinon.SinonStub; + let accessTokenStub: sinon.SinonStub; + + before(() => { + channel = eventarc.channel(); + mockAccessToken = utils.generateRandomAccessToken(); + accessTokenStub = utils.stubGetAccessToken(mockAccessToken); + }); + + after(() => { + accessTokenStub?.restore(); + }); + + afterEach(() => { + httpStub?.restore(); + }); + + it('inintializes Channel object', () => { + expect(channel.eventarc).eq(eventarc); + expect(channel.name).eq('locations/us-central1/channels/firebase'); + expect(channel.allowedEventTypes).is.undefined; + }); + + it('publishes single event to the API', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish(TEST_EVENT1); + + expect(httpStub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://eventarcpublishing.googleapis.com/v1/projects/project_id/locations/us-central1/channels/firebase:publishEvents', + data: `{"events":[${TEST_EVENT1_SERIALIZED}]}`, + headers: { + 'X-Firebase-Client': 'fire-admin-node/' + getSdkVersion(), + Authorization: 'Bearer ' + mockAccessToken + } + }); + }); + + it('publishes multiple events to the API', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish([TEST_EVENT1, TEST_EVENT2]); + + expect(httpStub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://eventarcpublishing.googleapis.com/v1/projects/project_id/locations/us-central1/channels/firebase:publishEvents', + data: `{"events":[${TEST_EVENT1_SERIALIZED},${TEST_EVENT2_SERIALIZED}]}`, + headers: { + 'X-Firebase-Client': 'fire-admin-node/' + getSdkVersion(), + Authorization: 'Bearer ' + mockAccessToken + } + }); + }); + }); + + describe('full resource name Channel', () => { + let channel : Channel; + let mockAccessToken: string; + let httpStub: sinon.SinonStub; + let accessTokenStub: sinon.SinonStub; + + before(() => { + channel = eventarc.channel('projects/other-project-id/locations/us-west1/channels/my-channel2'); + mockAccessToken = utils.generateRandomAccessToken(); + accessTokenStub = utils.stubGetAccessToken(mockAccessToken); + }); + + after(() => { + accessTokenStub?.restore(); + }); + + afterEach(() => { + httpStub?.restore(); + }); + + it('inintializes Channel object', () => { + expect(channel.eventarc).eq(eventarc); + expect(channel.name).eq('projects/other-project-id/locations/us-west1/channels/my-channel2'); + expect(channel.allowedEventTypes).is.undefined; + }); + + it('publishes single event to the API', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish(TEST_EVENT1); + + expect(httpStub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://eventarcpublishing.googleapis.com/v1/projects/other-project-id/locations/us-west1/channels/my-channel2:publishEvents', + data: `{"events":[${TEST_EVENT1_SERIALIZED}]}`, + headers: { + 'X-Firebase-Client': 'fire-admin-node/' + getSdkVersion(), + Authorization: 'Bearer ' + mockAccessToken + } + }); + }); + + it('publishes multiple events to the API', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish([TEST_EVENT1, TEST_EVENT2]); + + expect(httpStub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://eventarcpublishing.googleapis.com/v1/projects/other-project-id/locations/us-west1/channels/my-channel2:publishEvents', + data: `{"events":[${TEST_EVENT1_SERIALIZED},${TEST_EVENT2_SERIALIZED}]}`, + headers: { + 'X-Firebase-Client': 'fire-admin-node/' + getSdkVersion(), + Authorization: 'Bearer ' + mockAccessToken + } + }); + }); + }); + + describe('partial (no project) Channel', () => { + let channel : Channel; + let mockAccessToken: string; + let httpStub: sinon.SinonStub; + let accessTokenStub: sinon.SinonStub; + + before(() => { + channel = eventarc.channel('locations/us-west1/channels/my-channel'); + mockAccessToken = utils.generateRandomAccessToken(); + accessTokenStub = utils.stubGetAccessToken(mockAccessToken); + }); + + after(() => { + accessTokenStub?.restore(); + }); + + afterEach(() => { + httpStub?.restore(); + }); + + it('inintializes Channel object', () => { + expect(channel.eventarc).eq(eventarc); + expect(channel.name).eq('locations/us-west1/channels/my-channel'); + expect(channel.allowedEventTypes).is.undefined; + }); + + it('publishes single event to the API', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish(TEST_EVENT1); + + expect(httpStub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://eventarcpublishing.googleapis.com/v1/projects/project_id/locations/us-west1/channels/my-channel:publishEvents', + data: `{"events":[${TEST_EVENT1_SERIALIZED}]}`, + headers: { + 'X-Firebase-Client': 'fire-admin-node/' + getSdkVersion(), + Authorization: 'Bearer ' + mockAccessToken + } + }); + }); + + it('publishes multiple events to the API', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish([TEST_EVENT1, TEST_EVENT2]); + + expect(httpStub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://eventarcpublishing.googleapis.com/v1/projects/project_id/locations/us-west1/channels/my-channel:publishEvents', + data: `{"events":[${TEST_EVENT1_SERIALIZED},${TEST_EVENT2_SERIALIZED}]}`, + headers: { + 'X-Firebase-Client': 'fire-admin-node/' + getSdkVersion(), + Authorization: 'Bearer ' + mockAccessToken + } + }); + }); + }); + + describe('partial (channel id only) Channel', () => { + let channel : Channel; + let mockAccessToken: string; + let httpStub: sinon.SinonStub; + let accessTokenStub: sinon.SinonStub; + + before(() => { + channel = eventarc.channel('my-channel'); + mockAccessToken = utils.generateRandomAccessToken(); + accessTokenStub = utils.stubGetAccessToken(mockAccessToken); + }); + + after(() => { + accessTokenStub?.restore(); + }); + + afterEach(() => { + httpStub?.restore(); + }); + + it('inintializes Channel object', () => { + expect(channel.eventarc).eq(eventarc); + expect(channel.name).eq('my-channel'); + expect(channel.allowedEventTypes).is.undefined; + }); + + it('publishes single event to the API', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish(TEST_EVENT1); + + expect(httpStub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://eventarcpublishing.googleapis.com/v1/projects/project_id/locations/us-central1/channels/my-channel:publishEvents', + data: `{"events":[${TEST_EVENT1_SERIALIZED}]}`, + headers: { + 'X-Firebase-Client': 'fire-admin-node/' + getSdkVersion(), + Authorization: 'Bearer ' + mockAccessToken + } + }); + }); + + it('publishes multiple events to the API', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish([TEST_EVENT1, TEST_EVENT2]); + + expect(httpStub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://eventarcpublishing.googleapis.com/v1/projects/project_id/locations/us-central1/channels/my-channel:publishEvents', + data: `{"events":[${TEST_EVENT1_SERIALIZED},${TEST_EVENT2_SERIALIZED}]}`, + headers: { + 'X-Firebase-Client': 'fire-admin-node/' + getSdkVersion(), + Authorization: 'Bearer ' + mockAccessToken + } + }); + }); + }); + + describe('Channel with empty allowed events', () => { + let channel : Channel; + let mockAccessToken: string; + let httpStub: sinon.SinonStub; + let accessTokenStub: sinon.SinonStub; + + before(() => { + channel = eventarc.channel({ allowedEventTypes: [] }); + mockAccessToken = utils.generateRandomAccessToken(); + accessTokenStub = utils.stubGetAccessToken(mockAccessToken); + }); + + after(() => { + accessTokenStub?.restore(); + }); + + afterEach(() => { + httpStub?.restore(); + }); + + it('inintializes Channel object', () => { + expect(channel.eventarc).eq(eventarc); + expect(channel.allowedEventTypes).is.empty; + }); + + it('filters out event and publishes none', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish(TEST_EVENT1); + + expect(httpStub).to.not.have.been.called; + }); + + it('filters out all event and publishes none', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish([TEST_EVENT1, { type: 'foo' }]); + + expect(httpStub).to.not.have.been.called; + }); + }); + + describe('Channel with channel and empty allowed events', () => { + let channel : Channel; + let mockAccessToken: string; + let httpStub: sinon.SinonStub; + let accessTokenStub: sinon.SinonStub; + + before(() => { + channel = eventarc.channel( + 'adasdas', + { allowedEventTypes: [] }); + mockAccessToken = utils.generateRandomAccessToken(); + accessTokenStub = utils.stubGetAccessToken(mockAccessToken); + }); + + after(() => { + accessTokenStub?.restore(); + }); + + afterEach(() => { + httpStub?.restore(); + }); + + it('inintializes Channel object', () => { + expect(channel.eventarc).eq(eventarc); + expect(channel.allowedEventTypes).is.empty; + }); + + it('filters out event and publishes none', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish(TEST_EVENT1); + + expect(httpStub).to.not.have.been.called; + }); + + it('filters out all event and publishes none', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish([TEST_EVENT1, { type: 'foo' }]); + + expect(httpStub).to.not.have.been.called; + }); + }); + + describe('Channel with allowed events', () => { + let channel : Channel; + let mockAccessToken: string; + let httpStub: sinon.SinonStub; + let accessTokenStub: sinon.SinonStub; + + before(() => { + channel = eventarc.channel({ allowedEventTypes: ['some.custom.event1'] }); + mockAccessToken = utils.generateRandomAccessToken(); + accessTokenStub = utils.stubGetAccessToken(mockAccessToken); + }); + + after(() => { + accessTokenStub?.restore(); + }); + + afterEach(() => { + httpStub?.restore(); + }); + + it('inintializes Channel object', () => { + expect(channel.eventarc).eq(eventarc); + expect(channel.allowedEventTypes).deep.eq(['some.custom.event1']); + }); + + it('publishes events with allowed type', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish(TEST_EVENT1); + + expect(httpStub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://eventarcpublishing.googleapis.com/v1/projects/project_id/locations/us-central1/channels/firebase:publishEvents', + data: `{"events":[${TEST_EVENT1_SERIALIZED}]}`, + headers: { + 'X-Firebase-Client': 'fire-admin-node/' + getSdkVersion(), + Authorization: 'Bearer ' + mockAccessToken + } + }); + }); + + it('publishes events with allowed type and filters out others', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish([TEST_EVENT1, { + type: 'some.custom.event2' + }]); + + expect(httpStub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://eventarcpublishing.googleapis.com/v1/projects/project_id/locations/us-central1/channels/firebase:publishEvents', + data: `{"events":[${TEST_EVENT1_SERIALIZED}]}`, + headers: { + 'X-Firebase-Client': 'fire-admin-node/' + getSdkVersion(), + Authorization: 'Bearer ' + mockAccessToken + } + }); + }); + }); + + describe('Channel with allowed events as string', () => { + let channel : Channel; + let mockAccessToken: string; + let httpStub: sinon.SinonStub; + let accessTokenStub: sinon.SinonStub; + + before(() => { + channel = eventarc.channel({ allowedEventTypes: 'some.custom.event1,some.other.event.type' }); + mockAccessToken = utils.generateRandomAccessToken(); + accessTokenStub = utils.stubGetAccessToken(mockAccessToken); + }); + + after(() => { + accessTokenStub?.restore(); + }); + + afterEach(() => { + httpStub?.restore(); + }); + + it('inintializes Channel object', () => { + expect(channel.eventarc).eq(eventarc); + expect(channel.allowedEventTypes).deep.eq(['some.custom.event1', 'some.other.event.type']); + }); + + it('publishes events with allowed type', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish(TEST_EVENT1); + + expect(httpStub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://eventarcpublishing.googleapis.com/v1/projects/project_id/locations/us-central1/channels/firebase:publishEvents', + data: `{"events":[${TEST_EVENT1_SERIALIZED}]}`, + headers: { + 'X-Firebase-Client': 'fire-admin-node/' + getSdkVersion(), + Authorization: 'Bearer ' + mockAccessToken + } + }); + }); + + it('publishes events with allowed type and filters out others', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish([TEST_EVENT1, { + type: 'some.custom.event2' + }]); + + expect(httpStub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://eventarcpublishing.googleapis.com/v1/projects/project_id/locations/us-central1/channels/firebase:publishEvents', + data: `{"events":[${TEST_EVENT1_SERIALIZED}]}`, + headers: { + 'X-Firebase-Client': 'fire-admin-node/' + getSdkVersion(), + Authorization: 'Bearer ' + mockAccessToken + } + }); + }); + }); +}); diff --git a/test/unit/extensions/extensions-api-client-internal.spec.ts b/test/unit/extensions/extensions-api-client-internal.spec.ts new file mode 100644 index 0000000000..a95baff154 --- /dev/null +++ b/test/unit/extensions/extensions-api-client-internal.spec.ts @@ -0,0 +1,90 @@ +/*! + * @license + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import * as sinon from 'sinon'; + +import * as utils from '../utils'; +import * as mocks from '../../resources/mocks'; +import { FirebaseApp } from '../../../src/app/firebase-app'; +import { ExtensionsApiClient, FirebaseExtensionsError } from '../../../src/extensions/extensions-api-client-internal'; +import { HttpClient, HttpRequestConfig } from '../../../src/utils/api-request'; +import { SettableProcessingState } from '../../../src/extensions/extensions-api'; + +const testProjectId = 'test-project'; +const testInstanceId = 'test-instance'; + +describe('Extension API client', () => { + let app: FirebaseApp; + let apiClient: ExtensionsApiClient; + + let httpClientStub: sinon.SinonStub; + const mockOptions = { + credential: new mocks.MockCredential(), + projectId: 'test-project', + serviceAccountId: 'service-acct@email.com' + }; + + before(() => { + app = mocks.appWithOptions(mockOptions); + apiClient = new ExtensionsApiClient(app); + }); + + after(() => { + return app.delete(); + }); + + beforeEach(() => { + httpClientStub = sinon.stub(HttpClient.prototype, 'send'); + }); + + afterEach(() => { + httpClientStub.restore(); + }); + + describe('Constructor', () => { + it('should reject when the app is null', () => { + expect(() => new ExtensionsApiClient(null as unknown as FirebaseApp)) + .to.throw('First argument passed to getExtensions() must be a valid Firebase app instance.'); + }); + }); + + describe('updateRuntimeData', () => { + it('should updateRuntimeData', async () => { + const testRuntimeData = { + processingState: { + state: 'PROCESSING_COMPLETE' as SettableProcessingState, + detailMessage: 'done processing', + }, + } + const expected = sinon.match((req: HttpRequestConfig) => { + const url = 'https://firebaseextensions.googleapis.com/' + + 'v1beta/projects/test-project/instances/test-instance/runtimeData'; + return req.method == 'PATCH' && req.url == url && req.data == testRuntimeData; + }, 'Incorrect URL or Method'); + httpClientStub.withArgs(expected).resolves(utils.responseFrom(testRuntimeData, 200)); + await expect(apiClient.updateRuntimeData(testProjectId, testInstanceId, testRuntimeData)) + .to.eventually.deep.equal(testRuntimeData); + }); + + it('should convert errors in FirebaseErrors', async () => { + httpClientStub.rejects(utils.errorFrom('Something went wrong', 404)); + await expect(apiClient.updateRuntimeData(testProjectId, testInstanceId, {})) + .to.eventually.be.rejectedWith(FirebaseExtensionsError); + }); + }); +}); diff --git a/test/unit/extensions/extensions.spec.ts b/test/unit/extensions/extensions.spec.ts new file mode 100644 index 0000000000..0b3ffb3004 --- /dev/null +++ b/test/unit/extensions/extensions.spec.ts @@ -0,0 +1,179 @@ +/*! + * @license + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as sinon from 'sinon'; +import { expect } from 'chai'; + +import * as mocks from '../../resources/mocks'; +import * as utils from '../utils'; +import { FirebaseApp } from '../../../src/app/firebase-app'; +import { Extensions } from '../../../src/extensions/extensions'; +import { FirebaseAppError } from '../../../src/utils/error'; +import { HttpClient, HttpRequestConfig } from '../../../src/utils/api-request'; +import { SettableProcessingState } from '../../../src/extensions/extensions-api'; +import { FirebaseExtensionsError } from '../../../src/extensions/extensions-api-client-internal'; + +describe('Extensions', () => { + const mockOptions = { + credential: new mocks.MockCredential(), + projectId: 'test-project', + }; + + let extensions: Extensions; + let mockApp: FirebaseApp; + + beforeEach(() => { + mockApp = mocks.appWithOptions(mockOptions); + extensions = new Extensions(mockApp); + }); + + afterEach(() => { + return mockApp.delete(); + }); + + describe('Constructor', () => { + it('should reject when the app is null', () => { + expect(() => new Extensions(null as unknown as FirebaseApp)) + .to.throw(FirebaseAppError); + }); + }); + + describe('app', () => { + it('returns the app from the constructor', () => { + // We expect referential equality here + expect(extensions.app).to.equal(mockApp); + }); + }); + + describe('Runtime', () => { + let processEnvCopy: Record; + beforeEach(() => { + processEnvCopy = JSON.parse(JSON.stringify(process.env)) as Record; + }); + + afterEach(() => { + process.env = processEnvCopy; + }); + + describe('Constructor', () => { + it('should error if called without PROJECT_ID', () => { + process.env['EXT_INSTANCE_ID'] = 'test-instance'; + expect(() => extensions.runtime()) + .to.throw('PROJECT_ID must not be undefined in Extensions runtime environment'); + }); + + + it('should error if called without EXT_INSTANCE_ID', () => { + process.env['PROJECT_ID'] = 'test-project'; + expect(() => extensions.runtime()) + .to.throw('Runtime is only available from within a running Extension instance.'); + }); + + it('should not error if called from an extension', () => { + process.env['PROJECT_ID'] = 'test-project'; + process.env['EXT_INSTANCE_ID'] = 'test-instance'; + expect(() => extensions.runtime()).not.to.throw(); + }); + }); + + describe('setProcessingState', () => { + let httpClientStub: sinon.SinonStub; + beforeEach(() => { + process.env['PROJECT_ID'] = 'test-project'; + process.env['EXT_INSTANCE_ID'] = 'test-instance'; + httpClientStub = sinon.stub(HttpClient.prototype, 'send'); + }); + + afterEach(() => { + httpClientStub.restore(); + }); + + for (const state of ['PROCESSING_FAILED', 'PROCESSING_WARNING','PROCESSING_COMPLETE', 'NONE']) { + it(`should set ${state} state`, async () => { + const expectedRuntimeData = { + processingState: { + state: state as SettableProcessingState, + detailMessage: 'done processing', + }, + } + const expected = sinon.match((req: HttpRequestConfig) => { + const url = 'https://firebaseextensions.googleapis.com/' + + 'v1beta/projects/test-project/instances/test-instance/runtimeData'; + return req.method == 'PATCH' && + req.url == url && + JSON.stringify(req.data) == JSON.stringify(expectedRuntimeData); + }, 'Incorrect URL or Method'); + httpClientStub.withArgs(expected).resolves(utils.responseFrom(expectedRuntimeData, 200)); + + + await extensions.runtime().setProcessingState(state as SettableProcessingState, 'done processing'); + expect(httpClientStub).to.have.been.calledOnce; + }); + } + + it('should covert errors in FirebaseErrors', async () => { + httpClientStub.rejects(utils.errorFrom('Something went wrong', 404)); + await expect(extensions.runtime().setProcessingState('PROCESSING_COMPLETE', 'a message')) + .to.eventually.be.rejectedWith(FirebaseExtensionsError); + }); + }); + + describe('setFatalError', () => { + let httpClientStub: sinon.SinonStub; + beforeEach(() => { + process.env['PROJECT_ID'] = 'test-project'; + process.env['EXT_INSTANCE_ID'] = 'test-instance'; + httpClientStub = sinon.stub(HttpClient.prototype, 'send'); + }); + + afterEach(() => { + httpClientStub.restore(); + }); + + it('should set fatal error', async () => { + const expectedRuntimeData = { + fatalError: { + errorMessage: 'A bad error!', + }, + }; + const expected = sinon.match((req: HttpRequestConfig) => { + const url = 'https://firebaseextensions.googleapis.com/' + + 'v1beta/projects/test-project/instances/test-instance/runtimeData'; + return req.method == 'PATCH' && + req.url == url && + JSON.stringify(req.data) == JSON.stringify(expectedRuntimeData); + }, 'Incorrect URL or Method'); + httpClientStub.withArgs(expected).resolves(utils.responseFrom(expectedRuntimeData, 200)); + + + await extensions.runtime().setFatalError('A bad error!'); + expect(httpClientStub).to.have.been.calledOnce; + }); + + it('should error if errorMessage is empty', async () => { + await expect(extensions.runtime().setFatalError('')) + .to.eventually.be.rejectedWith(FirebaseExtensionsError, 'errorMessage must not be empty'); + }); + + it('should convert errors in FirebaseErrors', async () => { + httpClientStub.rejects(utils.errorFrom('Something went wrong', 404)); + await expect(extensions.runtime().setFatalError('a message')) + .to.eventually.be.rejectedWith(FirebaseExtensionsError); + }); + }) + }); +}); diff --git a/test/unit/firebase-namespace.spec.ts b/test/unit/firebase-namespace.spec.ts deleted file mode 100644 index 750866e4c1..0000000000 --- a/test/unit/firebase-namespace.spec.ts +++ /dev/null @@ -1,588 +0,0 @@ -/*! - * 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 nock from 'nock'; -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 {FirebaseNamespace} from '../../src/firebase-namespace'; -import {FirebaseApp} from '../../src/firebase-app'; -import {Auth} from '../../src/auth/auth'; -import { - enableLogging, - Database, - DataSnapshot, - OnDisconnect, - Query, - Reference, - ServerValue, -} from '@firebase/database'; -import {Messaging} from '../../src/messaging/messaging'; -import {Storage} from '../../src/storage/storage'; -import { - Firestore, - FieldPath, - FieldValue, - GeoPoint, - setLogFunction, -} from '@google-cloud/firestore'; -import {InstanceId} from '../../src/instance-id/instance-id'; - -chai.should(); -chai.use(sinonChai); -chai.use(chaiAsPromised); - -const expect = chai.expect; - - -const DEFAULT_APP_NAME = '[DEFAULT]'; -const DEFAULT_APP_NOT_FOUND = 'The default Firebase app does not exist. Make sure you call initializeApp() ' - + 'before using any of the Firebase services.'; - -describe('FirebaseNamespace', () => { - let firebaseNamespace: FirebaseNamespace; - - before(() => utils.mockFetchAccessTokenRequests()); - - after(() => nock.cleanAll()); - - beforeEach(() => { - firebaseNamespace = new FirebaseNamespace(); - }); - - describe('#SDK_VERSION', () => { - it('should return the SDK version', () => { - expect(firebaseNamespace.SDK_VERSION).to.equal(''); - }); - }); - - describe('#apps', () => { - it('should return an empty array if there are no apps within this namespace', () => { - expect(firebaseNamespace.apps).to.deep.equal([]); - }); - - it('should return an array of apps within this namespace', () => { - const appNames = ['one', 'two', 'three']; - const apps = appNames.map((appName) => { - return firebaseNamespace.initializeApp(mocks.appOptions, appName); - }); - - expect(firebaseNamespace.apps).to.have.length(apps.length); - expect(firebaseNamespace.apps).to.deep.equal(apps); - }); - - it('should not include apps which have been deleted', () => { - const appNames = ['one', 'two', 'three']; - const apps = appNames.map((appName) => { - return firebaseNamespace.initializeApp(mocks.appOptions, appName); - }); - - return apps[0].delete().then(() => { - apps.shift(); - expect(firebaseNamespace.apps).to.have.length(apps.length); - expect(firebaseNamespace.apps).to.deep.equal(apps); - }); - }); - - it('should be read-only', () => { - expect(() => { - (firebaseNamespace as any).apps = 'foo'; - }).to.throw(`Cannot set property apps of # which has only a getter`); - }); - }); - - describe('#app()', () => { - const invalidAppNames = [null, NaN, 0, 1, true, false, [], ['a'], {}, { a: 1 }, _.noop]; - invalidAppNames.forEach((invalidAppName) => { - it('should throw given non-string app name: ' + JSON.stringify(invalidAppName), () => { - expect(() => { - return firebaseNamespace.app(invalidAppName as any); - }).to.throw(`Invalid Firebase app name "${invalidAppName}" provided. App name must be a non-empty string.`); - }); - }); - - 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.`); - }); - - it('should throw given an app name which does not correspond to an existing app', () => { - expect(() => { - return firebaseNamespace.app(mocks.appName); - }).to.throw(`Firebase app named "${mocks.appName}" does not exist.`); - }); - - it('should throw given a deleted app', () => { - const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); - return app.delete().then(() => { - expect(() => { - return firebaseNamespace.app(mocks.appName); - }).to.throw(`Firebase app named "${mocks.appName}" does not exist.`); - }); - }); - - it('should throw given no app name if the default app does not exist', () => { - expect(() => { - return firebaseNamespace.app(); - }).to.throw('The default Firebase app does not exist.'); - }); - - it('should return the app associated with the provided app name', () => { - const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); - expect(firebaseNamespace.app(mocks.appName)).to.deep.equal(app); - }); - - it('should return the default app if no app name is provided', () => { - const app = firebaseNamespace.initializeApp(mocks.appOptions); - expect(firebaseNamespace.app()).to.deep.equal(app); - }); - - it('should return the default app if the default app name is provided', () => { - const app = firebaseNamespace.initializeApp(mocks.appOptions); - expect(firebaseNamespace.app(DEFAULT_APP_NAME)).to.deep.equal(app); - }); - }); - - describe('#initializeApp()', () => { - const invalidAppNames = [null, NaN, 0, 1, true, false, [], ['a'], {}, { a: 1 }, _.noop]; - invalidAppNames.forEach((invalidAppName) => { - it('should throw given non-string app name: ' + JSON.stringify(invalidAppName), () => { - expect(() => { - firebaseNamespace.initializeApp(mocks.appOptions, invalidAppName as any); - }).to.throw(`Invalid Firebase app name "${invalidAppName}" provided. App name must be a non-empty string.`); - }); - }); - - 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.`); - }); - - it('should throw given a name corresponding to an existing app', () => { - expect(() => { - firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); - firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); - }).to.throw(`Firebase app named "${mocks.appName}" already exists.`); - }); - - it('should throw given no app name if the default app already exists', () => { - expect(() => { - firebaseNamespace.initializeApp(mocks.appOptions); - firebaseNamespace.initializeApp(mocks.appOptions); - }).to.throw('The default Firebase app already exists.'); - - expect(() => { - firebaseNamespace.initializeApp(mocks.appOptions); - firebaseNamespace.initializeApp(mocks.appOptions, DEFAULT_APP_NAME); - }).to.throw('The default Firebase app already exists.'); - - expect(() => { - firebaseNamespace.initializeApp(mocks.appOptions, DEFAULT_APP_NAME); - firebaseNamespace.initializeApp(mocks.appOptions); - }).to.throw('The default Firebase app already exists.'); - - expect(() => { - firebaseNamespace.initializeApp(mocks.appOptions, DEFAULT_APP_NAME); - firebaseNamespace.initializeApp(mocks.appOptions, DEFAULT_APP_NAME); - }).to.throw('The default Firebase app already exists.'); - }); - - it('should return a new app with the provided options and app name', () => { - const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); - expect(app.name).to.equal(mocks.appName); - expect(app.options).to.deep.equal(mocks.appOptions); - }); - - it('should return an app with the default app name if no app name is provided', () => { - const app = firebaseNamespace.initializeApp(mocks.appOptions); - expect(app.name).to.deep.equal(DEFAULT_APP_NAME); - }); - - it('should allow re-use of a deleted app name', () => { - let app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); - return app.delete().then(() => { - app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); - expect(firebaseNamespace.app(mocks.appName)).to.deep.equal(app); - }); - }); - - it('should add the new app to the namespace\'s app list', () => { - 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()', () => { - const invalidAppNames = [null, NaN, 0, 1, true, false, [], ['a'], {}, { a: 1 }, _.noop]; - invalidAppNames.forEach((invalidAppName) => { - it('should throw given non-string app name: ' + JSON.stringify(invalidAppName), () => { - expect(() => { - firebaseNamespace.INTERNAL.removeApp(invalidAppName as any); - }).to.throw(`Invalid Firebase app name "${invalidAppName}" provided. App name must be a non-empty string.`); - }); - }); - - 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.`); - }); - - it('should throw given an app name which does not correspond to an existing app', () => { - expect(() => { - firebaseNamespace.INTERNAL.removeApp(mocks.appName); - }).to.throw(`Firebase app named "${mocks.appName}" does not exist.`); - }); - - 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.`); - }); - - 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.`); - }); - - it('should remove the app corresponding to the provided app name from the namespace\'s app list', () => { - firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); - firebaseNamespace.INTERNAL.removeApp(mocks.appName); - expect(() => { - return firebaseNamespace.app(mocks.appName); - }).to.throw(`Firebase app named "${mocks.appName}" does not exist.`); - }); - - it('should remove the default app from the namespace\'s app list if the default app name is provided', () => { - firebaseNamespace.initializeApp(mocks.appOptions); - firebaseNamespace.INTERNAL.removeApp(DEFAULT_APP_NAME); - expect(() => { - return firebaseNamespace.app(); - }).to.throw('The default Firebase app does not exist.'); - }); - - it('should not be idempotent', () => { - firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); - firebaseNamespace.INTERNAL.removeApp(mocks.appName); - expect(() => { - 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.reset(); - - 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, 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()', () => { - it('should throw when called before initializing an app', () => { - expect(() => { - firebaseNamespace.auth(); - }).to.throw(DEFAULT_APP_NOT_FOUND); - }); - - it('should throw when default app is not initialized', () => { - firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); - expect(() => { - firebaseNamespace.auth(); - }).to.throw(DEFAULT_APP_NOT_FOUND); - }); - - it('should return a valid namespace when the default app is initialized', () => { - const app: FirebaseApp = 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 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); - }); - }); - - describe('#database()', () => { - it('should throw when called before initializing an app', () => { - expect(() => { - firebaseNamespace.database(); - }).to.throw(DEFAULT_APP_NOT_FOUND); - }); - - it('should throw when default app is not initialized', () => { - firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); - expect(() => { - firebaseNamespace.database(); - }).to.throw(DEFAULT_APP_NOT_FOUND); - }); - - it('should return a valid namespace when the default app is initialized', () => { - const app: FirebaseApp = 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: 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); - }); - - it('should return a reference to DataSnapshot type', () => { - expect(firebaseNamespace.database.DataSnapshot).to.be.deep.equal(DataSnapshot); - }); - - it('should return a reference to OnDisconnect type', () => { - expect(firebaseNamespace.database.OnDisconnect).to.be.deep.equal(OnDisconnect); - }); - - it('should return a reference to Query type', () => { - expect(firebaseNamespace.database.Query).to.be.deep.equal(Query); - }); - - it('should return a reference to Reference type', () => { - expect(firebaseNamespace.database.Reference).to.be.deep.equal(Reference); - }); - - it('should return a reference to ServerValue type', () => { - expect(firebaseNamespace.database.ServerValue).to.be.deep.equal(ServerValue); - }); - - it('should return a reference to enableLogging function', () => { - expect(firebaseNamespace.database.enableLogging).to.be.deep.equal(enableLogging); - }); - }); - - describe('#messaging()', () => { - it('should throw when called before initializing an app', () => { - expect(() => { - firebaseNamespace.messaging(); - }).to.throw(DEFAULT_APP_NOT_FOUND); - }); - - it('should throw when default app is not initialized', () => { - firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); - expect(() => { - firebaseNamespace.messaging(); - }).to.throw(DEFAULT_APP_NOT_FOUND); - }); - - it('should return a valid namespace when the default app is initialized', () => { - const app: FirebaseApp = 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 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); - }); - }); - - describe('#storage()', () => { - it('should throw when called before initializing an app', () => { - expect(() => { - firebaseNamespace.storage(); - }).to.throw(DEFAULT_APP_NOT_FOUND); - }); - - it('should throw when default app is not initialized', () => { - firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); - expect(() => { - firebaseNamespace.storage(); - }).to.throw(DEFAULT_APP_NOT_FOUND); - }); - - it('should return a valid namespace when the default app is initialized', () => { - const app: FirebaseApp = 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 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); - }); - }); - - describe('#firestore()', () => { - it('should throw when called before initializing an app', () => { - expect(() => { - firebaseNamespace.firestore(); - }).to.throw(DEFAULT_APP_NOT_FOUND); - }); - - it('should throw when default app is not initialized', () => { - firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); - expect(() => { - firebaseNamespace.firestore(); - }).to.throw(DEFAULT_APP_NOT_FOUND); - }); - - it('should return a valid namespace when the default app is initialized', () => { - const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions); - const fs: Firestore = firebaseNamespace.firestore(); - expect(fs).to.not.be.null; - }); - - it('should return a valid namespace when the named app is initialized', () => { - const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); - const fs: Firestore = firebaseNamespace.firestore(app); - expect(fs).to.not.be.null; - }); - - it('should return a reference to Firestore type', () => { - expect(firebaseNamespace.firestore.Firestore).to.be.deep.equal(Firestore); - }); - - it('should return a reference to FieldPath type', () => { - expect(firebaseNamespace.firestore.FieldPath).to.be.deep.equal(FieldPath); - }); - - it('should return a reference to FieldValue type', () => { - expect(firebaseNamespace.firestore.FieldValue).to.be.deep.equal(FieldValue); - }); - - it('should return a reference to GeoPoint type', () => { - expect(firebaseNamespace.firestore.GeoPoint).to.be.deep.equal(GeoPoint); - }); - - it('should return a reference to setLogFunction', () => { - expect(firebaseNamespace.firestore.setLogFunction).to.be.deep.equal(setLogFunction); - }); - }); - - describe('#instanceId()', () => { - it('should throw when called before initializing an app', () => { - expect(() => { - firebaseNamespace.instanceId(); - }).to.throw(DEFAULT_APP_NOT_FOUND); - }); - - it('should throw when default app is not initialized', () => { - firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); - expect(() => { - firebaseNamespace.instanceId(); - }).to.throw(DEFAULT_APP_NOT_FOUND); - }); - - it('should return a valid namespace when the default app is initialized', () => { - const app: FirebaseApp = 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 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); - }); - }); -}); diff --git a/test/unit/firebase.spec.ts b/test/unit/firebase.spec.ts index a96353c9cd..ca441f852d 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"); @@ -20,15 +21,18 @@ import path = require('path'); import * as _ from 'lodash'; +import * as sinon from 'sinon'; import * as chai from 'chai'; -import * as nock from 'nock'; import * as chaiAsPromised from 'chai-as-promised'; -import * as utils from './utils'; import * as mocks from '../resources/mocks'; import * as firebaseAdmin from '../../src/index'; -import {ApplicationDefaultCredential} from '../../src/auth/credential'; +import { FirebaseApp, FirebaseAppInternals } from '../../src/app/firebase-app'; +import { + RefreshTokenCredential, ServiceAccountCredential, isApplicationDefault +} from '../../src/app/credential-internal'; +import { defaultAppStore, initializeApp } from '../../src/app/lifecycle'; chai.should(); chai.use(chaiAsPromised); @@ -37,26 +41,25 @@ const expect = chai.expect; describe('Firebase', () => { - let mockedRequests: nock.Scope[] = []; + let getTokenStub: sinon.SinonStub; - before(() => utils.mockFetchAccessTokenRequests()); - - after(() => nock.cleanAll()); - - afterEach(() => { - const deletePromises = []; - firebaseAdmin.apps.forEach((app) => { - deletePromises.push(app.delete()); + before(() => { + getTokenStub = sinon.stub(ServiceAccountCredential.prototype, 'getAccessToken').resolves({ + access_token: 'mock-access-token', + expires_in: 3600, }); + }); - _.forEach(mockedRequests, (mockedRequest) => mockedRequest.done()); - mockedRequests = []; + after(() => { + getTokenStub.restore(); + }); - return Promise.all(deletePromises); + afterEach(() => { + return defaultAppStore.clearAllApps(); }); describe('#initializeApp()', () => { - const invalidOptions = [null, NaN, 0, 1, true, false, '', 'a', [], _.noop]; + const invalidOptions: any[] = [null, NaN, 0, 1, true, false, '', 'a', [], _.noop]; invalidOptions.forEach((invalidOption: any) => { it('should throw given invalid options object: ' + JSON.stringify(invalidOption), () => { expect(() => { @@ -68,7 +71,7 @@ describe('Firebase', () => { it('should use application default credentials when no credentials are explicitly specified', () => { const app = firebaseAdmin.initializeApp(mocks.appOptionsNoAuth); expect(app.options).to.have.property('credential'); - expect(app.options.credential).to.be.instanceOf(ApplicationDefaultCredential); + expect(app.options.credential).to.not.be.undefined; }); it('should not modify the provided options object', () => { @@ -91,9 +94,7 @@ describe('Firebase', () => { it('should throw given a credential which doesn\'t implement the Credential interface', () => { expect(() => { firebaseAdmin.initializeApp({ - credential: { - foo: () => null, - }, + credential: {}, } as any); }).to.throw('Invalid Firebase app options'); @@ -111,7 +112,8 @@ describe('Firebase', () => { credential: firebaseAdmin.credential.cert(mocks.certificateObject), }); - return firebaseAdmin.app().INTERNAL.getToken() + expect(isApplicationDefault(firebaseAdmin.app().options.credential)).to.be.false; + return getAppInternals().getToken() .should.eventually.have.keys(['accessToken', 'expirationTime']); }); @@ -121,19 +123,20 @@ describe('Firebase', () => { credential: firebaseAdmin.credential.cert(keyPath), }); - return firebaseAdmin.app().INTERNAL.getToken() + expect(isApplicationDefault(firebaseAdmin.app().options.credential)).to.be.false; + return getAppInternals().getToken() .should.eventually.have.keys(['accessToken', 'expirationTime']); }); it('should initialize SDK given an application default credential', () => { - let credPath: string; - credPath = process.env.GOOGLE_APPLICATION_CREDENTIALS; + const credPath: string | undefined = process.env.GOOGLE_APPLICATION_CREDENTIALS; process.env.GOOGLE_APPLICATION_CREDENTIALS = path.resolve(__dirname, '../resources/mock.key.json'); firebaseAdmin.initializeApp({ credential: firebaseAdmin.credential.applicationDefault(), }); - return firebaseAdmin.app().INTERNAL.getToken().then((token) => { + expect(isApplicationDefault(firebaseAdmin.app().options.credential)).to.be.true; + return getAppInternals().getToken().then((token) => { if (typeof credPath === 'undefined') { delete process.env.GOOGLE_APPLICATION_CREDENTIALS; } else { @@ -143,16 +146,38 @@ describe('Firebase', () => { }).should.eventually.have.keys(['accessToken', 'expirationTime']); }); - // TODO(jwenger): mock out the refresh token endpoint so this test will work - xit('should initialize SDK given a refresh token credential', () => { - nock.recorder.rec(); + it('should initialize SDK given a refresh token credential', () => { + getTokenStub.restore(); + getTokenStub = sinon.stub(RefreshTokenCredential.prototype, 'getAccessToken') + .resolves({ + access_token: 'mock-access-token', + expires_in: 3600, + }); firebaseAdmin.initializeApp({ credential: firebaseAdmin.credential.refreshToken(mocks.refreshToken), }); - return firebaseAdmin.app().INTERNAL.getToken() + expect(isApplicationDefault(firebaseAdmin.app().options.credential)).to.be.false; + return getAppInternals().getToken() .should.eventually.have.keys(['accessToken', 'expirationTime']); }); + + it('should initialize App instance with extended service methods', () => { + const app = firebaseAdmin.initializeApp(mocks.appOptions); + expect((app as any).__extended).to.be.true; + expect(app.auth).to.be.not.undefined; + }); + + it('should add extended service methods when retrieved via namespace', () => { + const app = initializeApp(mocks.appOptions); + expect((app as any).__extended).to.be.undefined; + expect((app as any).auth).to.be.undefined; + + const extendedApp = firebaseAdmin.app(); + expect(app).to.equal(extendedApp); + expect((app as any).__extended).to.be.true; + expect((app as any).auth).to.be.not.undefined; + }); }); describe('#database()', () => { @@ -208,6 +233,36 @@ describe('Firebase', () => { }); }); + describe('#remoteConfig', () => { + it('should throw if the app has not be initialized', () => { + expect(() => { + return firebaseAdmin.remoteConfig(); + }).to.throw('The default Firebase app does not exist.'); + }); + + it('should return the remoteConfig service', () => { + firebaseAdmin.initializeApp(mocks.appOptions); + expect(() => { + return firebaseAdmin.remoteConfig(); + }).not.to.throw(); + }); + }); + + 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(() => { @@ -222,4 +277,8 @@ describe('Firebase', () => { }).not.to.throw(); }); }); + + function getAppInternals(): FirebaseAppInternals { + return (firebaseAdmin.app() as unknown as FirebaseApp).INTERNAL; + } }); diff --git a/test/unit/firestore/firestore.spec.ts b/test/unit/firestore/firestore.spec.ts index bee168b389..19d769c555 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"); @@ -16,41 +17,56 @@ 'use strict'; -import path = require('path'); 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 {ApplicationDefaultCredential} from '../../../src/auth/credential'; -import {FirestoreService} from '../../../src/firestore/firestore'; +import { FirebaseApp } from '../../../src/app/firebase-app'; +import { + ComputeEngineCredential, RefreshTokenCredential +} from '../../../src/app/credential-internal'; +import { FirestoreService, getFirestoreOptions } from '../../../src/firestore/firestore-internal'; +import { DEFAULT_DATABASE_ID } from '@google-cloud/firestore/build/src/path'; describe('Firestore', () => { let mockApp: FirebaseApp; let mockCredentialApp: FirebaseApp; - let defaultCredentialApp: FirebaseApp; let projectIdApp: FirebaseApp; let firestore: any; - let appCredentials: string; - let gcloudProject: string; + let appCredentials: string | undefined; + let googleCloudProject: string | undefined; + let gcloudProject: string | undefined; const invalidCredError = 'Failed to initialize Google Cloud Firestore client with the available ' + 'credentials. Must initialize the SDK with a certificate credential or application default ' + 'credentials to use Cloud Firestore API.'; - const mockServiceAccount = path.resolve(__dirname, '../../resources/mock.key.json'); + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { version: firebaseVersion } = require('../../../package.json'); + const defaultCredentialApps = [ + { + name: 'ComputeEngineCredentials', + app: mocks.appWithOptions({ + credential: new ComputeEngineCredential(), + }), + }, + { + name: 'RefreshTokenCredentials', + app: mocks.appWithOptions({ + credential: new RefreshTokenCredential(mocks.refreshToken, undefined, true), + }), + }, + ]; beforeEach(() => { appCredentials = process.env.GOOGLE_APPLICATION_CREDENTIALS; + googleCloudProject = process.env.GOOGLE_CLOUD_PROJECT; gcloudProject = process.env.GCLOUD_PROJECT; delete process.env.GOOGLE_APPLICATION_CREDENTIALS; mockApp = mocks.app(); mockCredentialApp = mocks.mockCredentialApp(); - defaultCredentialApp = mocks.appWithOptions({ - credential: new ApplicationDefaultCredential(), - }); projectIdApp = mocks.appWithOptions({ credential: mocks.credential, projectId: 'explicit-project-id', @@ -59,8 +75,21 @@ describe('Firestore', () => { }); afterEach(() => { - process.env.GOOGLE_APPLICATION_CREDENTIALS = appCredentials; - process.env.GCLOUD_PROJECT = gcloudProject; + if (appCredentials) { + process.env.GOOGLE_APPLICATION_CREDENTIALS = appCredentials; + } else { + delete process.env.GOOGLE_APPLICATION_CREDENTIALS; + } + if (googleCloudProject) { + process.env.GOOGLE_CLOUD_PROJECT = googleCloudProject; + } else { + delete process.env.GOOGLE_CLOUD_PROJECT; + } + if (gcloudProject) { + process.env.GCLOUD_PROJECT = gcloudProject; + } else { + delete process.env.GCLOUD_PROJECT; + } return mockApp.delete(); }); @@ -70,7 +99,8 @@ describe('Firestore', () => { it(`should throw given invalid app: ${ JSON.stringify(invalidApp) }`, () => { expect(() => { const firestoreAny: any = FirestoreService; - return new firestoreAny(invalidApp); + const firestoreService: FirestoreService = new firestoreAny(invalidApp); + return firestoreService.getDatabase(DEFAULT_DATABASE_ID) }).to.throw('First argument passed to admin.firestore() must be a valid Firebase app instance.'); }); }); @@ -78,39 +108,43 @@ describe('Firestore', () => { it('should throw given no app', () => { expect(() => { const firestoreAny: any = FirestoreService; - return new firestoreAny(); + const firestoreService: FirestoreService = new firestoreAny(); + return firestoreService.getDatabase(DEFAULT_DATABASE_ID) }).to.throw('First argument passed to admin.firestore() must be a valid Firebase app instance.'); }); it('should throw given an invalid credential with project ID', () => { // Project ID is read from the environment variable, but the credential is unsupported. - process.env.GCLOUD_PROJECT = 'project_id'; + process.env.GOOGLE_CLOUD_PROJECT = 'project_id'; expect(() => { - return new FirestoreService(mockCredentialApp); + return new FirestoreService(mockCredentialApp).getDatabase(DEFAULT_DATABASE_ID); }).to.throw(invalidCredError); }); it('should throw 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; expect(() => { - return new FirestoreService(mockCredentialApp); + return new FirestoreService(mockCredentialApp).getDatabase(DEFAULT_DATABASE_ID); }).to.throw(invalidCredError); }); it('should not throw given a valid app', () => { expect(() => { - return new FirestoreService(mockApp); + return new FirestoreService(mockApp).getDatabase(DEFAULT_DATABASE_ID); }).not.to.throw(); }); - it('should not throw given application default credentials without project ID', () => { - // Project ID not set in the environment. - delete process.env.GCLOUD_PROJECT; - process.env.GOOGLE_APPLICATION_CREDENTIALS = mockServiceAccount; - expect(() => { - return new FirestoreService(defaultCredentialApp); - }).not.to.throw(); + defaultCredentialApps.forEach((config) => { + it(`should not throw given default ${config.name} without project ID`, () => { + // Project ID not set in the environment. + delete process.env.GOOGLE_CLOUD_PROJECT; + delete process.env.GCLOUD_PROJECT; + expect(() => { + return new FirestoreService(config.app).getDatabase(DEFAULT_DATABASE_ID); + }).not.to.throw(); + }); }); }); @@ -127,32 +161,55 @@ describe('Firestore', () => { }); }); - describe('client', () => { - it('returns the client from the constructor', () => { - // We expect referential equality here - expect(firestore.client).to.not.be.null; + describe('options.projectId', () => { + it('should return a string when project ID is present in credential', () => { + const options = getFirestoreOptions(mockApp); + expect(options.projectId).to.equal('project_id'); }); - it('is read-only', () => { - expect(() => { - firestore.client = mockApp; - }).to.throw('Cannot set property client of # which has only a getter'); + it('should return a string when project ID is present in app options', () => { + const options = getFirestoreOptions(projectIdApp); + expect(options.projectId).to.equal('explicit-project-id'); + }); + + defaultCredentialApps.forEach((config) => { + it(`should return a string when GOOGLE_CLOUD_PROJECT is set with ${config.name}`, () => { + process.env.GOOGLE_CLOUD_PROJECT = 'env-project-id'; + const options = getFirestoreOptions(config.app); + expect(options.projectId).to.equal('env-project-id'); + }); + + it(`should return a string when GCLOUD_PROJECT is set with ${config.name}`, () => { + process.env.GCLOUD_PROJECT = 'env-project-id'; + const options = getFirestoreOptions(config.app); + expect(options.projectId).to.equal('env-project-id'); + }); }); }); - describe('client.projectId', () => { - it('should return a string when project ID is present in credential', () => { - expect(firestore.client.projectId).to.equal('project_id'); + describe('options.firebaseVersion', () => { + it('should return firebaseVersion when using credential with service account certificate', () => { + const options = getFirestoreOptions(mockApp); + expect(options.firebaseVersion).to.equal(firebaseVersion); }); - it('should return a string when project ID is present in app options', () => { - expect((new FirestoreService(projectIdApp).client as any).projectId).to.equal('explicit-project-id'); + defaultCredentialApps.forEach((config) => { + it(`should return firebaseVersion when using default ${config.name}`, () => { + const options = getFirestoreOptions(config.app); + expect(options.firebaseVersion).to.equal(firebaseVersion); + }); + }); + }); + + describe('options.preferRest', () => { + it('should not enable preferRest by default', () => { + const options = getFirestoreOptions(mockApp); + expect(options.preferRest).to.be.undefined; }); - it('should return a string when project ID is present in environment', () => { - process.env.GCLOUD_PROJECT = 'env-project-id'; - process.env.GOOGLE_APPLICATION_CREDENTIALS = mockServiceAccount; - expect((new FirestoreService(defaultCredentialApp).client as any).projectId).to.equal('env-project-id'); + it('should enable preferRest if provided', () => { + const options = getFirestoreOptions(mockApp, { preferRest: true }); + expect(options.preferRest).to.be.true; }); }); }); diff --git a/test/unit/firestore/index.spec.ts b/test/unit/firestore/index.spec.ts new file mode 100644 index 0000000000..11c5c24936 --- /dev/null +++ b/test/unit/firestore/index.spec.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. + */ + +'use strict'; + +import * as chai from 'chai'; +import * as sinonChai from 'sinon-chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +import * as mocks from '../../resources/mocks'; +import { App } from '../../../src/app/index'; +import { getFirestore, initializeFirestore, Firestore } from '../../../src/firestore/index'; +import { DEFAULT_DATABASE_ID } from '@google-cloud/firestore/build/src/path'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +describe('Firestore', () => { + let mockAppOne: App; + let mockAppTwo: App; + let mockCredentialApp: App; + + const noProjectIdError = 'Failed to initialize Google Cloud Firestore client with the ' + + 'available credentials. Must initialize the SDK with a certificate credential or ' + + 'application default credentials to use Cloud Firestore API.'; + + beforeEach(() => { + mockAppOne = mocks.app(); + mockAppTwo = mocks.app(); + mockCredentialApp = mocks.mockCredentialApp(); + }); + + describe('getFirestore()', () => { + it('should throw when default app is not available', () => { + expect(() => { + return getFirestore(); + }).to.throw('The default Firebase app does not exist.'); + }); + + it('should reject given an invalid credential without project ID', () => { + // Project ID not set in the environment. + delete process.env.GOOGLE_CLOUD_PROJECT; + delete process.env.GCLOUD_PROJECT; + expect(() => getFirestore(mockCredentialApp)).to.throw(noProjectIdError); + }); + + it('should not throw given a valid app', () => { + expect(() => { + return getFirestore(mockAppOne); + }).not.to.throw(); + }); + + it('should return the same instance for a given app instance', () => { + const db1: Firestore = getFirestore(mockAppOne); + const db2: Firestore = getFirestore(mockAppOne, DEFAULT_DATABASE_ID); + expect(db1).to.equal(db2); + }); + + it('should return the same instance for a given app instance and databaseId', () => { + const db1: Firestore = getFirestore(mockAppOne, 'db'); + const db2: Firestore = getFirestore(mockAppOne, 'db'); + expect(db1).to.equal(db2); + }); + + it('should return the different instance for given same app instance, but different databaseId', () => { + const db0: Firestore = getFirestore(mockAppOne, DEFAULT_DATABASE_ID); + const db1: Firestore = getFirestore(mockAppOne, 'db1'); + const db2: Firestore = getFirestore(mockAppOne, 'db2'); + expect(db0).to.not.equal(db1); + expect(db0).to.not.equal(db2); + expect(db1).to.not.equal(db2); + }); + }); + + describe('initializeFirestore()', () => { + 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; + expect(() => initializeFirestore(mockCredentialApp)).to.throw(noProjectIdError); + }); + + it('should not throw given a valid app', () => { + expect(() => { + return initializeFirestore(mockAppOne); + }).not.to.throw(); + }); + + it('should return the same instance for a given app instance', () => { + const db1: Firestore = initializeFirestore(mockAppOne); + const db2: Firestore = initializeFirestore(mockAppOne, {}, DEFAULT_DATABASE_ID); + + const db3: Firestore = initializeFirestore(mockAppTwo, { preferRest: true }); + const db4: Firestore = initializeFirestore(mockAppTwo, { preferRest: true }, DEFAULT_DATABASE_ID); + + expect(db1).to.equal(db2); + expect(db3).to.equal(db4); + }); + + it('should return the same instance for a given app instance and databaseId', () => { + const db1: Firestore = initializeFirestore(mockAppOne, {}, 'db'); + const db2: Firestore = initializeFirestore(mockAppOne, {}, 'db'); + + const db3: Firestore = initializeFirestore(mockAppTwo, { preferRest: true }, 'db'); + const db4: Firestore = initializeFirestore(mockAppTwo, { preferRest: true }, 'db'); + + expect(db1).to.equal(db2); + expect(db3).to.equal(db4); + }); + + it('should return a different instance for given same app instance, but different databaseId', () => { + const db0: Firestore = initializeFirestore(mockAppOne, {}, DEFAULT_DATABASE_ID); + const db1: Firestore = initializeFirestore(mockAppOne, {}, 'db1'); + const db2: Firestore = initializeFirestore(mockAppOne, {}, 'db2'); + + const db3: Firestore = initializeFirestore(mockAppTwo, { preferRest: true }, DEFAULT_DATABASE_ID); + const db4: Firestore = initializeFirestore(mockAppTwo, { preferRest: true }, 'db1'); + const db5: Firestore = initializeFirestore(mockAppTwo, { preferRest: true }, 'db2'); + + expect(db0).to.not.equal(db1); + expect(db0).to.not.equal(db2); + expect(db1).to.not.equal(db2); + + expect(db3).to.not.equal(db4); + expect(db3).to.not.equal(db5); + expect(db4).to.not.equal(db5); + }); + + it('getFirestore should return the same instance as initializeFirestore returned earlier', () => { + const db1: Firestore = initializeFirestore(mockAppOne, {}, 'db'); + const db2: Firestore = getFirestore(mockAppOne, 'db'); + + const db3: Firestore = initializeFirestore(mockAppTwo, { preferRest: true }); + const db4: Firestore = getFirestore(mockAppTwo); + + expect(db1).to.equal(db2); + expect(db3).to.equal(db4); + }); + + it('initializeFirestore should not allow create an instance with different settings', () => { + initializeFirestore(mockAppTwo, {}, 'db'); + expect(() => { + return initializeFirestore(mockAppTwo, { preferRest: true }, 'db'); + }).to.throw(/has already been called with different options/); + }); + }); +}); diff --git a/test/unit/functions/functions-api-client-internal.spec.ts b/test/unit/functions/functions-api-client-internal.spec.ts new file mode 100644 index 0000000000..138280f287 --- /dev/null +++ b/test/unit/functions/functions-api-client-internal.spec.ts @@ -0,0 +1,524 @@ +/*! + * @license + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +import * as _ from 'lodash'; +import * as chai from 'chai'; +import * as sinon from 'sinon'; +import * as utils from '../utils'; +import * as mocks from '../../resources/mocks'; +import { getSdkVersion } from '../../../src/utils'; + +import { FirebaseApp } from '../../../src/app/firebase-app'; +import { FirebaseFunctionsError, FunctionsApiClient, Task } from '../../../src/functions/functions-api-client-internal'; +import { HttpClient } from '../../../src/utils/api-request'; +import { FirebaseAppError } from '../../../src/utils/error'; +import { deepCopy } from '../../../src/utils/deep-copy'; + +const expect = chai.expect; + +describe('FunctionsApiClient', () => { + + const ERROR_RESPONSE = { + error: { + code: 404, + message: 'Requested entity not found', + status: 'NOT_FOUND', + }, + }; + + const EXPECTED_HEADERS = { + 'X-Firebase-Client': `fire-admin-node/${getSdkVersion()}`, + 'Authorization': 'Bearer mock-token' + }; + + const noProjectId = 'Failed to determine project ID. Initialize the SDK with service ' + + 'account credentials or set project ID as an app option. Alternatively, set the ' + + 'GOOGLE_CLOUD_PROJECT environment variable.'; + + const DEFAULT_REGION = 'us-central1'; + const CUSTOM_REGION = 'us-west1'; + const FUNCTION_NAME = 'function-name'; + const CUSTOM_PROJECT_ID = 'taskq-project'; + const EXTENSION_ID = 'image-resize'; + const PARTIAL_RESOURCE_NAME = `locations/${CUSTOM_REGION}/functions/${FUNCTION_NAME}`; + const FULL_RESOURCE_NAME = `projects/${CUSTOM_PROJECT_ID}/locations/${CUSTOM_REGION}/functions/${FUNCTION_NAME}`; + + const mockOptions = { + credential: new mocks.MockCredential(), + projectId: 'test-project', + serviceAccountId: 'service-acct@email.com' + }; + + const mockExtensionOptions = { + credential: new mocks.MockComputeEngineCredential(), + projectId: 'test-project', + serviceAccountId: 'service-acct@email.com' + }; + + const TEST_TASK_PAYLOAD: Task = { + httpRequest: { + url: `https://${DEFAULT_REGION}-${mockOptions.projectId}.cloudfunctions.net/${FUNCTION_NAME}`, + oidcToken: { + serviceAccountEmail: mockOptions.serviceAccountId, + }, + body: Buffer.from(JSON.stringify({ data: {} })).toString('base64'), + headers: { 'Content-Type' : 'application/json' } + } + } + + const CLOUD_TASKS_URL = `https://cloudtasks.googleapis.com/v2/projects/${mockOptions.projectId}/locations/${DEFAULT_REGION}/queues/${FUNCTION_NAME}/tasks`; + + const CLOUD_TASKS_URL_EXT = `https://cloudtasks.googleapis.com/v2/projects/${mockOptions.projectId}/locations/${DEFAULT_REGION}/queues/ext-${EXTENSION_ID}-${FUNCTION_NAME}/tasks`; + + const CLOUD_TASKS_URL_FULL_RESOURCE = `https://cloudtasks.googleapis.com/v2/projects/${CUSTOM_PROJECT_ID}/locations/${CUSTOM_REGION}/queues/${FUNCTION_NAME}/tasks`; + + const CLOUD_TASKS_URL_PARTIAL_RESOURCE = `https://cloudtasks.googleapis.com/v2/projects/${mockOptions.projectId}/locations/${CUSTOM_REGION}/queues/${FUNCTION_NAME}/tasks`; + + const clientWithoutProjectId = new FunctionsApiClient(mocks.mockCredentialApp()); + + // Stubs used to simulate underlying api calls. + let stubs: sinon.SinonStub[] = []; + let app: FirebaseApp; + let apiClient: FunctionsApiClient; + + beforeEach(() => { + app = mocks.appWithOptions(mockOptions); + apiClient = new FunctionsApiClient(app); + }); + + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + return app.delete(); + }); + + describe('Constructor', () => { + it('should reject when the app is null', () => { + expect(() => new FunctionsApiClient(null as unknown as FirebaseApp)) + .to.throw('First argument passed to getFunctions() must be a valid Firebase app instance.'); + }); + }); + + describe('enqueue', () => { + let clock: sinon.SinonFakeTimers | undefined; + + afterEach(() => { + if (clock) { + clock.restore(); + clock = undefined; + } + }); + + it('should reject when project id is not available', () => { + return clientWithoutProjectId.enqueue({}, FUNCTION_NAME) + .should.eventually.be.rejectedWith(noProjectId); + }); + + it('should reject when project id is not available in partial resource name', () => { + return clientWithoutProjectId.enqueue({}, PARTIAL_RESOURCE_NAME) + .should.eventually.be.rejectedWith(noProjectId); + }); + + for (const invalidName of [null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1 }, _.noop, undefined]) { + it(`should throw if functionName is ${invalidName}`, () => { + expect(apiClient.enqueue({}, invalidName as any)) + .to.eventually.throw('Function name must be a non empty string'); + }); + } + + for (const invalidName of ['project/abc/locations/east/fname', 'location/west/', '//']) { + it(`should throw if functionName is ${invalidName}`, () => { + expect(apiClient.enqueue({}, invalidName as any)) + .to.eventually.throw('Function name must be a single string or a qualified resource name'); + }); + } + + for (const invalidOption of [null, 'abc', '', [], true, 102, 1.2]) { + it(`should throw if options is ${invalidOption}`, () => { + expect(apiClient.enqueue({}, FUNCTION_NAME, '', invalidOption as any)) + .to.eventually.throw('TaskOptions must be a non-null object'); + }); + } + + for (const invalidScheduleTime of [null, '', 'abc', 102, 1.2, [], {}, true, NaN]) { + it(`should throw if scheduleTime is ${invalidScheduleTime}`, () => { + expect(apiClient.enqueue({}, FUNCTION_NAME, '', { scheduleTime: invalidScheduleTime } as any)) + .to.eventually.throw('scheduleTime must be a valid Date object.'); + }); + } + + for (const invalidScheduleDelaySeconds of [null, 'abc', '', [], {}, true, NaN, -1]) { + it(`should throw if scheduleDelaySeconds is ${invalidScheduleDelaySeconds}`, () => { + expect(apiClient.enqueue({}, FUNCTION_NAME, '', + { scheduleDelaySeconds: invalidScheduleDelaySeconds } as any)) + .to.eventually.throw('scheduleDelaySeconds must be a non-negative duration in seconds.'); + }); + } + + for (const invalidDispatchDeadlineSeconds of [null, 'abc', '', [], {}, true, NaN, -1, 14, 1801]) { + it(`should throw if dispatchDeadlineSeconds is ${invalidDispatchDeadlineSeconds}`, () => { + expect(apiClient.enqueue({}, FUNCTION_NAME, '', + { dispatchDeadlineSeconds: invalidDispatchDeadlineSeconds } as any)) + .to.eventually.throw('dispatchDeadlineSeconds must be a non-negative duration in seconds ' + + 'and must be in the range of 15s to 30 mins.'); + }); + } + + for (const invalidUri of [null, '', 'a', 'foo', 'image.jpg', [], {}, true, NaN]) { + it(`should throw given an invalid uri: ${invalidUri}`, () => { + expect(apiClient.enqueue({}, FUNCTION_NAME, '', + { uri: invalidUri } as any)) + .to.eventually.throw('uri must be a valid URL string.'); + }); + } + + for (const invalidTaskId of [1234, 'task!', 'id:0', '[1234]', '(1234)']) { + it(`should throw given an invalid task ID: ${invalidTaskId}`, () => { + expect(apiClient.enqueue({}, FUNCTION_NAME, '', + { id: invalidTaskId } as any )) + .to.eventually.throw('id can contain only letters ([A-Za-z]), numbers ([0-9]), ' + + 'hyphens (-), or underscores (_). The maximum length is 500 characters.') + }); + } + + it('should throw when both scheduleTime and scheduleDelaySeconds are provided', () => { + expect(apiClient.enqueue({}, FUNCTION_NAME, '', { + scheduleTime: new Date(), + scheduleDelaySeconds: 1000 + } as any)) + .to.eventually.throw('Both scheduleTime and scheduleDelaySeconds are provided. Only one value should be set.'); + }); + + it('should reject when a full platform error response is received', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .rejects(utils.errorFrom(ERROR_RESPONSE, 404)); + stubs.push(stub); + const expected = new FirebaseFunctionsError('not-found', 'Requested entity not found'); + return apiClient.enqueue({}, FUNCTION_NAME) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should reject with unknown-error when error code is not present', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .rejects(utils.errorFrom({}, 404)); + stubs.push(stub); + const expected = new FirebaseFunctionsError('unknown-error', 'Unknown server error: {}'); + return apiClient.enqueue({}, FUNCTION_NAME) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should reject with unknown-error for non-json response', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .rejects(utils.errorFrom('not json', 404)); + stubs.push(stub); + const expected = new FirebaseFunctionsError( + 'unknown-error', 'Unexpected response with status: 404 and body: not json'); + return apiClient.enqueue({}, FUNCTION_NAME) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should reject when rejected with a FirebaseAppError', () => { + const expected = new FirebaseAppError('network-error', 'socket hang up'); + const stub = sinon + .stub(HttpClient.prototype, 'send') + .rejects(expected); + stubs.push(stub); + return apiClient.enqueue({}, FUNCTION_NAME) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should reject when a task with the same ID exists', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .rejects(utils.errorFrom({}, 409)); + stubs.push(stub); + expect(apiClient.enqueue({}, FUNCTION_NAME, undefined, { id: 'mock-task' })).to.eventually.throw( + new FirebaseFunctionsError( + 'task-already-exists', + 'A task with ID mock-task already exists' + ) + ) + }); + + it('should resolve on success', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({}, 200)); + stubs.push(stub); + return apiClient.enqueue({}, FUNCTION_NAME) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: CLOUD_TASKS_URL, + headers: EXPECTED_HEADERS, + data: { + task: TEST_TASK_PAYLOAD + } + }); + }); + }); + + it('should resolve the projectId and location from the full resource name', () => { + const expectedPayload = deepCopy(TEST_TASK_PAYLOAD); + expectedPayload.httpRequest.url = + `https://${CUSTOM_REGION}-${CUSTOM_PROJECT_ID}.cloudfunctions.net/${FUNCTION_NAME}`; + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({}, 200)); + stubs.push(stub); + return apiClient.enqueue({}, FULL_RESOURCE_NAME) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: CLOUD_TASKS_URL_FULL_RESOURCE, + headers: EXPECTED_HEADERS, + data: { + task: expectedPayload + } + }); + }); + }); + + it('should resolve the location from the partial resource name', () => { + const expectedPayload = deepCopy(TEST_TASK_PAYLOAD); + expectedPayload.httpRequest.url = + `https://${CUSTOM_REGION}-${mockOptions.projectId}.cloudfunctions.net/${FUNCTION_NAME}`; + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({}, 200)); + stubs.push(stub); + return apiClient.enqueue({}, PARTIAL_RESOURCE_NAME) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: CLOUD_TASKS_URL_PARTIAL_RESOURCE, + headers: EXPECTED_HEADERS, + data: { + task: expectedPayload + } + }); + }); + }); + + it('should update the function name and set headers when the extension-id is provided', () => { + app = mocks.appWithOptions(mockExtensionOptions); + apiClient = new FunctionsApiClient(app); + + const expectedPayload = deepCopy(TEST_TASK_PAYLOAD); + expectedPayload.httpRequest.url = + `https://${DEFAULT_REGION}-${mockOptions.projectId}.cloudfunctions.net/ext-${EXTENSION_ID}-${FUNCTION_NAME}`; + expectedPayload.httpRequest.headers['Authorization'] = 'Bearer mockIdToken'; + delete expectedPayload.httpRequest.oidcToken; + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({}, 200)); + stubs.push(stub); + return apiClient.enqueue({}, FUNCTION_NAME, EXTENSION_ID) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: CLOUD_TASKS_URL_EXT, + headers: EXPECTED_HEADERS, + data: { + task: expectedPayload + } + }); + }); + }); + + it('should use the default projectId following a request with a full resource name', () => { + const expectedPayload = deepCopy(TEST_TASK_PAYLOAD); + expectedPayload.httpRequest.url = + `https://${CUSTOM_REGION}-${CUSTOM_PROJECT_ID}.cloudfunctions.net/${FUNCTION_NAME}`; + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({}, 200)); + stubs.push(stub); + // pass the full resource name. SDK should not use the default values + return apiClient.enqueue({}, FULL_RESOURCE_NAME) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: CLOUD_TASKS_URL_FULL_RESOURCE, + headers: EXPECTED_HEADERS, + data: { + task: expectedPayload + } + }); + + // passing just the function name. SDK should deffer to default values + return apiClient.enqueue({}, FUNCTION_NAME); + }) + .then(() => { + expect(stub).to.have.been.calledTwice.and.calledWith({ + method: 'POST', + url: CLOUD_TASKS_URL, + headers: EXPECTED_HEADERS, + data: { + task: TEST_TASK_PAYLOAD + } + }); + }); + }); + + + + // tests for Task Options + it('should convert scheduleTime to ISO string', () => { + const scheduleTime = new Date(); + const expectedPayload = deepCopy(TEST_TASK_PAYLOAD); + // timestamps should be converted to ISO strings + (expectedPayload as any).scheduleTime = scheduleTime.toISOString(); + + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({}, 200)); + stubs.push(stub); + return apiClient.enqueue({}, FUNCTION_NAME, '', { scheduleTime }) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: CLOUD_TASKS_URL, + headers: EXPECTED_HEADERS, + data: { + task: expectedPayload, + } + }); + }); + }); + + it('should set scheduleTime based on scheduleDelaySeconds', () => { + clock = sinon.useFakeTimers(1000); + + const scheduleDelaySeconds = 1800; + const scheduleTime = new Date(); // '1970-01-01T00:00:01.000Z' + scheduleTime.setSeconds(scheduleTime.getSeconds() + scheduleDelaySeconds); // '1970-01-01T00:30:01.000Z' + const expectedPayload = deepCopy(TEST_TASK_PAYLOAD); + // timestamps should be converted to ISO strings + (expectedPayload as any).scheduleTime = scheduleTime.toISOString(); + + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({}, 200)); + stubs.push(stub); + return apiClient.enqueue({}, FUNCTION_NAME, '', { scheduleDelaySeconds }) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: CLOUD_TASKS_URL, + headers: EXPECTED_HEADERS, + data: { + task: expectedPayload, + } + }); + }); + }); + + it('should convert dispatchDeadline to a duration with `s` prefix', () => { + const dispatchDeadlineSeconds = 1800; + const expectedPayload = deepCopy(TEST_TASK_PAYLOAD); + (expectedPayload as any).dispatchDeadline = `${dispatchDeadlineSeconds}s`; + + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({}, 200)); + stubs.push(stub); + return apiClient.enqueue({}, FUNCTION_NAME, '', { dispatchDeadlineSeconds }) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: CLOUD_TASKS_URL, + headers: EXPECTED_HEADERS, + data: { + task: expectedPayload, + } + }); + }); + }); + + it('should encode data in the payload', () => { + const data = { privateKey: '~/.ssh/id_rsa.pub' }; + const expectedPayload = deepCopy(TEST_TASK_PAYLOAD); + expectedPayload.httpRequest.body = Buffer.from(JSON.stringify({ data })).toString('base64'); + + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({}, 200)); + stubs.push(stub); + return apiClient.enqueue(data, FUNCTION_NAME) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: CLOUD_TASKS_URL, + headers: EXPECTED_HEADERS, + data: { + task: expectedPayload, + } + }); + }); + }); + }); + + describe('delete', () => { + for (const invalidTaskId of [1234, 'task!', 'id:0', '[1234]', '(1234)']) { + it(`should throw given an invalid task ID: ${invalidTaskId}`, () => { + expect(apiClient.delete(invalidTaskId as any, FUNCTION_NAME)) + .to.eventually.throw('id can contain only letters ([A-Za-z]), numbers ([0-9]), ' + + 'hyphens (-), or underscores (_). The maximum length is 500 characters.') + }); + } + + it('should reject when no valid function name is specified', () => { + expect(apiClient.delete('mock-task', '/projects/abc/locations/def')) + .to.eventually.throw('No valid function name specified to enqueue tasks for.'); + }); + + it('should resolve on success', async () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({}, 200)); + stubs.push(stub); + await apiClient.delete('mock-task', FUNCTION_NAME); + expect(stub).to.have.been.calledWith({ + method: 'DELETE', + url: CLOUD_TASKS_URL.concat('/', 'mock-task'), + headers: EXPECTED_HEADERS, + }); + }); + + it('should ignore deletes if no task with task ID exists', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .rejects(utils.errorFrom({}, 404)); + stubs.push(stub); + expect(apiClient.delete('nonexistent-task', FUNCTION_NAME)).to.eventually.not.throw(utils.errorFrom({}, 404)); + }); + + it('should throw on non-404 HTTP errors', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .rejects(utils.errorFrom({}, 500)); + stubs.push(stub); + expect(apiClient.delete('mock-task', FUNCTION_NAME)).to.eventually.throw(utils.errorFrom({}, 500)); + }); + }) +}); diff --git a/test/unit/functions/functions.spec.ts b/test/unit/functions/functions.spec.ts new file mode 100644 index 0000000000..36e6098d42 --- /dev/null +++ b/test/unit/functions/functions.spec.ts @@ -0,0 +1,187 @@ +/*! + * @license + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +import * as _ from 'lodash'; +import * as chai from 'chai'; +import * as sinon from 'sinon'; +import * as mocks from '../../resources/mocks'; + +import { FirebaseApp } from '../../../src/app/firebase-app'; +import { FunctionsApiClient, FirebaseFunctionsError } from '../../../src/functions/functions-api-client-internal'; +import { HttpClient } from '../../../src/utils/api-request'; +import { Functions, TaskQueue } from '../../../src/functions/functions'; + +const expect = chai.expect; + +describe('Functions', () => { + const mockOptions = { + credential: new mocks.MockCredential(), + projectId: 'test-project', + }; + + let functions: Functions; + + let mockApp: FirebaseApp; + let mockCredentialApp: FirebaseApp; + + // Stubs used to simulate underlying api calls. + let stubs: sinon.SinonStub[] = []; + + before(() => { + mockApp = mocks.appWithOptions(mockOptions); + mockCredentialApp = mocks.mockCredentialApp(); + functions = new Functions(mockApp); + }); + + after(() => { + return mockApp.delete(); + }); + + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + describe('Constructor', () => { + for (const invalidApp of [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop, + undefined]) { + it('should throw given invalid app: ' + JSON.stringify(invalidApp), () => { + expect(() => { + const functionsAny: any = Functions; + return new functionsAny(invalidApp); + }).to.throw( + 'First argument passed to getFunctions() must be a valid Firebase app ' + + 'instance.'); + }); + } + + it('should reject when initialized without project ID', () => { + // Remove Project ID from the environment. + delete process.env.GOOGLE_CLOUD_PROJECT; + delete process.env.GCLOUD_PROJECT; + const noProjectId = 'Failed to determine project ID. Initialize the SDK with service ' + + 'account credentials or set project ID as an app option. Alternatively, set the ' + + 'GOOGLE_CLOUD_PROJECT environment variable.'; + const functionsWithoutProjectId = new Functions(mockCredentialApp); + return functionsWithoutProjectId.taskQueue('task-name').enqueue({}) + .should.eventually.rejectedWith(noProjectId); + }); + + it('should reject when failed to contact the Metadata server for service account email', () => { + const functionsWithProjectId = new Functions(mockApp); + const stub = sinon.stub(HttpClient.prototype, 'send') + .rejects(new Error('network error.')); + stubs.push(stub); + const expected = 'Failed to determine service account. Initialize the ' + + 'SDK with service account credentials or set service account ID as an app option.'; + return functionsWithProjectId.taskQueue('task-name').enqueue({}) + .should.eventually.be.rejectedWith(expected); + }); + + it('should not throw given a valid app', () => { + expect(() => { + return new Functions(mockApp); + }).not.to.throw(); + }); + }); + + describe('app', () => { + it('returns the app from the constructor', () => { + // We expect referential equality here + expect(functions.app).to.equal(mockApp); + }); + }); +}); + +describe('TaskQueue', () => { + const INTERNAL_ERROR = new FirebaseFunctionsError('internal-error', 'message'); + const FUNCTION_NAME = 'function-name'; + + let taskQueue: TaskQueue; + let mockClient: FunctionsApiClient; + + let mockApp: FirebaseApp; + + // Stubs used to simulate underlying api calls. + let stubs: sinon.SinonStub[] = []; + + before(() => { + mockApp = mocks.app(); + mockClient = new FunctionsApiClient(mockApp); + taskQueue = new TaskQueue(FUNCTION_NAME, mockClient); + }); + + after(() => { + return mockApp.delete(); + }); + + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + describe('Constructor', () => { + for (const invalidClient of [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop, + undefined]) { + it('should throw given invalid client: ' + JSON.stringify(invalidClient), () => { + expect(() => { + const taskQueueAny: any = TaskQueue; + return new taskQueueAny(FUNCTION_NAME, invalidClient); + }).to.throw( + 'Must provide a valid FunctionsApiClient instance to create a new TaskQueue.'); + }); + } + + for (const invalidFunctionName of [null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1 }, _.noop, + undefined]) { + it('should throw given invalid name: ' + JSON.stringify(invalidFunctionName), () => { + expect(() => { + const taskQueueAny: any = TaskQueue; + return new taskQueueAny(invalidFunctionName, mockClient); + }).to.throw('`functionName` must be a non-empty string.'); + }); + } + + it('should not throw given a valid name and client', () => { + expect(() => { + return new TaskQueue(FUNCTION_NAME, mockClient); + }).not.to.throw(); + }); + }); + + describe('enqueue', () => { + it('should propagate API errors', () => { + const stub = sinon + .stub(FunctionsApiClient.prototype, 'enqueue') + .rejects(INTERNAL_ERROR); + stubs.push(stub); + return taskQueue.enqueue({}) + .should.eventually.be.rejected.and.deep.equal(INTERNAL_ERROR); + }); + + it('should propagate API errors with task options', () => { + const stub = sinon + .stub(FunctionsApiClient.prototype, 'enqueue') + .rejects(INTERNAL_ERROR); + stubs.push(stub); + return taskQueue.enqueue({}, { scheduleDelaySeconds: 3600 }) + .should.eventually.be.rejected.and.deep.equal(INTERNAL_ERROR); + }); + }); +}); diff --git a/test/unit/functions/index.spec.ts b/test/unit/functions/index.spec.ts new file mode 100644 index 0000000000..8bfcaa5dfe --- /dev/null +++ b/test/unit/functions/index.spec.ts @@ -0,0 +1,76 @@ +/*! + * @license + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +import * as chai from 'chai'; +import * as sinonChai from 'sinon-chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +import * as mocks from '../../resources/mocks'; +import { App } from '../../../src/app/index'; +import { getFunctions, Functions } from '../../../src/functions/index'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +describe('Functions', () => { + let mockApp: App; + let mockCredentialApp: App; + + const noProjectIdError = 'Failed to determine project ID. Initialize the SDK ' + + 'with service account credentials or set project ID as an app option. Alternatively, set the ' + + 'GOOGLE_CLOUD_PROJECT environment variable.'; + + beforeEach(() => { + mockApp = mocks.app(); + mockCredentialApp = mocks.mockCredentialApp(); + }); + + describe('getFunctions()', () => { + it('should throw when default app is not available', () => { + expect(() => { + return getFunctions(); + }).to.throw('The default Firebase app does not exist.'); + }); + + it('should reject given an invalid credential without project ID', () => { + // Project ID not set in the environment. + delete process.env.GOOGLE_CLOUD_PROJECT; + delete process.env.GCLOUD_PROJECT; + const functions = getFunctions(mockCredentialApp); + const factorizedQueue = functions.taskQueue('task-name'); + return factorizedQueue.enqueue({}) + .should.eventually.rejectedWith(noProjectIdError); + }); + + it('should not throw given a valid app', () => { + expect(() => { + return getFunctions(mockApp); + }).not.to.throw(); + }); + + it('should return the same instance for a given app instance', () => { + const fn1: Functions = getFunctions(mockApp); + const fn2: Functions = getFunctions(mockApp); + expect(fn1).to.equal(fn2); + }); + }); +}); diff --git a/test/unit/index.spec.ts b/test/unit/index.spec.ts index 5307702df4..31efeaf979 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"); @@ -16,34 +17,104 @@ // General import './firebase.spec'; -import './firebase-app.spec'; -import './firebase-namespace.spec'; +import './app/credential-internal.spec'; +import './app/index.spec'; +import './app/firebase-app.spec'; +import './app/firebase-namespace.spec'; // Utilities import './utils/index.spec'; 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'; -import './auth/credential.spec'; +import './auth/index.spec'; import './auth/user-record.spec'; import './auth/token-generator.spec'; +import './auth/token-verifier.spec'; import './auth/auth-api-request.spec'; +import './auth/user-import-builder.spec'; +import './auth/action-code-settings-builder.spec'; +import './auth/auth-config.spec'; +import './auth/tenant.spec'; +import './auth/tenant-manager.spec'; // Database import './database/database.spec'; +import './database/index.spec'; // Messaging +import './messaging/index.spec'; import './messaging/messaging.spec'; +import './messaging/batch-requests.spec'; + +// Machine Learning +import './machine-learning/index.spec'; +import './machine-learning/machine-learning.spec'; +import './machine-learning/machine-learning-api-client.spec'; // Storage import './storage/storage.spec'; +import './storage/index.spec'; // Firestore import './firestore/firestore.spec'; +import './firestore/index.spec'; + +// Installations +import './installations/installations.spec'; +import './installations/installations-request-handler.spec'; + +// Installations +import './installations/installations.spec'; +import './installations/installations-request-handler.spec'; + +// Installations +import './installations/installations.spec'; +import './installations/installations-request-handler.spec'; // InstanceId +import './instance-id/index.spec'; import './instance-id/instance-id.spec'; -import './instance-id/instance-id-request.spec'; + +// ProjectManagement +import './project-management/index.spec'; +import './project-management/project-management.spec'; +import './project-management/project-management-api-request.spec'; +import './project-management/android-app.spec'; +import './project-management/ios-app.spec'; + +// SecurityRules +import './security-rules/index.spec'; +import './security-rules/security-rules.spec'; +import './security-rules/security-rules-api-client.spec'; + +// RemoteConfig +import './remote-config/index.spec'; +import './remote-config/remote-config.spec'; +import './remote-config/remote-config-api-client.spec'; +import './remote-config/condition-evaluator.spec'; +import './remote-config/internal/value-impl.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'; + +// Eventarc +import './eventarc/eventarc.spec'; +import './eventarc/eventarc-utils.spec'; + +// Functions +import './functions/index.spec'; +import './functions/functions.spec'; +import './functions/functions-api-client-internal.spec'; + +// Extensions +import './extensions/extensions.spec'; +import './extensions/extensions-api-client-internal.spec'; diff --git a/test/unit/installations/installations-request-handler.spec.ts b/test/unit/installations/installations-request-handler.spec.ts new file mode 100644 index 0000000000..36e696dd2d --- /dev/null +++ b/test/unit/installations/installations-request-handler.spec.ts @@ -0,0 +1,149 @@ +/*! + * @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 { FirebaseApp } from '../../../src/app/firebase-app'; +import { HttpClient } from '../../../src/utils/api-request'; +import { FirebaseInstallationsRequestHandler } from '../../../src/installations/installations-request-handler'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +describe('FirebaseInstallationsRequestHandler', () => { + const projectId = 'project_id'; + const mockAccessToken: string = utils.generateRandomAccessToken(); + let stubs: sinon.SinonStub[] = []; + let getTokenStub: sinon.SinonStub; + let mockApp: FirebaseApp; + let expectedHeaders: object; + + before(() => { + getTokenStub = utils.stubGetAccessToken(mockAccessToken); + }); + + after(() => { + stubs = []; + getTokenStub.restore(); + }); + + beforeEach(() => { + mockApp = mocks.app(); + expectedHeaders = { + Authorization: 'Bearer ' + mockAccessToken, + }; + return mockApp.INTERNAL.getToken(); + }); + + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + return mockApp.delete(); + }); + + describe('Constructor', () => { + it('should succeed with a FirebaseApp instance', () => { + expect(() => { + return new FirebaseInstallationsRequestHandler(mockApp); + }).not.to.throw(Error); + }); + }); + + describe('deleteInstallation', () => { + const httpMethod = 'DELETE'; + const host = 'console.firebase.google.com'; + const path = `/v1/project/${projectId}/instanceId/test-fid`; + const timeout = 10000; + + 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 FirebaseInstallationsRequestHandler(mockApp); + return requestHandler.deleteInstallation('test-fid') + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: httpMethod, + url: `https://${host}${path}`, + headers: expectedHeaders, + timeout, + }); + }); + }); + + it('should throw for HTTP 404 errors', () => { + const stub = sinon.stub(HttpClient.prototype, 'send') + .rejects(utils.errorFrom({}, 404)); + stubs.push(stub); + + const requestHandler = new FirebaseInstallationsRequestHandler(mockApp); + return requestHandler.deleteInstallation('test-fid') + .then(() => { + throw new Error('Unexpected success'); + }) + .catch((error) => { + expect(error.code).to.equal('installations/api-error'); + expect(error.message).to.equal('Installation ID "test-fid": Failed to find the installation ID.'); + }); + }); + + it('should throw for HTTP 409 errors', () => { + const stub = sinon.stub(HttpClient.prototype, 'send') + .rejects(utils.errorFrom({}, 409)); + stubs.push(stub); + + const requestHandler = new FirebaseInstallationsRequestHandler(mockApp); + return requestHandler.deleteInstallation('test-fid') + .then(() => { + throw new Error('Unexpected success'); + }) + .catch((error) => { + expect(error.code).to.equal('installations/api-error'); + expect(error.message).to.equal('Installation ID "test-fid": Already deleted.'); + }); + }); + + it('should throw for unexpected HTTP errors', () => { + const expectedResult = { error: 'test error' }; + const stub = sinon.stub(HttpClient.prototype, 'send') + .rejects(utils.errorFrom(expectedResult, 511)); + stubs.push(stub); + + const requestHandler = new FirebaseInstallationsRequestHandler(mockApp); + return requestHandler.deleteInstallation('test-fid') + .then(() => { + throw new Error('Unexpected success'); + }) + .catch((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..6a38c8413c --- /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/app/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/index.spec.ts b/test/unit/instance-id/index.spec.ts new file mode 100644 index 0000000000..2f1d690e6a --- /dev/null +++ b/test/unit/instance-id/index.spec.ts @@ -0,0 +1,75 @@ +/*! + * @license + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +import * as chai from 'chai'; +import * as sinonChai from 'sinon-chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +import * as mocks from '../../resources/mocks'; +import { App } from '../../../src/app/index'; +import { getInstanceId, InstanceId } from '../../../src/instance-id/index'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +describe('InstanceId', () => { + let mockApp: App; + let mockCredentialApp: App; + + 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(); + mockCredentialApp = mocks.mockCredentialApp(); + }); + + describe('getInstanceId()', () => { + it('should throw when default app is not available', () => { + expect(() => { + return getInstanceId(); + }).to.throw('The default Firebase app does not exist.'); + }); + + it('should reject given an invalid credential without project ID', () => { + // Project ID not set in the environment. + delete process.env.GOOGLE_CLOUD_PROJECT; + delete process.env.GCLOUD_PROJECT; + const iid = getInstanceId(mockCredentialApp); + return iid.deleteInstanceId('iid') + .should.eventually.rejectedWith(noProjectIdError); + }); + + it('should not throw given a valid app', () => { + expect(() => { + return getInstanceId(mockApp); + }).not.to.throw(); + }); + + it('should return the same instance for a given app instance', () => { + const iid1: InstanceId = getInstanceId(mockApp); + const iid2: InstanceId = getInstanceId(mockApp); + expect(iid1).to.equal(iid2); + }); + }); +}); diff --git a/test/unit/instance-id/instance-id-request.spec.ts b/test/unit/instance-id/instance-id-request.spec.ts deleted file mode 100644 index db866ec8d2..0000000000 --- a/test/unit/instance-id/instance-id-request.spec.ts +++ /dev/null @@ -1,152 +0,0 @@ -/*! - * 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 nock from 'nock'; -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 {FirebaseApp} from '../../../src/firebase-app'; -import {HttpRequestHandler} from '../../../src/utils/api-request'; -import {FirebaseInstanceIdRequestHandler} from '../../../src/instance-id/instance-id-request'; - -chai.should(); -chai.use(sinonChai); -chai.use(chaiAsPromised); - -const expect = chai.expect; - -describe('FirebaseInstanceIdRequestHandler', () => { - const projectId: string = 'test-project-id'; - const mockedRequests: nock.Scope[] = []; - const mockAccessToken: string = utils.generateRandomAccessToken(); - let stubs: sinon.SinonStub[] = []; - let mockApp: FirebaseApp; - let expectedHeaders: object; - - before(() => utils.mockFetchAccessTokenRequests(mockAccessToken)); - - after(() => { - stubs = []; - nock.cleanAll(); - }); - - beforeEach(() => { - mockApp = mocks.app(); - expectedHeaders = { - Authorization: 'Bearer ' + mockAccessToken, - }; - }); - - afterEach(() => { - _.forEach(stubs, (stub) => stub.restore()); - _.forEach(mockedRequests, (mockedRequest) => mockedRequest.done()); - return mockApp.delete(); - }); - - describe('Constructor', () => { - it('should succeed with a FirebaseApp instance', () => { - expect(() => { - return new FirebaseInstanceIdRequestHandler(mockApp, projectId); - }).not.to.throw(Error); - }); - }); - - describe('deleteInstanceId', () => { - const httpMethod = 'DELETE'; - const host = 'console.firebase.google.com'; - const port = 443; - const path = `/v1/project/${projectId}/instanceId/test-iid`; - const timeout = 10000; - - it('should be fulfilled given a valid instance ID', () => { - const expectedResult = {}; - - const stub = sinon.stub(HttpRequestHandler.prototype, 'sendRequest') - .returns(Promise.resolve(expectedResult)); - stubs.push(stub); - - const requestHandler = new FirebaseInstanceIdRequestHandler(mockApp, projectId); - return requestHandler.deleteInstanceId('test-iid') - .then((result) => { - expect(result).to.deep.equal(expectedResult); - expect(stub).to.have.been.calledOnce.and.calledWith( - host, port, path, httpMethod, undefined, expectedHeaders, timeout); - }); - }); - - it('should throw for HTTP 404 errors', () => { - const expectedResult = {statusCode: 404}; - - const stub = sinon.stub(HttpRequestHandler.prototype, 'sendRequest') - .rejects(expectedResult); - stubs.push(stub); - - const requestHandler = new FirebaseInstanceIdRequestHandler(mockApp, projectId); - return requestHandler.deleteInstanceId('test-iid') - .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.'); - }); - }); - - it('should throw for HTTP 409 errors', () => { - const expectedResult = {statusCode: 409}; - - const stub = sinon.stub(HttpRequestHandler.prototype, 'sendRequest') - .rejects(expectedResult); - stubs.push(stub); - - const requestHandler = new FirebaseInstanceIdRequestHandler(mockApp, projectId); - return requestHandler.deleteInstanceId('test-iid') - .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.'); - }); - }); - - it('should throw for unexpected HTTP errors', () => { - const expectedResult = {statusCode: 511}; - - const stub = sinon.stub(HttpRequestHandler.prototype, 'sendRequest') - .rejects(expectedResult); - stubs.push(stub); - - const requestHandler = new FirebaseInstanceIdRequestHandler(mockApp, projectId); - return requestHandler.deleteInstanceId('test-iid') - .then(() => { - throw new Error('Unexpected success'); - }) - .catch((error) => { - expect(error.code).to.equal('instance-id/api-error'); - expect(error.message).to.equal(JSON.stringify(expectedResult)); - }); - }); - }); -}); diff --git a/test/unit/instance-id/instance-id.spec.ts b/test/unit/instance-id/instance-id.spec.ts index ea94bb7080..a610d7678d 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"); @@ -18,7 +19,6 @@ 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'; @@ -26,12 +26,13 @@ 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 * as validator from '../../../src/utils/validator'; +import { InstanceId } from '../../../src/instance-id/index'; +import { Installations } from '../../../src/installations/index'; +import { FirebaseApp } from '../../../src/app/firebase-app'; +import { + FirebaseInstanceIdError, InstanceIdClientErrorCode, + FirebaseInstallationsError, InstallationsClientErrorCode, +} from '../../../src/utils/error'; chai.should(); chai.use(sinonChai); @@ -43,26 +44,26 @@ describe('InstanceId', () => { let iid: InstanceId; let mockApp: FirebaseApp; let mockCredentialApp: FirebaseApp; + let getTokenStub: sinon.SinonStub; let nullAccessTokenClient: InstanceId; let malformedAccessTokenClient: InstanceId; let rejectedPromiseAccessTokenClient: InstanceId; - let gcloudProject: string; + 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 ' - + 'GCLOUD_PROJECT environment variable.'; - - before(() => utils.mockFetchAccessTokenRequests()); - - after(() => nock.cleanAll()); + + 'GOOGLE_CLOUD_PROJECT environment variable.'; beforeEach(() => { mockApp = mocks.app(); + getTokenStub = utils.stubGetAccessToken(undefined, mockApp); mockCredentialApp = mocks.mockCredentialApp(); iid = new InstanceId(mockApp); + googleCloudProject = process.env.GOOGLE_CLOUD_PROJECT; gcloudProject = process.env.GCLOUD_PROJECT; nullAccessTokenClient = new InstanceId(mocks.appReturningNullAccessToken()); @@ -71,6 +72,8 @@ describe('InstanceId', () => { }); afterEach(() => { + getTokenStub.restore(); + process.env.GOOGLE_CLOUD_PROJECT = googleCloudProject; process.env.GCLOUD_PROJECT = gcloudProject; return mockApp.delete(); }); @@ -83,7 +86,7 @@ describe('InstanceId', () => { expect(() => { const iidAny: any = InstanceId; return new iidAny(invalidApp); - }).to.throw('First argument passed to admin.instanceId() must be a valid Firebase app instance.'); + }).to.throw('First argument passed to instanceId() must be a valid Firebase app instance.'); }); }); @@ -91,15 +94,16 @@ describe('InstanceId', () => { expect(() => { const iidAny: any = InstanceId; return new iidAny(); - }).to.throw('First argument passed to admin.instanceId() must be a valid Firebase app instance.'); + }).to.throw('First argument passed to instanceId() must be a valid Firebase app instance.'); }); - it('should throw given an invalid credential without project ID', () => { + 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; - expect(() => { - return new InstanceId(mockCredentialApp); - }).to.throw(noProjectIdError); + const instanceId = new InstanceId(mockCredentialApp); + return instanceId.deleteInstanceId('iid') + .should.eventually.rejectedWith(noProjectIdError); }); it('should not throw given a valid app', () => { @@ -126,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(() => { @@ -160,29 +163,32 @@ describe('InstanceId', () => { }); it('should resolve without errors on success', () => { - const stub = sinon.stub(FirebaseInstanceIdRequestHandler.prototype, 'deleteInstanceId') - .returns(Promise.resolve(null)); + const stub = sinon.stub(Installations.prototype, 'deleteInstallation') + .resolves(); stubs.push(stub); return iid.deleteInstanceId(testInstanceId) - .then((result) => { + .then(() => { // Confirm underlying API called with expected parameters. expect(stub).to.have.been.calledOnce.and.calledWith(testInstanceId); }); }); - 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((result) => { + .then(() => { throw new Error('Unexpected success'); }, (error) => { // 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); }); }); }); diff --git a/test/unit/machine-learning/index.spec.ts b/test/unit/machine-learning/index.spec.ts new file mode 100644 index 0000000000..1937f5387d --- /dev/null +++ b/test/unit/machine-learning/index.spec.ts @@ -0,0 +1,75 @@ +/*! + * @license + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +import * as chai from 'chai'; +import * as sinonChai from 'sinon-chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +import * as mocks from '../../resources/mocks'; +import { App } from '../../../src/app/index'; +import { getMachineLearning, MachineLearning } from '../../../src/machine-learning/index'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +describe('MachineLearning', () => { + let mockApp: App; + let mockCredentialApp: App; + + const noProjectIdError = 'Failed to determine project ID. Initialize the SDK ' + + 'with service account credentials, or set project ID as an app option. Alternatively, set the ' + + 'GOOGLE_CLOUD_PROJECT environment variable.'; + + beforeEach(() => { + mockApp = mocks.app(); + mockCredentialApp = mocks.mockCredentialApp(); + }); + + describe('getMachineLearning()', () => { + it('should throw when default app is not available', () => { + expect(() => { + return getMachineLearning(); + }).to.throw('The default Firebase app does not exist.'); + }); + + it('should reject given an invalid credential without project ID', () => { + // Project ID not set in the environment. + delete process.env.GOOGLE_CLOUD_PROJECT; + delete process.env.GCLOUD_PROJECT; + const client = getMachineLearning(mockCredentialApp); + return client.getModel('test') + .should.eventually.rejectedWith(noProjectIdError); + }); + + it('should not throw given a valid app', () => { + expect(() => { + return getMachineLearning(mockApp); + }).not.to.throw(); + }); + + it('should return the same instance for a given app instance', () => { + const client1: MachineLearning = getMachineLearning(mockApp); + const client2: MachineLearning = getMachineLearning(mockApp); + expect(client1).to.equal(client2); + }); + }); +}); diff --git a/test/unit/machine-learning/machine-learning-api-client.spec.ts b/test/unit/machine-learning/machine-learning-api-client.spec.ts new file mode 100644 index 0000000000..9fbb94066e --- /dev/null +++ b/test/unit/machine-learning/machine-learning-api-client.spec.ts @@ -0,0 +1,824 @@ +/*! + * 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 strict'; + +import * as _ from 'lodash'; +import * as chai from 'chai'; +import * as sinon from 'sinon'; +import { FirebaseMachineLearningError } from '../../../src/machine-learning/machine-learning-utils'; +import { HttpClient } from '../../../src/utils/api-request'; +import * as utils from '../utils'; +import * as mocks from '../../resources/mocks'; +import { FirebaseAppError } from '../../../src/utils/error'; +import { FirebaseApp } from '../../../src/app/firebase-app'; +import { getSdkVersion } from '../../../src/utils/index'; +import { MachineLearningApiClient } from '../../../src/machine-learning/machine-learning-api-client'; +import { ListModelsOptions, ModelOptions } from '../../../src/machine-learning/index'; + +const expect = chai.expect; + +describe('MachineLearningApiClient', () => { + + const BASE_URL = 'https://firebaseml.googleapis.com/v1beta2'; + + const MODEL_ID = '1234567'; + const 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'], + state: { published: true }, + tfliteModel: { + gcsTfliteUri: 'gs://test-project-bucket/Firebase/ML/Models/model1.tflite', + sizeBytes: 16900988, + }, + }; + const MODEL_RESPONSE2 = { + name: 'projects/test-project/models/2345678', + createTime: '2020-02-07T23:45:22.288047Z', + updateTime: '2020-02-08T23:45:22.288047Z', + etag: 'etag234', + modelHash: 'modelHash234', + displayName: 'model_2', + tags: ['tag_2', 'tag_3'], + state: { published: true }, + tfliteModel: { + gcsTfliteUri: 'gs://test-project-bucket/Firebase/ML/Models/model2.tflite', + sizeBytes: 2220022, + }, + }; + + 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: STATUS_ERROR_MESSAGE, + }; + const OPERATION_SUCCESS_RESPONSE = { + done: true, + response: MODEL_RESPONSE, + }; + const OPERATION_ERROR_RESPONSE = { + 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 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: { + 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 mockOptions = { + credential: new mocks.MockCredential(), + projectId: 'test-project', + }; + + const clientWithoutProjectId = new MachineLearningApiClient( + mocks.mockCredentialApp()); + + // Stubs used to simulate underlying api calls. + let stubs: sinon.SinonStub[] = []; + let app: FirebaseApp; + let apiClient: MachineLearningApiClient; + + beforeEach(() => { + app = mocks.appWithOptions(mockOptions); + apiClient = new MachineLearningApiClient(app); + }); + + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + return app.delete(); + }); + + describe('Constructor', () => { + it('should throw when the app is null', () => { + expect(() => new MachineLearningApiClient(null as unknown as FirebaseApp)) + .to.throw('First argument passed to admin.machineLearning() must be a valid Firebase app'); + }); + }); + + describe('createModel', () => { + const NAME_ONLY_OPTIONS: ModelOptions = { displayName: 'name1' }; + const GCS_OPTIONS: ModelOptions = { + displayName: 'name2', + tfliteModel: { + gcsTfliteUri: 'gcsUri1', + }, + }; + + const invalidContent: any[] = [null, undefined, {}, { tags: [] }]; + invalidContent.forEach((content) => { + it(`should reject when called with: ${JSON.stringify(content)}`, () => { + return apiClient.createModel(content) + .should.eventually.be.rejected.and.have.property( + 'message', 'Invalid model content.'); + }); + }); + + it('should reject when project id is not available', () => { + return clientWithoutProjectId.createModel(NAME_ONLY_OPTIONS) + .should.eventually.be.rejectedWith(noProjectId); + }); + + it('should throw when an 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.createModel(NAME_ONLY_OPTIONS) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should resolve with the created resource on success', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(OPERATION_SUCCESS_RESPONSE)); + stubs.push(stub); + 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; + expect(resp.response).to.deep.equal(MODEL_RESPONSE); + }); + }); + + 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_OPTIONS) + .then((resp) => { + expect(resp.done).to.be.true; + expect(resp.name).to.be.undefined; + expect(resp.error).to.deep.equal(STATUS_ERROR_RESPONSE); + }); + }); + + 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.createModel(NAME_ONLY_OPTIONS) + .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.createModel(NAME_ONLY_OPTIONS) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should reject with 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.createModel(NAME_ONLY_OPTIONS) + .should.eventually.be.rejected.and.deep.include(expected); + }); + }); + + describe('updateModel', () => { + const NAME_ONLY_OPTIONS: ModelOptions = { displayName: 'name1' }; + const GCS_OPTIONS: ModelOptions = { + displayName: 'name2', + tfliteModel: { + gcsTfliteUri: 'gcsUri1', + }, + }; + + const NAME_ONLY_MASK_LIST = ['displayName']; + const GCS_MASK_LIST = ['displayName', 'tfliteModel.gcsTfliteUri']; + + const NAME_ONLY_UPDATE_MASK_STRING = 'updateMask=displayName'; + const GCS_UPDATE_MASK_STRING = 'updateMask=displayName,tfliteModel.gcsTfliteUri'; + + 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.'); + }); + }); + + it('should reject when called with empty mask', () => { + return apiClient.updateModel(MODEL_ID, {}, []) + .should.eventually.be.rejected.and.have.property( + 'message', 'Invalid model or mask content.'); + }); + + it('should reject when project id is not available', () => { + return clientWithoutProjectId.updateModel(MODEL_ID, NAME_ONLY_OPTIONS, NAME_ONLY_MASK_LIST) + .should.eventually.be.rejectedWith(noProjectId); + }); + + it('should throw when an 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.updateModel(MODEL_ID, NAME_ONLY_OPTIONS, NAME_ONLY_MASK_LIST) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should resolve with the updated resource on success', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(OPERATION_SUCCESS_RESPONSE)); + stubs.push(stub); + 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; + 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}?${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 error when the operation fails', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(OPERATION_ERROR_RESPONSE)); + stubs.push(stub); + 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; + expect(resp.error).to.deep.equal(STATUS_ERROR_RESPONSE); + }); + }); + + 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.updateModel(MODEL_ID, NAME_ONLY_OPTIONS, NAME_ONLY_MASK_LIST) + .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.updateModel(MODEL_ID, NAME_ONLY_OPTIONS, NAME_ONLY_MASK_LIST) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should reject with 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.updateModel(MODEL_ID, NAME_ONLY_OPTIONS, NAME_ONLY_MASK_LIST) + .should.eventually.be.rejected.and.deep.include(expected); + }); + }); + + describe('getModel', () => { + const INVALID_NAMES: any[] = [null, undefined, '', 1, true, {}, []]; + INVALID_NAMES.forEach((invalidName) => { + it(`should reject when called with: ${JSON.stringify(invalidName)}`, () => { + return apiClient.getModel(invalidName) + .should.eventually.be.rejected.and.have.property( + 'message', 'Model ID must be a non-empty string.'); + }); + }); + + 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', () => { + return clientWithoutProjectId.getModel(MODEL_ID) + .should.eventually.be.rejectedWith(noProjectId); + }); + + it('should resolve with the requested model on success', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({ name: 'bar' })); + stubs.push(stub); + return apiClient.getModel(MODEL_ID) + .then((resp) => { + expect(resp.name).to.equal('bar'); + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'GET', + url: `${BASE_URL}/projects/test-project/models/1234567`, + 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.getModel(MODEL_ID) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should reject 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.getModel(MODEL_ID) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should reject 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.getModel(MODEL_ID) + .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.getModel(MODEL_ID) + .should.eventually.be.rejected.and.deep.include(expected); + }); + }); + + 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], + nextPageToken: 'next', + }; + + 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 }) + .should.eventually.be.rejected.and.have.property( + 'message', 'Invalid list filter.'); + }); + }); + + 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 }) + .should.eventually.be.rejected.and.have.property( + 'message', 'Invalid page size.'); + }); + }); + + const outOfRangePageSizes: number[] = [-1, 0, 101]; + outOfRangePageSizes.forEach((invalidPageSize) => { + it(`should reject when called with invalid page size: ${invalidPageSize}`, () => { + return apiClient.listModels({ pageSize: invalidPageSize }) + .should.eventually.be.rejected.and.have.property( + 'message', 'Page size must be between 1 and 100.'); + }); + }); + + 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 }) + .should.eventually.be.rejected.and.have.property( + 'message', 'Next page token must be a non-empty string.'); + }); + }); + + it('should resolve on success when called without any arguments', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(LIST_RESPONSE)); + stubs.push(stub); + return apiClient.listModels() + .then((resp) => { + expect(resp).to.deep.equal(LIST_RESPONSE); + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'GET', + url: `${BASE_URL}/projects/test-project/models`, + headers: EXPECTED_HEADERS, + data: {}, + }); + }); + }); + + const validOptions: ListModelsOptions[] = [ + { pageSize: 5 }, + { pageToken: 'next' }, + { filter: 'displayName=name1' }, + { + filter: 'displayName=name1', + pageSize: 5, + pageToken: 'next', + }, + ]; + validOptions.forEach((options) => { + it(`should resolve on success when called with options: ${JSON.stringify(options)}`, () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(LIST_RESPONSE)); + stubs.push(stub); + return apiClient.listModels(options) + .then((resp) => { + expect(resp.models).not.to.be.empty; + expect(resp.models!.length).to.equal(2); + expect(resp.models![0]).to.deep.equal(MODEL_RESPONSE); + expect(resp.models![1]).to.deep.equal(MODEL_RESPONSE2); + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'GET', + url: `${BASE_URL}/projects/test-project/models`, + headers: EXPECTED_HEADERS, + data: options, + }); + }); + }); + }); + + it('should throw 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.listModels() + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should throw 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.listModels() + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should throw 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.listModels() + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should throw 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.listModels() + .should.eventually.be.rejected.and.deep.include(expected); + }); + }); + + describe('deleteModel', () => { + const INVALID_NAMES: any[] = [null, undefined, '', 1, true, {}, []]; + INVALID_NAMES.forEach((invalidName) => { + it(`should reject when called with: ${JSON.stringify(invalidName)}`, () => { + return apiClient.deleteModel(invalidName) + .should.eventually.be.rejected.and.have.property( + 'message', 'Model ID must be a non-empty string.'); + }); + }); + + 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', () => { + return clientWithoutProjectId.deleteModel(MODEL_ID) + .should.eventually.be.rejectedWith(noProjectId); + }); + + it('should resolve on success', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + stubs.push(stub); + return apiClient.deleteModel(MODEL_ID) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'DELETE', + url: `${BASE_URL}/projects/test-project/models/1234567`, + 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.deleteModel(MODEL_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 FirebaseMachineLearningError('unknown-error', 'Unknown server error: {}'); + return apiClient.deleteModel(MODEL_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 FirebaseMachineLearningError( + 'unknown-error', 'Unexpected response with status: 404 and body: not json'); + return apiClient.deleteModel(MODEL_ID) + .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.deleteModel(MODEL_ID) + .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 new file mode 100644 index 0000000000..58e424484d --- /dev/null +++ b/test/unit/machine-learning/machine-learning.spec.ts @@ -0,0 +1,992 @@ +/*! + * 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 strict'; + +import * as _ from 'lodash'; +import * as chai from 'chai'; +import * as sinon from 'sinon'; +import { FirebaseApp } from '../../../src/app/firebase-app'; +import * as mocks from '../../resources/mocks'; +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, Model, ModelOptions } from '../../../src/machine-learning/index'; + +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'; + const MODEL_RESPONSE: { + name: string; + createTime: string; + updateTime: string; + etag: string; + modelHash: string; + displayName?: string; + tags?: string[]; + 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'], + state: { published: true }, + tfliteModel: { + gcsTfliteUri: 'gs://test-project-bucket/Firebase/ML/Models/model1.tflite', + sizeBytes: 16900988, + }, + }; + + + const MODEL_RESPONSE2: { + name: string; + createTime: string; + updateTime: string; + etag: string; + modelHash: string; + displayName?: string; + tags?: string[]; + state?: { + validationError?: { + code: number; + message: string; + }; + published?: boolean; + }; + tfliteModel?: { + gcsTfliteUri: string; + sizeBytes: number; + }; + } = { + name: 'projects/test-project/models/2345678', + createTime: '2020-02-07T23:45:22.288047Z', + updateTime: '2020-02-08T23:45:22.288047Z', + etag: 'etag234', + modelHash: 'modelHash234', + displayName: 'model_2', + tags: ['tag_2', 'tag_3'], + state: { published: false }, + tfliteModel: { + gcsTfliteUri: 'gs://test-project-bucket/Firebase/ML/Models/model2.tflite', + sizeBytes: 22200222, + }, + }; + + 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; + } = { + code: 3, + message: 'Invalid Argument message', + }; + + const OPERATION_RESPONSE: { + name?: string; + metadata?: any; + done: boolean; + error?: StatusErrorResponse; + response?: { + name: string; + createTime: string; + updateTime: string; + etag: string; + modelHash: string; + displayName?: string; + tags?: string[]; + state?: { + validationError?: { + code: number; + message: string; + }; + published?: boolean; + }; + tfliteModel?: { + gcsTfliteUri: string; + sizeBytes: number; + }; + }; + } = { + done: true, + response: MODEL_RESPONSE, + }; + + const OPERATION_RESPONSE_ERROR: { + name?: string; + metadata?: any; + done: boolean; + error?: { + code: number; + message: string; + }; + response?: ModelResponse; + } = { + done: true, + 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(() => { + return mockApp.delete(); + }); + + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + }); + + 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 machineLearningAny: any = MachineLearning; + return new machineLearningAny(invalidApp); + }).to.throw( + 'First argument passed to admin.machineLearning() must be a valid Firebase app ' + + 'instance.'); + }); + }); + + it('should throw given no app', () => { + expect(() => { + const machineLearningAny: any = MachineLearning; + return new machineLearningAny(); + }).to.throw( + 'First argument passed to admin.machineLearning() must be a valid Firebase app ' + + 'instance.'); + }); + + it('should throw given invalid credential', () => { + const expectedError = 'Failed to initialize Google Cloud Storage client with ' + + 'the available credential. Must initialize the SDK with a certificate credential ' + + 'or application default credentials to use Cloud Storage API.'; + expect(() => { + const machineLearningAny: any = MachineLearning; + return new machineLearningAny(mockCredentialApp).createModel({ + displayName: 'foo', + tfliteModel: { + gcsTfliteUri: 'gs://some-bucket/model.tflite', + } }); + }).to.throw(expectedError); + }); + + it('should not throw given a valid app', () => { + expect(() => { + return new MachineLearning(mockApp); + }).not.to.throw(); + }); + }); + + describe('app', () => { + it('returns the app from the constructor', () => { + // We expect referential equality here + expect(machineLearning.app).to.equal(mockApp); + }); + }); + + describe('Model', () => { + it('should successfully construct a model', () => { + 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']); + 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('etag123'); + expect(model.modelHash).to.equal('modelHash123'); + + const tflite = model.tfliteModel!; + expect(tflite.gcsTfliteUri).to.be.equal( + '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, mockClient); + 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; + }); + + 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); + }); + }); + }); + + + + describe('getModel', () => { + it('should propagate API errors', () => { + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'getModel') + .rejects(EXPECTED_ERROR); + stubs.push(stub); + return machineLearning.getModel(MODEL_ID) + .should.eventually.be.rejected.and.deep.equal(EXPECTED_ERROR); + }); + + it('should reject when API response is invalid', () => { + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'getModel') + .resolves(null as any); + stubs.push(stub); + return machineLearning.getModel(MODEL_ID) + .should.eventually.be.rejected.and.have.property( + 'message', 'Invalid Model response: null'); + }); + + it('should reject when API response does not contain a name', () => { + const response = deepCopy(MODEL_RESPONSE); + response.name = ''; + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'getModel') + .resolves(response); + stubs.push(stub); + return machineLearning.getModel(MODEL_ID) + .should.eventually.be.rejected.and.have.property( + 'message', `Invalid Model response: ${JSON.stringify(response)}`); + }); + + it('should reject when API response does not contain a createTime', () => { + const response = deepCopy(MODEL_RESPONSE); + response.createTime = ''; + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'getModel') + .resolves(response); + stubs.push(stub); + return machineLearning.getModel(MODEL_ID) + .should.eventually.be.rejected.and.have.property( + 'message', `Invalid Model response: ${JSON.stringify(response)}`); + }); + + it('should reject when API response does not contain a updateTime', () => { + const response = deepCopy(MODEL_RESPONSE); + response.updateTime = ''; + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'getModel') + .resolves(response); + stubs.push(stub); + return machineLearning.getModel(MODEL_ID) + .should.eventually.be.rejected.and.have.property( + 'message', `Invalid Model response: ${JSON.stringify(response)}`); + }); + + it('should reject when API response does not contain a displayName', () => { + const response = deepCopy(MODEL_RESPONSE); + response.displayName = ''; + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'getModel') + .resolves(response); + stubs.push(stub); + return machineLearning.getModel(MODEL_ID) + .should.eventually.be.rejected.and.have.property( + 'message', `Invalid Model response: ${JSON.stringify(response)}`); + }); + + it('should reject when API response does not contain an etag', () => { + const response = deepCopy(MODEL_RESPONSE); + response.etag = ''; + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'getModel') + .resolves(response); + stubs.push(stub); + return machineLearning.getModel(MODEL_ID) + .should.eventually.be.rejected.and.have.property( + 'message', `Invalid Model response: ${JSON.stringify(response)}`); + }); + + it('should resolve with Model on success', () => { + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'getModel') + .resolves(MODEL_RESPONSE); + stubs.push(stub); + + return machineLearning.getModel(MODEL_ID) + .then((model) => { + expect(model).to.deep.equal(model1); + }); + }); + }); + + describe('listModels', () => { + + const LIST_MODELS_RESPONSE = { + models: [ + MODEL_RESPONSE, + MODEL_RESPONSE2, + ], + nextPageToken: 'next', + }; + + it('should propagate API errors', () => { + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'listModels') + .rejects(EXPECTED_ERROR); + stubs.push(stub); + return machineLearning.listModels({}) + .should.eventually.be.rejected.and.deep.equal(EXPECTED_ERROR); + }); + + it('should reject when API response is invalid', () => { + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'listModels') + .resolves(null as any); + stubs.push(stub); + return machineLearning.listModels() + .should.eventually.be.rejected.and.have.property( + 'message', 'Invalid ListModels response: null'); + }); + + it('should resolve with Models on success', () => { + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'listModels') + .resolves(LIST_MODELS_RESPONSE); + stubs.push(stub); + 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.pageToken).to.equal(LIST_MODELS_RESPONSE.nextPageToken); + }); + }); + }); + + describe('deleteModel', () => { + it('should propagate API errors', () => { + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'deleteModel') + .rejects(EXPECTED_ERROR); + stubs.push(stub); + return machineLearning.deleteModel(MODEL_ID) + .should.eventually.be.rejected.and.deep.equal(EXPECTED_ERROR); + }); + + it('should resolve on success', () => { + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'deleteModel') + .resolves(); + stubs.push(stub); + + return machineLearning.deleteModel(MODEL_ID); + }); + }); + + describe('createModel', () => { + const GCS_TFLITE_URI = 'gs://test-bucket/Firebase/ML/Models/model1.tflite'; + const MODEL_OPTIONS_NO_GCS: ModelOptions = { + displayName: 'display_name', + tags: ['tag1', 'tag2'], + }; + const MODEL_OPTIONS_WITH_GCS: ModelOptions = { + displayName: 'display_name_2', + tags: ['tag3', 'tag4'], + tfliteModel: { + gcsTfliteUri: GCS_TFLITE_URI, + }, + }; + + it('should propagate API errors', () => { + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'createModel') + .rejects(EXPECTED_ERROR); + stubs.push(stub); + return machineLearning.createModel(MODEL_OPTIONS_NO_GCS) + .should.eventually.be.rejected.and.deep.equal(EXPECTED_ERROR); + }); + + it('should reject when API response is invalid', () => { + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'createModel') + .resolves(null as any); + stubs.push(stub); + return machineLearning.createModel(MODEL_OPTIONS_WITH_GCS) + .should.eventually.be.rejected; + }); + + it('should reject when API response does not contain a name', () => { + const op = deepCopy(OPERATION_RESPONSE); + op.response!.name = ''; + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'createModel') + .resolves(op); + stubs.push(stub); + return machineLearning.createModel(MODEL_OPTIONS_NO_GCS) + .should.eventually.be.rejected.and.have.property( + 'message', `Invalid Model response: ${JSON.stringify(op.response)}`); + }); + + it('should reject when API response does not contain a createTime', () => { + const op = deepCopy(OPERATION_RESPONSE); + op.response!.createTime = ''; + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'createModel') + .resolves(op); + stubs.push(stub); + return machineLearning.createModel(MODEL_OPTIONS_NO_GCS) + .should.eventually.be.rejected.and.have.property( + 'message', `Invalid Model response: ${JSON.stringify(op.response)}`); + }); + + it('should reject when API response does not contain a updateTime', () => { + const op = deepCopy(OPERATION_RESPONSE); + op.response!.updateTime = ''; + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'createModel') + .resolves(op); + stubs.push(stub); + return machineLearning.createModel(MODEL_OPTIONS_NO_GCS) + .should.eventually.be.rejected.and.have.property( + 'message', `Invalid Model response: ${JSON.stringify(op.response)}`); + }); + + it('should reject when API response does not contain a displayName', () => { + const op = deepCopy(OPERATION_RESPONSE); + op.response!.displayName = ''; + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'createModel') + .resolves(op); + stubs.push(stub); + return machineLearning.createModel(MODEL_OPTIONS_NO_GCS) + .should.eventually.be.rejected.and.have.property( + 'message', `Invalid Model response: ${JSON.stringify(op.response)}`); + }); + + it('should reject when API response does not contain an etag', () => { + const op = deepCopy(OPERATION_RESPONSE); + op.response!.etag = ''; + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'createModel') + .resolves(op); + stubs.push(stub); + return machineLearning.createModel(MODEL_OPTIONS_NO_GCS) + .should.eventually.be.rejected.and.have.property( + 'message', `Invalid Model response: ${JSON.stringify(op.response)}`); + }); + + it('should resolve with Model on success', () => { + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'createModel') + .resolves(OPERATION_RESPONSE); + stubs.push(stub); + + return machineLearning.createModel(MODEL_OPTIONS_WITH_GCS) + .then((model) => { + expect(model).to.deep.equal(model1); + }); + }); + + it('should resolve with Error on operation error', () => { + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'createModel') + .resolves(OPERATION_RESPONSE_ERROR); + stubs.push(stub); + + return machineLearning.createModel(MODEL_OPTIONS_WITH_GCS) + .should.eventually.be.rejected.and.have.property( + 'message', 'Invalid Argument message'); + }); + }); + + describe('updateModel', () => { + const GCS_TFLITE_URI = 'gs://test-bucket/Firebase/ML/Models/model1.tflite'; + const MODEL_OPTIONS_NO_GCS: ModelOptions = { + displayName: 'display_name', + tags: ['tag1', 'tag2'], + }; + const MODEL_OPTIONS_WITH_GCS: ModelOptions = { + displayName: 'display_name_2', + tags: ['tag3', 'tag4'], + tfliteModel: { + gcsTfliteUri: GCS_TFLITE_URI, + }, + }; + + it('should propagate API errors', () => { + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'updateModel') + .rejects(EXPECTED_ERROR); + stubs.push(stub); + return machineLearning.updateModel(MODEL_ID, MODEL_OPTIONS_NO_GCS) + .should.eventually.be.rejected.and.deep.equal(EXPECTED_ERROR); + }); + + it('should reject when API response is invalid', () => { + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'updateModel') + .resolves(null as any); + stubs.push(stub); + return machineLearning.updateModel(MODEL_ID, MODEL_OPTIONS_WITH_GCS) + .should.eventually.be.rejected; + }); + + it('should reject when API response does not contain a name', () => { + const op = deepCopy(OPERATION_RESPONSE); + op.response!.name = ''; + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'updateModel') + .resolves(op); + stubs.push(stub); + return machineLearning.updateModel(MODEL_ID, MODEL_OPTIONS_NO_GCS) + .should.eventually.be.rejected.and.have.property( + 'message', `Invalid Model response: ${JSON.stringify(op.response)}`); + }); + + it('should reject when API response does not contain a createTime', () => { + const op = deepCopy(OPERATION_RESPONSE); + op.response!.createTime = ''; + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'updateModel') + .resolves(op); + stubs.push(stub); + return machineLearning.updateModel(MODEL_ID, MODEL_OPTIONS_NO_GCS) + .should.eventually.be.rejected.and.have.property( + 'message', `Invalid Model response: ${JSON.stringify(op.response)}`); + }); + + it('should reject when API response does not contain a updateTime', () => { + const op = deepCopy(OPERATION_RESPONSE); + op.response!.updateTime = ''; + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'updateModel') + .resolves(op); + stubs.push(stub); + return machineLearning.updateModel(MODEL_ID, MODEL_OPTIONS_NO_GCS) + .should.eventually.be.rejected.and.have.property( + 'message', `Invalid Model response: ${JSON.stringify(op.response)}`); + }); + + it('should reject when API response does not contain a displayName', () => { + const op = deepCopy(OPERATION_RESPONSE); + op.response!.displayName = ''; + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'updateModel') + .resolves(op); + stubs.push(stub); + return machineLearning.updateModel(MODEL_ID, MODEL_OPTIONS_NO_GCS) + .should.eventually.be.rejected.and.have.property( + 'message', `Invalid Model response: ${JSON.stringify(op.response)}`); + }); + + it('should reject when API response does not contain an etag', () => { + const op = deepCopy(OPERATION_RESPONSE); + op.response!.etag = ''; + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'updateModel') + .resolves(op); + stubs.push(stub); + return machineLearning.updateModel(MODEL_ID, MODEL_OPTIONS_NO_GCS) + .should.eventually.be.rejected.and.have.property( + 'message', `Invalid Model response: ${JSON.stringify(op.response)}`); + }); + + it('should resolve with Model on success', () => { + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'updateModel') + .resolves(OPERATION_RESPONSE); + stubs.push(stub); + + return machineLearning.updateModel(MODEL_ID, MODEL_OPTIONS_WITH_GCS) + .then((model) => { + expect(model).to.deep.equal(model1); + }); + }); + + it('should resolve with Error on operation error', () => { + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'updateModel') + .resolves(OPERATION_RESPONSE_ERROR); + stubs.push(stub); + + return machineLearning.updateModel(MODEL_ID, MODEL_OPTIONS_WITH_GCS) + .should.eventually.be.rejected.and.have.property( + 'message', 'Invalid Argument message'); + }); + }); + + describe('publishModel', () => { + it('should propagate API errors', () => { + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'updateModel') + .rejects(EXPECTED_ERROR); + stubs.push(stub); + return machineLearning.publishModel(MODEL_ID) + .should.eventually.be.rejected.and.deep.equal(EXPECTED_ERROR); + }); + + it('should reject when API response is invalid', () => { + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'updateModel') + .resolves(null as any); + stubs.push(stub); + return machineLearning.publishModel(MODEL_ID) + .should.eventually.be.rejected; + }); + + it('should reject when API response does not contain a name', () => { + const op = deepCopy(OPERATION_RESPONSE); + op.response!.name = ''; + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'updateModel') + .resolves(op); + stubs.push(stub); + return machineLearning.publishModel(MODEL_ID) + .should.eventually.be.rejected.and.have.property( + 'message', `Invalid Model response: ${JSON.stringify(op.response)}`); + }); + + it('should reject when API response does not contain a createTime', () => { + const op = deepCopy(OPERATION_RESPONSE); + op.response!.createTime = ''; + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'updateModel') + .resolves(op); + stubs.push(stub); + return machineLearning.publishModel(MODEL_ID) + .should.eventually.be.rejected.and.have.property( + 'message', `Invalid Model response: ${JSON.stringify(op.response)}`); + }); + + it('should reject when API response does not contain a updateTime', () => { + const op = deepCopy(OPERATION_RESPONSE); + op.response!.updateTime = ''; + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'updateModel') + .resolves(op); + stubs.push(stub); + return machineLearning.publishModel(MODEL_ID) + .should.eventually.be.rejected.and.have.property( + 'message', `Invalid Model response: ${JSON.stringify(op.response)}`); + }); + + it('should reject when API response does not contain a displayName', () => { + const op = deepCopy(OPERATION_RESPONSE); + op.response!.displayName = ''; + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'updateModel') + .resolves(op); + stubs.push(stub); + return machineLearning.publishModel(MODEL_ID) + .should.eventually.be.rejected.and.have.property( + 'message', `Invalid Model response: ${JSON.stringify(op.response)}`); + }); + + it('should reject when API response does not contain an etag', () => { + const op = deepCopy(OPERATION_RESPONSE); + op.response!.etag = ''; + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'updateModel') + .resolves(op); + stubs.push(stub); + return machineLearning.publishModel(MODEL_ID) + .should.eventually.be.rejected.and.have.property( + 'message', `Invalid Model response: ${JSON.stringify(op.response)}`); + }); + + it('should resolve with Model on success', () => { + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'updateModel') + .resolves(OPERATION_RESPONSE); + stubs.push(stub); + + return machineLearning.publishModel(MODEL_ID) + .then((model) => { + expect(model).to.deep.equal(model1); + }); + }); + + it('should resolve with Error on operation error', () => { + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'updateModel') + .resolves(OPERATION_RESPONSE_ERROR); + stubs.push(stub); + + return machineLearning.publishModel(MODEL_ID) + .should.eventually.be.rejected.and.have.property( + 'message', 'Invalid Argument message'); + }); + }); + + describe('unpublishModel', () => { + it('should propagate API errors', () => { + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'updateModel') + .rejects(EXPECTED_ERROR); + stubs.push(stub); + return machineLearning.unpublishModel(MODEL_ID) + .should.eventually.be.rejected.and.deep.equal(EXPECTED_ERROR); + }); + + it('should reject when API response is invalid', () => { + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'updateModel') + .resolves(null as any); + stubs.push(stub); + return machineLearning.unpublishModel(MODEL_ID) + .should.eventually.be.rejected; + }); + + it('should reject when API response does not contain a name', () => { + const op = deepCopy(OPERATION_RESPONSE); + op.response!.name = ''; + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'updateModel') + .resolves(op); + stubs.push(stub); + return machineLearning.unpublishModel(MODEL_ID) + .should.eventually.be.rejected.and.have.property( + 'message', `Invalid Model response: ${JSON.stringify(op.response)}`); + }); + + it('should reject when API response does not contain a createTime', () => { + const op = deepCopy(OPERATION_RESPONSE); + op.response!.createTime = ''; + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'updateModel') + .resolves(op); + stubs.push(stub); + return machineLearning.unpublishModel(MODEL_ID) + .should.eventually.be.rejected.and.have.property( + 'message', `Invalid Model response: ${JSON.stringify(op.response)}`); + }); + + it('should reject when API response does not contain a updateTime', () => { + const op = deepCopy(OPERATION_RESPONSE); + op.response!.updateTime = ''; + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'updateModel') + .resolves(op); + stubs.push(stub); + return machineLearning.unpublishModel(MODEL_ID) + .should.eventually.be.rejected.and.have.property( + 'message', `Invalid Model response: ${JSON.stringify(op.response)}`); + }); + + it('should reject when API response does not contain a displayName', () => { + const op = deepCopy(OPERATION_RESPONSE); + op.response!.displayName = ''; + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'updateModel') + .resolves(op); + stubs.push(stub); + return machineLearning.unpublishModel(MODEL_ID) + .should.eventually.be.rejected.and.have.property( + 'message', `Invalid Model response: ${JSON.stringify(op.response)}`); + }); + + it('should reject when API response does not contain an etag', () => { + const op = deepCopy(OPERATION_RESPONSE); + op.response!.etag = ''; + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'updateModel') + .resolves(op); + stubs.push(stub); + return machineLearning.unpublishModel(MODEL_ID) + .should.eventually.be.rejected.and.have.property( + 'message', `Invalid Model response: ${JSON.stringify(op.response)}`); + }); + + it('should resolve with Model on success', () => { + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'updateModel') + .resolves(OPERATION_RESPONSE); + stubs.push(stub); + + return machineLearning.unpublishModel(MODEL_ID) + .then((model) => { + expect(model).to.deep.equal(model1); + }); + }); + + it('should resolve with Error on operation error', () => { + const stub = sinon + .stub(MachineLearningApiClient.prototype, 'updateModel') + .resolves(OPERATION_RESPONSE_ERROR); + stubs.push(stub); + + return machineLearning.unpublishModel(MODEL_ID) + .should.eventually.be.rejected.and.have.property( + 'message', 'Invalid Argument message'); + }); + }); +}); diff --git a/test/unit/messaging/batch-requests.spec.ts b/test/unit/messaging/batch-requests.spec.ts new file mode 100644 index 0000000000..fdde0c37f6 --- /dev/null +++ b/test/unit/messaging/batch-requests.spec.ts @@ -0,0 +1,270 @@ +/*! + * 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. + */ + +'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 utils from '../utils'; + +import { HttpClient, HttpResponse, HttpRequestConfig, HttpError } from '../../../src/utils/api-request'; +import { SubRequest, BatchRequestClient } from '../../../src/messaging/batch-request-internal'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +function parseHttpRequest(text: string | Buffer): any { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const httpMessageParser = require('http-message-parser'); + return httpMessageParser(text); +} + +function getParsedPartData(obj: object): string { + const json = JSON.stringify(obj); + return 'POST https://example.com HTTP/1.1\r\n' + + `Content-Length: ${json.length}\r\n` + + 'Content-Type: application/json; charset=UTF-8\r\n' + + '\r\n' + + `${json}`; +} + +function createMultipartResponse(success: object[], failures: object[] = []): HttpResponse { + 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 += `${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 += `${JSON.stringify(part)}\r\n`; + multipart.push(Buffer.from(payload, 'utf-8')); + }); + return { + status: 200, + headers: { 'Content-Type': 'multipart/mixed; boundary=boundary' }, + multipart, + text: '', + data: null, + isJson: () => false, + }; +} + +describe('BatchRequestClient', () => { + + const batchUrl = 'https://batch.url'; + const responseObject = { success: true }; + const httpClient = new HttpClient(); + + let stubs: sinon.SinonStub[] = []; + + afterEach(() => { + stubs.forEach((mock) => { + mock.restore(); + }); + stubs = []; + }); + + it('should serialize a batch with a single request', async () => { + const stub = sinon.stub(httpClient, 'send').resolves( + createMultipartResponse([responseObject])); + stubs.push(stub); + const requests: SubRequest[] = [ + { url: 'https://example.com', body: { foo: 1 } }, + ]; + const batch = new BatchRequestClient(httpClient, batchUrl); + + const responses: HttpResponse[] = await batch.send(requests); + + expect(responses.length).to.equal(1); + expect(responses[0].status).to.equal(200); + expect(responses[0].data).to.deep.equal(responseObject); + checkOutgoingRequest(stub, requests); + }); + + it('should serialize a batch with multiple requests', async () => { + const stub = sinon.stub(httpClient, 'send').resolves( + 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 } }, + ]; + const batch = new BatchRequestClient(httpClient, batchUrl); + + const responses: HttpResponse[] = await batch.send(requests); + + expect(responses.length).to.equal(3); + responses.forEach((response) => { + expect(response.status).to.equal(200); + expect(response.data).to.deep.equal(responseObject); + }); + checkOutgoingRequest(stub, requests); + }); + + it('should handle both success and failure HTTP responses in a batch', async () => { + const stub = sinon.stub(httpClient, 'send').resolves( + 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 } }, + ]; + const batch = new BatchRequestClient(httpClient, batchUrl); + + const responses: HttpResponse[] = await batch.send(requests); + + expect(responses.length).to.equal(3); + responses.forEach((response, idx) => { + const expectedStatus = idx < 2 ? 200 : 500; + expect(response.status).to.equal(expectedStatus); + expect(response.data).to.deep.equal(responseObject); + }); + checkOutgoingRequest(stub, requests); + }); + + it('should reject on top-level HTTP error responses', async () => { + const stub = sinon.stub(httpClient, 'send').rejects( + 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 } }, + ]; + const batch = new BatchRequestClient(httpClient, batchUrl); + + try { + await batch.send(requests); + sinon.assert.fail('No error thrown for HTTP error'); + } catch (err) { + expect(err).to.be.instanceOf(HttpError); + expect((err as HttpError).response.status).to.equal(500); + checkOutgoingRequest(stub, requests); + } + }); + + it('should add common headers to the parent and sub requests in a batch', async () => { + const stub = sinon.stub(httpClient, 'send').resolves( + createMultipartResponse([responseObject])); + stubs.push(stub); + const requests: SubRequest[] = [ + { url: 'https://example.com', body: { foo: 1 } }, + { url: 'https://example.com', body: { foo: 2 } }, + ]; + const commonHeaders = { 'X-Custom-Header': 'value' }; + const batch = new BatchRequestClient(httpClient, batchUrl, commonHeaders); + + const responses: HttpResponse[] = await batch.send(requests); + + expect(responses.length).to.equal(1); + expect(stub).to.have.been.calledOnce; + const args: HttpRequestConfig = stub.getCall(0).args[0]; + expect(args.headers).to.have.property('X-Custom-Header', 'value'); + + const parsedRequest = parseHttpRequest(args.data as Buffer); + expect(parsedRequest.multipart.length).to.equal(requests.length); + parsedRequest.multipart.forEach((sub: {body: Buffer}) => { + const parsedSubRequest: {headers: object} = parseHttpRequest(sub.body.toString().trim()); + expect(parsedSubRequest.headers).to.have.property('X-Custom-Header', 'value'); + }); + }); + + it('should add sub request headers to the payload', async () => { + const stub = sinon.stub(httpClient, 'send').resolves( + 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' } }, + ]; + const batch = new BatchRequestClient(httpClient, batchUrl); + + const responses: HttpResponse[] = await batch.send(requests); + + expect(responses.length).to.equal(1); + expect(stub).to.have.been.calledOnce; + const args: HttpRequestConfig = stub.getCall(0).args[0]; + const parsedRequest = parseHttpRequest(args.data as Buffer); + expect(parsedRequest.multipart.length).to.equal(requests.length); + parsedRequest.multipart.forEach((sub: {body: Buffer}) => { + const parsedSubRequest: {headers: object} = parseHttpRequest(sub.body.toString().trim()); + expect(parsedSubRequest.headers).to.have.property('X-Custom-Header', 'value'); + }); + }); + + it('sub request headers should get precedence', async () => { + const stub = sinon.stub(httpClient, 'send').resolves( + 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' } }, + ]; + const commonHeaders = { 'X-Custom-Header': 'value' }; + const batch = new BatchRequestClient(httpClient, batchUrl, commonHeaders); + + const responses: HttpResponse[] = await batch.send(requests); + + expect(responses.length).to.equal(1); + expect(stub).to.have.been.calledOnce; + const args: HttpRequestConfig = stub.getCall(0).args[0]; + const parsedRequest = parseHttpRequest(args.data as Buffer); + expect(parsedRequest.multipart.length).to.equal(requests.length); + parsedRequest.multipart.forEach((part: {body: Buffer}) => { + const parsedPart: {headers: object} = parseHttpRequest(part.body.toString().trim()); + expect(parsedPart.headers).to.have.property('X-Custom-Header', 'overwrite'); + }); + }); + + function checkOutgoingRequest(stub: sinon.SinonStub, requests: SubRequest[]): void { + expect(stub).to.have.been.calledOnce; + const args: HttpRequestConfig = stub.getCall(0).args[0]; + expect(args.method).to.equal('POST'); + expect(args.url).to.equal(batchUrl); + expect(args.headers).to.have.property( + 'Content-Type', 'multipart/mixed; boundary=__END_OF_PART__'); + expect(args.timeout).to.equal(15000); + const parsedRequest = parseHttpRequest(args.data as Buffer); + expect(parsedRequest.multipart.length).to.equal(requests.length); + + if (requests.length === 1) { + // http-message-parser handles single-element batches slightly differently. Specifically, the + // payload contents are exposed through body instead of multipart, and the body string uses + // \n instead of \r\n for line breaks. + let expectedPartData = getParsedPartData(requests[0].body); + expectedPartData = expectedPartData.replace(/\r\n/g, '\n'); + expect(parsedRequest.body.trim()).to.equal(expectedPartData); + } else { + requests.forEach((req, idx) => { + const part = parsedRequest.multipart[idx].body.toString().trim(); + expect(part).to.equal(getParsedPartData(req.body)); + }); + } + } +}); diff --git a/test/unit/messaging/index.spec.ts b/test/unit/messaging/index.spec.ts new file mode 100644 index 0000000000..56374ff1a6 --- /dev/null +++ b/test/unit/messaging/index.spec.ts @@ -0,0 +1,75 @@ +/*! + * @license + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +import * as chai from 'chai'; +import * as sinonChai from 'sinon-chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +import * as mocks from '../../resources/mocks'; +import { App } from '../../../src/app/index'; +import { getMessaging, Messaging } from '../../../src/messaging/index'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +describe('Messaging', () => { + let mockApp: App; + let mockCredentialApp: App; + + const noProjectIdError = '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.'; + + beforeEach(() => { + mockApp = mocks.app(); + mockCredentialApp = mocks.mockCredentialApp(); + }); + + describe('getMessaging()', () => { + it('should throw when default app is not available', () => { + expect(() => { + return getMessaging(); + }).to.throw('The default Firebase app does not exist.'); + }); + + it('should reject given an invalid credential without project ID', () => { + // Project ID not set in the environment. + delete process.env.GOOGLE_CLOUD_PROJECT; + delete process.env.GCLOUD_PROJECT; + const messaging = getMessaging(mockCredentialApp); + return messaging.send({ topic: 'test' }) + .should.eventually.rejectedWith(noProjectIdError); + }); + + it('should not throw given a valid app', () => { + expect(() => { + return getMessaging(mockApp); + }).not.to.throw(); + }); + + it('should return the same instance for a given app instance', () => { + const fcm1: Messaging = getMessaging(mockApp); + const fcm2: Messaging = getMessaging(mockApp); + expect(fcm1).to.equal(fcm2); + }); + }); +}); diff --git a/test/unit/messaging/messaging.spec.ts b/test/unit/messaging/messaging.spec.ts index b848897695..c343ea319d 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"); @@ -16,10 +17,6 @@ 'use strict'; -// Use untyped import syntax for Node built-ins. -import https = require('https'); -import stream = require('stream'); - import * as _ from 'lodash'; import * as chai from 'chai'; import * as nock from 'nock'; @@ -27,15 +24,17 @@ 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 {FirebaseApp} from '../../../src/firebase-app'; +import { FirebaseApp } from '../../../src/app/firebase-app'; import { - AndroidConfig, Message, Messaging, MessagingOptions, MessagingPayload, MessagingDevicesResponse, - MessagingTopicManagementResponse, WebpushConfig, - BLACKLISTED_OPTIONS_KEYS, BLACKLISTED_DATA_PAYLOAD_KEYS, -} from '../../../src/messaging/messaging'; + Message, MessagingOptions, MessagingPayload, MessagingDevicesResponse, + MessagingDeviceGroupResponse, MessagingTopicManagementResponse, BatchResponse, + SendResponse, MulticastMessage, Messaging, TokenMessage, TopicMessage, ConditionMessage, +} from '../../../src/messaging/index'; +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'; +import * as utils from '../utils'; chai.should(); chai.use(sinonChai); @@ -73,18 +72,74 @@ const STATUS_CODE_TO_ERROR_MAP = { 503: 'messaging/server-unavailable', }; -function mockSendRequest(): nock.Scope { +function mockSendRequest(messageId = 'projects/projec_id/messages/message_id'): nock.Scope { return nock(`https://${FCM_SEND_HOST}:443`) .post('/v1/projects/project_id/messages:send') .reply(200, { - name: 'projects/projec_id/messages/message_id', + name: `${messageId}`, }); } +function mockBatchRequest(ids: string[]): nock.Scope { + return mockBatchRequestWithErrors(ids); +} + +function mockBatchRequestWithErrors(ids: string[], errors: object[] = []): nock.Scope { + const mockPayload = createMultipartPayloadWithErrors(ids.map((id) => { + return { name: id }; + }), errors); + return nock(`https://${FCM_SEND_HOST}:443`) + .post('/batch') + .reply(200, mockPayload, { + 'Content-type': 'multipart/mixed; boundary=boundary', + }); +} + +function createMultipartPayloadWithErrors( + success: object[], failures: object[] = []): string { + + const boundary = 'boundary'; + let payload = ''; + 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 += `${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 += `${JSON.stringify(part)}\r\n`; + }); + payload += `--${boundary}--\r\n`; + return payload; +} + function mockSendError( statusCode: number, errorFormat: 'json' | 'text', responseOverride?: any, +): nock.Scope { + return mockErrorResponse( + '/v1/projects/project_id/messages:send', statusCode, errorFormat, responseOverride); +} + +function mockBatchError( + statusCode: number, + errorFormat: 'json' | 'text', + responseOverride?: any, +): nock.Scope { + return mockErrorResponse('/batch', statusCode, errorFormat, responseOverride); +} + +function mockErrorResponse( + path: string, + statusCode: number, + errorFormat: 'json' | 'text', + responseOverride?: any, ): nock.Scope { let response; let contentType; @@ -97,14 +152,14 @@ function mockSendError( } return nock(`https://${FCM_SEND_HOST}:443`) - .post('/v1/projects/project_id/messages:send') + .post(path) .reply(statusCode, responseOverride || response, { 'Content-Type': contentType, }); } function mockSendToDeviceStringRequest(mockFailure = false): nock.Scope { - let deviceResult: object = { message_id: `0:${ mocks.messaging.messageId }` }; + let deviceResult: object = { message_id: `0:${mocks.messaging.messageId}` }; if (mockFailure) { deviceResult = { error: 'InvalidRegistration' }; } @@ -116,7 +171,7 @@ function mockSendToDeviceStringRequest(mockFailure = false): nock.Scope { success: mockFailure ? 0 : 1, failure: mockFailure ? 1 : 0, canonical_ids: 0, - results: [ deviceResult ], + results: [deviceResult], }); } @@ -130,7 +185,7 @@ function mockSendToDeviceArrayRequest(): nock.Scope { canonical_ids: 1, results: [ { - message_id: `0:${ mocks.messaging.messageId }`, + message_id: `0:${mocks.messaging.messageId}`, registration_id: mocks.messaging.registrationToken + '3', }, { error: 'some-error' }, @@ -244,167 +299,1304 @@ function mockTopicSubscriptionRequestWithError( }); } +function disableRetries(messaging: Messaging): void { + (messaging as any).messagingRequestHandler.httpClient.retry = null; +} + +class CustomArray extends Array { } describe('Messaging', () => { let mockApp: FirebaseApp; let messaging: Messaging; - let mockResponse: stream.PassThrough; let mockedRequests: nock.Scope[] = []; - let requestWriteSpy: sinon.SinonSpy; let httpsRequestStub: sinon.SinonStub; - let mockRequestStream: mocks.MockStream; + let getTokenStub: sinon.SinonStub; let nullAccessTokenMessaging: Messaging; - let malformedAccessTokenMessaging: Messaging; - let rejectedPromiseAccessTokenMessaging: Messaging; - before(() => utils.mockFetchAccessTokenRequests()); + let messagingService: { [key: string]: any }; + let nullAccessTokenMessagingService: { [key: string]: any }; + + const mockAccessToken: string = utils.generateRandomAccessToken(); + const expectedHeaders = { + 'Authorization': 'Bearer ' + mockAccessToken, + 'X-Firebase-Client': `fire-admin-node/${getSdkVersion()}`, + 'access_token_auth': 'true', + }; + const emptyResponse = utils.responseFrom({}); - after(() => nock.cleanAll()); + after(() => { + nock.cleanAll(); + }); beforeEach(() => { mockApp = mocks.app(); + getTokenStub = utils.stubGetAccessToken(mockAccessToken, mockApp); messaging = new Messaging(mockApp); - - mockResponse = new stream.PassThrough(); - mockResponse.write(JSON.stringify({ mockResponse: true })); - mockResponse.end(); - - mockRequestStream = new mocks.MockStream(); - - requestWriteSpy = sinon.spy(mockRequestStream, 'write'); - nullAccessTokenMessaging = new Messaging(mocks.appReturningNullAccessToken()); - malformedAccessTokenMessaging = new Messaging(mocks.appReturningMalformedAccessToken()); - rejectedPromiseAccessTokenMessaging = new Messaging(mocks.appRejectedWhileFetchingAccessToken()); + messagingService = messaging; + nullAccessTokenMessagingService = nullAccessTokenMessaging; }); afterEach(() => { _.forEach(mockedRequests, (mockedRequest) => mockedRequest.done()); mockedRequests = []; - - requestWriteSpy.restore(); - if (httpsRequestStub && httpsRequestStub.restore) { httpsRequestStub.restore(); } - + getTokenStub.restore(); 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 messagingAny: any = Messaging; - return new messagingAny(invalidApp); - }).to.throw('First argument passed to admin.messaging() must be a valid Firebase app instance.'); - }); + 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 messagingAny: any = Messaging; + return new messagingAny(invalidApp); + }).to.throw('First argument passed to admin.messaging() must be a valid Firebase app instance.'); + }); + }); + + it('should throw given no app', () => { + expect(() => { + const messagingAny: any = Messaging; + return new messagingAny(); + }).to.throw('First argument passed to admin.messaging() must be a valid Firebase app instance.'); + }); + + it('should reject given app without project ID', () => { + const appWithoutProjectId = mocks.mockCredentialApp(); + const messagingWithoutProjectId = new Messaging(appWithoutProjectId); + messagingWithoutProjectId.send({ topic: 'test' }) + .should.eventually.be.rejectedWith( + 'Failed to determine project ID for Messaging. Initialize the SDK with service ' + + 'account credentials or set project ID as an app option. Alternatively set the ' + + 'GOOGLE_CLOUD_PROJECT environment variable.'); + }); + + it('should not throw given a valid app', () => { + expect(() => { + return new Messaging(mockApp); + }).not.to.throw(); + }); + }); + + describe('app', () => { + it('returns the app from the constructor', () => { + // We expect referential equality here + expect(messaging.app).to.equal(mockApp); + }); + + it('is read-only', () => { + expect(() => { + (messaging as any).app = mockApp; + }).to.throw('Cannot set property app of # which has only a getter'); + }); + }); + + describe('send()', () => { + it('should throw given no message', () => { + expect(() => { + messaging.send(undefined as any); + }).to.throw('Message must be a non-null object'); + expect(() => { + messaging.send(null as any); + }).to.throw('Message must be a non-null object'); + }); + + const noTarget = [ + {}, { token: null }, { token: '' }, { topic: null }, { topic: '' }, { condition: null }, { condition: '' }, + ]; + noTarget.forEach((message) => { + it(`should throw given message without target: ${JSON.stringify(message)}`, () => { + expect(() => { + messaging.send(message as any); + }).to.throw('Exactly one of topic, token or condition is required'); + }); + }); + + const multipleTargets = [ + { 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)}`, () => { + expect(() => { + messaging.send(message as any); + }).to.throw('Exactly one of topic, token or condition is required'); + }); + }); + + const invalidDryRun = [null, NaN, 0, 1, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidDryRun.forEach((dryRun) => { + it(`should throw given invalid dryRun parameter: ${JSON.stringify(dryRun)}`, () => { + expect(() => { + messaging.send({ token: 'a' }, dryRun as any); + }).to.throw('dryRun must be a boolean'); + }); + }); + + const invalidTopics = ['/topics/', '/foo/bar', 'foo bar']; + invalidTopics.forEach((topic) => { + it(`should throw given invalid topic name: ${JSON.stringify(topic)}`, () => { + expect(() => { + messaging.send({ topic }); + }).to.throw('Malformed topic name'); + }); + }); + + const targetMessages = [ + { 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)}`, () => { + mockedRequests.push(mockSendRequest()); + return messaging.send( + message, + ).should.eventually.equal('projects/projec_id/messages/message_id'); + }); + }); + targetMessages.forEach((message) => { + it(`should be fulfilled with a message ID in dryRun mode: ${JSON.stringify(message)}`, () => { + mockedRequests.push(mockSendRequest()); + return messaging.send( + message, + true, + ).should.eventually.equal('projects/projec_id/messages/message_id'); + }); + }); + + it('should fail when the backend server returns a detailed error', () => { + const resp = { + error: { + status: 'INVALID_ARGUMENT', + message: 'test error message', + }, + }; + mockedRequests.push(mockSendError(400, 'json', resp)); + return messaging.send( + { token: 'mock-token' }, + ).should.eventually.be.rejectedWith('test error message') + .and.have.property('code', 'messaging/invalid-argument'); + }); + + it('should fail when the backend server returns a detailed error with FCM error code', () => { + const resp = { + error: { + status: 'INVALID_ARGUMENT', + message: 'test error message', + details: [ + { + '@type': 'type.googleapis.com/google.firebase.fcm.v1.FcmError', + 'errorCode': 'UNREGISTERED', + }, + ], + }, + }; + mockedRequests.push(mockSendError(404, 'json', resp)); + return messaging.send( + { token: 'mock-token' }, + ).should.eventually.be.rejectedWith('test error message') + .and.have.property('code', 'messaging/registration-token-not-registered'); + }); + + ['THIRD_PARTY_AUTH_ERROR', 'APNS_AUTH_ERROR'].forEach((errorCode) => { + it(`should map ${errorCode} to third party auth error`, () => { + const resp = { + error: { + status: 'INVALID_ARGUMENT', + message: 'test error message', + details: [ + { + '@type': 'type.googleapis.com/google.firebase.fcm.v1.FcmError', + 'errorCode': errorCode, + }, + ], + }, + }; + mockedRequests.push(mockSendError(404, 'json', resp)); + return messaging.send( + { token: 'mock-token' }, + ).should.eventually.be.rejectedWith('test error message') + .and.have.property('code', 'messaging/third-party-auth-error'); + }); + }); + + it('should map server error code to client-side error', () => { + const resp = { + error: { + status: 'NOT_FOUND', + message: 'test error message', + }, + }; + mockedRequests.push(mockSendError(404, 'json', resp)); + return messaging.send( + { 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' }; + mockedRequests.push(mockSendError(400, 'json', resp)); + return messaging.send( + { token: 'mock-token' }, + ).should.eventually.be.rejected.and.have.property('code', 'messaging/unknown-error'); + }); + + it('should fail when the backend server returns a non-json error', () => { + // Error code will be determined based on the status code. + mockedRequests.push(mockSendError(400, 'text', 'foo bar')); + return messaging.send( + { token: 'mock-token' }, + ).should.eventually.be.rejected.and.have.property('code', 'messaging/invalid-argument'); + }); + }); + + describe('sendEach()', () => { + const validMessage: Message = { token: 'a' }; + + function checkSendResponseSuccess(response: SendResponse, messageId: string): void { + expect(response.success).to.be.true; + expect(response.messageId).to.equal(messageId); + expect(response.error).to.be.undefined; + } + + function checkSendResponseFailure(response: SendResponse, code: string, msg?: string): void { + expect(response.success).to.be.false; + expect(response.messageId).to.be.undefined; + expect(response.error).to.have.property('code', code); + if (msg) { + expect(response.error!.toString()).to.contain(msg); + } + } + + it('should throw given no messages', () => { + expect(() => { + messaging.sendEach(undefined as any); + }).to.throw('messages must be a non-empty array'); + expect(() => { + messaging.sendEach(null as any); + }).to.throw('messages must be a non-empty array'); + expect(() => { + messaging.sendEach([]); + }).to.throw('messages must be a non-empty array'); + }); + + it('should throw when called with more than 500 messages', () => { + const messages: Message[] = []; + for (let i = 0; i < 501; i++) { + messages.push(validMessage); + } + expect(() => { + messaging.sendEach(messages); + }).to.throw('messages list must not contain more than 500 items'); + }); + + it('should reject when a message is invalid', () => { + const invalidMessage: Message = {} as any; + messaging.sendEach([validMessage, invalidMessage]) + .should.eventually.be.rejectedWith('Exactly one of topic, token or condition is required'); + }); + + it('should reject a message when it does not pass local validation, but still try the other messages', () => { + const invalidMessage: Message = { token: 'a', notification: { imageUrl: 'abc' } }; + const messageIds = [ + 'projects/projec_id/messages/1', + ]; + messageIds.forEach(id => mockedRequests.push(mockSendRequest(id))) + return messaging.sendEach([invalidMessage, validMessage]) + .then((response: BatchResponse) => { + expect(response.successCount).to.equal(1); + expect(response.failureCount).to.equal(1); + }); + }); + + const invalidDryRun = [null, NaN, 0, 1, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidDryRun.forEach((dryRun) => { + it(`should throw given invalid dryRun parameter: ${JSON.stringify(dryRun)}`, () => { + expect(() => { + messaging.sendEach([{ token: 'a' }], dryRun as any); + }).to.throw('dryRun must be a boolean'); + }); + }); + + it('should be fulfilled with a BatchResponse given valid messages', () => { + const messageIds = [ + 'projects/projec_id/messages/1', + 'projects/projec_id/messages/2', + 'projects/projec_id/messages/3', + ]; + messageIds.forEach(id => mockedRequests.push(mockSendRequest(id))) + return messaging.sendEach([validMessage, validMessage, validMessage]) + .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; + }); + }); + }); + + it('should be fulfilled with a BatchResponse given array-like (issue #566)', () => { + const messageIds = [ + 'projects/projec_id/messages/1', + 'projects/projec_id/messages/2', + 'projects/projec_id/messages/3', + ]; + messageIds.forEach(id => mockedRequests.push(mockSendRequest(id))) + const message = { + token: 'a', + android: { + ttl: 3600, + }, + }; + const arrayLike = new CustomArray(); + arrayLike.push(message); + arrayLike.push(message); + arrayLike.push(message); + // Explicitly patch the constructor so that down compiling to ES5 doesn't affect the test. + // See https://github.com/firebase/firebase-admin-node/issues/566#issuecomment-501974238 + // for more context. + arrayLike.constructor = CustomArray; + + return messaging.sendEach(arrayLike) + .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; + }); + }); + }); + + it('should be fulfilled with a BatchResponse given valid messages in dryRun mode', () => { + const messageIds = [ + 'projects/projec_id/messages/1', + 'projects/projec_id/messages/2', + 'projects/projec_id/messages/3', + ]; + messageIds.forEach(id => mockedRequests.push(mockSendRequest(id))) + return messaging.sendEach([validMessage, validMessage, validMessage], true) + .then((response: BatchResponse) => { + expect(response.successCount).to.equal(3); + expect(response.failureCount).to.equal(0); + expect(response.responses.length).to.equal(3); + response.responses.forEach((resp, idx) => { + checkSendResponseSuccess(resp, messageIds[idx]); + }); + }); + }); + + it('should be fulfilled with a BatchResponse for partial failures', () => { + const messageIds = [ + 'projects/projec_id/messages/1', + 'projects/projec_id/messages/2', + ]; + const errors = [ + { + error: { + status: 'INVALID_ARGUMENT', + message: 'test error message', + }, + }, + ]; + messageIds.forEach(id => mockedRequests.push(mockSendRequest(id))) + errors.forEach(error => mockedRequests.push(mockSendError(400, 'json', error))) + return messaging.sendEach([validMessage, validMessage, validMessage], true) + .then((response: BatchResponse) => { + expect(response.successCount).to.equal(2); + expect(response.failureCount).to.equal(1); + expect(response.responses.length).to.equal(3); + + const responses = response.responses; + checkSendResponseSuccess(responses[0], messageIds[0]); + checkSendResponseSuccess(responses[1], messageIds[1]); + checkSendResponseFailure( + responses[2], 'messaging/invalid-argument', 'test error message'); + }); + }); + + it('should be fulfilled with a BatchResponse for all failures given an app which' + + 'returns null access tokens', () => { + return nullAccessTokenMessaging.sendEach( + [validMessage, validMessage], + ).then((response: BatchResponse) => { + expect(response.failureCount).to.equal(2); + response.responses.forEach(resp => checkSendResponseFailure( + resp, 'app/invalid-credential')); + }); + }); + + it('should expose the FCM error code in a detailed error via BatchResponse', () => { + const messageIds = [ + 'projects/projec_id/messages/1', + ]; + const errors = [ + { + error: { + status: 'INVALID_ARGUMENT', + message: 'test error message', + details: [ + { + '@type': 'type.googleapis.com/google.firebase.fcm.v1.FcmError', + 'errorCode': 'UNREGISTERED', + }, + ], + }, + }, + ]; + messageIds.forEach(id => mockedRequests.push(mockSendRequest(id))) + errors.forEach(error => mockedRequests.push(mockSendError(404, 'json', error))) + return messaging.sendEach([validMessage, validMessage], true) + .then((response: BatchResponse) => { + expect(response.successCount).to.equal(1); + expect(response.failureCount).to.equal(1); + expect(response.responses.length).to.equal(2); + + const responses = response.responses; + checkSendResponseSuccess(responses[0], messageIds[0]); + checkSendResponseFailure( + responses[1], 'messaging/registration-token-not-registered'); + }); + }); + + it('should map server error code to client-side error', () => { + const error = { + error: { + status: 'NOT_FOUND', + message: 'test error message', + } + }; + mockedRequests.push(mockSendError(404, 'json', error)); + mockedRequests.push(mockSendError(400, 'json', { error: 'test error message' })); + mockedRequests.push(mockSendError(400, 'text', 'foo bar')); + return messaging.sendEach( + [validMessage, validMessage, validMessage], + ).then((response: BatchResponse) => { + expect(response.failureCount).to.equal(3); + const responses = response.responses; + checkSendResponseFailure(responses[0], 'messaging/registration-token-not-registered'); + checkSendResponseFailure(responses[1], 'messaging/unknown-error'); + checkSendResponseFailure(responses[2], 'messaging/invalid-argument'); + }); + }); + + // 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]; + + messageIds.forEach(id => mockedRequests.push(mockSendRequest(id))) + + return messaging.sendEach(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('sendEachForMulticast()', () => { + const mockResponse: BatchResponse = { + 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' }, + ], + }; + + let stub: sinon.SinonStub | null; + + afterEach(() => { + if (stub) { + stub.restore(); + } + stub = null; + }); + + it('should throw given no messages', () => { + expect(() => { + messaging.sendEachForMulticast(undefined as any); + }).to.throw('MulticastMessage must be a non-null object'); + expect(() => { + messaging.sendEachForMulticast({} as any); + }).to.throw('tokens must be a non-empty array'); + expect(() => { + messaging.sendEachForMulticast({ tokens: [] }); + }).to.throw('tokens must be a non-empty array'); + }); + + it('should throw when called with more than 500 messages', () => { + const tokens: string[] = []; + for (let i = 0; i < 501; i++) { + tokens.push(`token${i}`); + } + expect(() => { + messaging.sendEachForMulticast({ tokens }); + }).to.throw('tokens list must not contain more than 500 items'); + }); + + const invalidDryRun = [null, NaN, 0, 1, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidDryRun.forEach((dryRun) => { + it(`should throw given invalid dryRun parameter: ${JSON.stringify(dryRun)}`, () => { + expect(() => { + messaging.sendEachForMulticast({ tokens: ['a'] }, dryRun as any); + }).to.throw('dryRun must be a boolean'); + }); + }); + + it('should create multiple messages using the empty multicast payload', () => { + stub = sinon.stub(messaging, 'sendEach').resolves(mockResponse); + const tokens = ['a', 'b', 'c']; + return messaging.sendEachForMulticast({ tokens }) + .then((response: BatchResponse) => { + expect(response).to.deep.equal(mockResponse); + expect(stub).to.have.been.calledOnce; + const messages: Message[] = stub!.args[0][0]; + expect(messages.length).to.equal(3); + expect(stub!.args[0][1]).to.be.undefined; + messages.forEach((message, 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; + expect(message.notification).to.be.undefined; + expect(message.webpush).to.be.undefined; + }); + }); + }); + + it('should create multiple messages using the multicast payload', () => { + stub = sinon.stub(messaging, 'sendEach').resolves(mockResponse); + 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' }, + }; + return messaging.sendEachForMulticast(multicast) + .then((response: BatchResponse) => { + expect(response).to.deep.equal(mockResponse); + expect(stub).to.have.been.calledOnce; + const messages: Message[] = stub!.args[0][0]; + expect(messages.length).to.equal(3); + expect(stub!.args[0][1]).to.be.undefined; + messages.forEach((message, 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); + expect(message.notification).to.deep.equal(multicast.notification); + expect(message.webpush).to.deep.equal(multicast.webpush); + expect(message.fcmOptions).to.deep.equal(multicast.fcmOptions); + }); + }); + }); + + it('should pass dryRun argument through', () => { + stub = sinon.stub(messaging, 'sendEach').resolves(mockResponse); + const tokens = ['a', 'b', 'c']; + return messaging.sendEachForMulticast({ tokens }, true) + .then((response: BatchResponse) => { + expect(response).to.deep.equal(mockResponse); + expect(stub).to.have.been.calledOnce; + expect(stub!.args[0][1]).to.be.true; + }); + }); + + it('should be fulfilled with a BatchResponse given valid message', () => { + const messageIds = [ + 'projects/projec_id/messages/1', + 'projects/projec_id/messages/2', + 'projects/projec_id/messages/3', + ]; + messageIds.forEach(id => mockedRequests.push(mockSendRequest(id))) + return messaging.sendEachForMulticast({ + tokens: ['a', 'b', 'c'], + 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); + response.responses.forEach((resp, idx) => { + expect(resp.success).to.be.true; + expect(resp.messageId).to.equal(messageIds[idx]); + expect(resp.error).to.be.undefined; + }); + }); + }); + + it('should be fulfilled with a BatchResponse given valid message in dryRun mode', () => { + const messageIds = [ + 'projects/projec_id/messages/1', + 'projects/projec_id/messages/2', + 'projects/projec_id/messages/3', + ]; + messageIds.forEach(id => mockedRequests.push(mockSendRequest(id))) + return messaging.sendEachForMulticast({ + tokens: ['a', 'b', 'c'], + 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); + expect(response.responses.length).to.equal(3); + response.responses.forEach((resp, idx) => { + checkSendResponseSuccess(resp, messageIds[idx]); + }); + }); + }); + + it('should be fulfilled with a BatchResponse for partial failures', () => { + const messageIds = [ + 'projects/projec_id/messages/1', + 'projects/projec_id/messages/2', + ]; + const errors = [ + { + error: { + status: 'INVALID_ARGUMENT', + message: 'test error message', + }, + }, + ]; + messageIds.forEach(id => mockedRequests.push(mockSendRequest(id))) + errors.forEach(err => mockedRequests.push(mockSendError(400, 'json', err))) + return messaging.sendEachForMulticast({ tokens: ['a', 'b', 'c'] }) + .then((response: BatchResponse) => { + expect(response.successCount).to.equal(2); + expect(response.failureCount).to.equal(1); + expect(response.responses.length).to.equal(3); + + const responses = response.responses; + checkSendResponseSuccess(responses[0], messageIds[0]); + checkSendResponseSuccess(responses[1], messageIds[1]); + checkSendResponseFailure( + responses[2], 'messaging/invalid-argument', 'test error message'); + }); + }); + + it('should be fulfilled with a BatchResponse for all failures given an app which ' + + 'returns null access tokens', () => { + return nullAccessTokenMessaging.sendEachForMulticast( + { tokens: ['a', 'a'] }, + ).then((response: BatchResponse) => { + expect(response.failureCount).to.equal(2); + response.responses.forEach(resp => checkSendResponseFailure( + resp, 'app/invalid-credential')); + }); + }); + + it('should expose the FCM error code in a detailed error via BatchResponse', () => { + const messageIds = [ + 'projects/projec_id/messages/1', + ]; + const errors = [ + { + error: { + status: 'INVALID_ARGUMENT', + message: 'test error message', + details: [ + { + '@type': 'type.googleapis.com/google.firebase.fcm.v1.FcmError', + 'errorCode': 'UNREGISTERED', + }, + ], + }, + }, + ]; + messageIds.forEach(id => mockedRequests.push(mockSendRequest(id))) + errors.forEach(err => mockedRequests.push(mockSendError(400, 'json', err))) + return messaging.sendEachForMulticast({ tokens: ['a', 'b'] }) + .then((response: BatchResponse) => { + expect(response.successCount).to.equal(1); + expect(response.failureCount).to.equal(1); + expect(response.responses.length).to.equal(2); + + const responses = response.responses; + checkSendResponseSuccess(responses[0], messageIds[0]); + checkSendResponseFailure( + responses[1], 'messaging/registration-token-not-registered'); + }); + }); + + it('should map server error code to client-side error', () => { + const error = { + error: { + status: 'NOT_FOUND', + message: 'test error message', + } + }; + mockedRequests.push(mockSendError(404, 'json', error)); + mockedRequests.push(mockSendError(400, 'json', { error: 'test error message' })); + mockedRequests.push(mockSendError(400, 'text', 'foo bar')); + return messaging.sendEachForMulticast( + { tokens: ['a', 'a', 'a'] }, + ).then((response: BatchResponse) => { + expect(response.failureCount).to.equal(3); + const responses = response.responses; + checkSendResponseFailure(responses[0], 'messaging/registration-token-not-registered'); + checkSendResponseFailure(responses[1], 'messaging/unknown-error'); + checkSendResponseFailure(responses[2], 'messaging/invalid-argument'); + }); + }); + + function checkSendResponseSuccess(response: SendResponse, messageId: string): void { + expect(response.success).to.be.true; + expect(response.messageId).to.equal(messageId); + expect(response.error).to.be.undefined; + } + + function checkSendResponseFailure(response: SendResponse, code: string, msg?: string): void { + expect(response.success).to.be.false; + expect(response.messageId).to.be.undefined; + expect(response.error).to.have.property('code', code); + if (msg) { + expect(response.error!.toString()).to.contain(msg); + } + } + }); + + describe('sendAll()', () => { + const validMessage: Message = { token: 'a' }; + + function checkSendResponseSuccess(response: SendResponse, messageId: string): void { + expect(response.success).to.be.true; + expect(response.messageId).to.equal(messageId); + expect(response.error).to.be.undefined; + } + + function checkSendResponseFailure(response: SendResponse, code: string, msg?: string): void { + expect(response.success).to.be.false; + expect(response.messageId).to.be.undefined; + expect(response.error).to.have.property('code', code); + if (msg) { + expect(response.error!.toString()).to.contain(msg); + } + } + + it('should throw given no messages', () => { + expect(() => { + messaging.sendAll(undefined as any); + }).to.throw('messages must be a non-empty array'); + expect(() => { + messaging.sendAll(null as any); + }).to.throw('messages must be a non-empty array'); + expect(() => { + messaging.sendAll([]); + }).to.throw('messages must be a non-empty array'); + }); + + it('should throw when called with more than 500 messages', () => { + const messages: Message[] = []; + for (let i = 0; i < 501; i++) { + messages.push(validMessage); + } + expect(() => { + messaging.sendAll(messages); + }).to.throw('messages list must not contain more than 500 items'); + }); + + it('should reject when a message is invalid', () => { + const invalidMessage: Message = {} as any; + messaging.sendAll([validMessage, invalidMessage]) + .should.eventually.be.rejectedWith('Exactly one of topic, token or condition is required'); + }); + + const invalidDryRun = [null, NaN, 0, 1, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidDryRun.forEach((dryRun) => { + it(`should throw given invalid dryRun parameter: ${JSON.stringify(dryRun)}`, () => { + expect(() => { + messaging.sendAll([{ token: 'a' }], dryRun as any); + }).to.throw('dryRun must be a boolean'); + }); + }); + + it('should be fulfilled with a BatchResponse given valid messages', () => { + const messageIds = [ + 'projects/projec_id/messages/1', + 'projects/projec_id/messages/2', + 'projects/projec_id/messages/3', + ]; + mockedRequests.push(mockBatchRequest(messageIds)); + return messaging.sendAll([validMessage, validMessage, validMessage]) + .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; + }); + }); + }); + + it('should be fulfilled with a BatchResponse given array-like (issue #566)', () => { + const messageIds = [ + 'projects/projec_id/messages/1', + 'projects/projec_id/messages/2', + 'projects/projec_id/messages/3', + ]; + mockedRequests.push(mockBatchRequest(messageIds)); + const message = { + token: 'a', + android: { + ttl: 3600, + }, + }; + const arrayLike = new CustomArray(); + arrayLike.push(message); + arrayLike.push(message); + arrayLike.push(message); + // Explicitly patch the constructor so that down compiling to ES5 doesn't affect the test. + // See https://github.com/firebase/firebase-admin-node/issues/566#issuecomment-501974238 + // for more context. + arrayLike.constructor = CustomArray; + + return messaging.sendAll(arrayLike) + .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; + }); + }); + }); + + it('should be fulfilled with a BatchResponse given valid messages in dryRun mode', () => { + const messageIds = [ + 'projects/projec_id/messages/1', + 'projects/projec_id/messages/2', + 'projects/projec_id/messages/3', + ]; + mockedRequests.push(mockBatchRequest(messageIds)); + return messaging.sendAll([validMessage, validMessage, validMessage], true) + .then((response: BatchResponse) => { + expect(response.successCount).to.equal(3); + expect(response.failureCount).to.equal(0); + expect(response.responses.length).to.equal(3); + response.responses.forEach((resp, idx) => { + checkSendResponseSuccess(resp, messageIds[idx]); + }); + }); + }); + + it('should be fulfilled with a BatchResponse when the response contains some errors', () => { + const messageIds = [ + 'projects/projec_id/messages/1', + 'projects/projec_id/messages/2', + ]; + const errors = [ + { + error: { + status: 'INVALID_ARGUMENT', + message: 'test error message', + }, + }, + ]; + mockedRequests.push(mockBatchRequestWithErrors(messageIds, errors)); + return messaging.sendAll([validMessage, validMessage, validMessage], true) + .then((response: BatchResponse) => { + expect(response.successCount).to.equal(2); + expect(response.failureCount).to.equal(1); + expect(response.responses.length).to.equal(3); + + const responses = response.responses; + checkSendResponseSuccess(responses[0], messageIds[0]); + checkSendResponseSuccess(responses[1], messageIds[1]); + checkSendResponseFailure( + responses[2], 'messaging/invalid-argument', 'test error message'); + }); + }); + + it('should expose the FCM error code via BatchResponse', () => { + const messageIds = [ + 'projects/projec_id/messages/1', + ]; + const errors = [ + { + error: { + status: 'INVALID_ARGUMENT', + message: 'test error message', + details: [ + { + '@type': 'type.googleapis.com/google.firebase.fcm.v1.FcmError', + 'errorCode': 'UNREGISTERED', + }, + ], + }, + }, + ]; + mockedRequests.push(mockBatchRequestWithErrors(messageIds, errors)); + return messaging.sendAll([validMessage, validMessage], true) + .then((response: BatchResponse) => { + expect(response.successCount).to.equal(1); + expect(response.failureCount).to.equal(1); + expect(response.responses.length).to.equal(2); + + const responses = response.responses; + checkSendResponseSuccess(responses[0], messageIds[0]); + checkSendResponseFailure( + responses[1], 'messaging/registration-token-not-registered'); + }); + }); + + it('should fail when the backend server returns a detailed error', () => { + const resp = { + error: { + status: 'INVALID_ARGUMENT', + message: 'test error message', + }, + }; + mockedRequests.push(mockBatchError(400, 'json', resp)); + return messaging.sendAll( + [validMessage], + ).should.eventually.be.rejectedWith('test error message') + .and.have.property('code', 'messaging/invalid-argument'); + }); + + it('should fail when the backend server returns a detailed error with FCM error code', () => { + const resp = { + error: { + status: 'INVALID_ARGUMENT', + message: 'test error message', + details: [ + { + '@type': 'type.googleapis.com/google.firebase.fcm.v1.FcmError', + 'errorCode': 'UNREGISTERED', + }, + ], + }, + }; + mockedRequests.push(mockBatchError(404, 'json', resp)); + return messaging.sendAll( + [validMessage], + ).should.eventually.be.rejectedWith('test error message') + .and.have.property('code', 'messaging/registration-token-not-registered'); + }); + + it('should map server error code to client-side error', () => { + const resp = { + error: { + status: 'NOT_FOUND', + message: 'test error message', + }, + }; + mockedRequests.push(mockBatchError(404, 'json', resp)); + return messaging.sendAll( + [validMessage], + ).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' }; + mockedRequests.push(mockBatchError(400, 'json', resp)); + return messaging.sendAll( + [validMessage], + ).should.eventually.be.rejected.and.have.property('code', 'messaging/unknown-error'); + }); + + it('should fail when the backend server returns a non-json error', () => { + // Error code will be determined based on the status code. + mockedRequests.push(mockBatchError(400, 'text', 'foo bar')); + return messaging.sendAll( + [validMessage], + ).should.eventually.be.rejected.and.have.property('code', 'messaging/invalid-argument'); }); - it('should throw given no app', () => { - expect(() => { - const messagingAny: any = Messaging; - return new messagingAny(); - }).to.throw('First argument passed to admin.messaging() must be a valid Firebase app instance.'); + it('should be rejected given an app which returns null access tokens', () => { + return nullAccessTokenMessaging.sendAll( + [validMessage], + ).should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); }); - it('should not throw given a valid app', () => { - expect(() => { - return new Messaging(mockApp); - }).not.to.throw(); + // 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('app', () => { - it('returns the app from the constructor', () => { - // We expect referential equality here - expect(messaging.app).to.equal(mockApp); - }); + describe('sendMulticast()', () => { + const mockResponse: BatchResponse = { + 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' }, + ], + }; - it('is read-only', () => { - expect(() => { - (messaging as any).app = mockApp; - }).to.throw('Cannot set property app of # which has only a getter'); + let stub: sinon.SinonStub | null; + + afterEach(() => { + if (stub) { + stub.restore(); + } + stub = null; }); - }); - describe('send()', () => { - it('should throw given no message', () => { + it('should throw given no messages', () => { expect(() => { - messaging.send(undefined as Message); - }).to.throw('Message must be a non-null object'); + messaging.sendMulticast(undefined as any); + }).to.throw('MulticastMessage must be a non-null object'); expect(() => { - messaging.send(null); - }).to.throw('Message must be a non-null object'); - }); - - const noTarget = [ - {}, {token: null}, {token: ''}, {topic: null}, {topic: ''}, {condition: null}, {condition: ''}, - ]; - noTarget.forEach((message) => { - it(`should throw given message without target: ${ JSON.stringify(message) }`, () => { - expect(() => { - messaging.send(message as any); - }).to.throw('Exactly one of topic, token or condition is required'); - }); + messaging.sendMulticast({} as any); + }).to.throw('tokens must be a non-empty array'); + expect(() => { + messaging.sendMulticast({ tokens: [] }); + }).to.throw('tokens must be a non-empty array'); }); - const multipleTargets = [ - {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)}`, () => { - expect(() => { - messaging.send(message as any); - }).to.throw('Exactly one of topic, token or condition is required'); - }); + it('should throw when called with more than 500 messages', () => { + const tokens: string[] = []; + for (let i = 0; i < 501; i++) { + tokens.push(`token${i}`); + } + expect(() => { + messaging.sendMulticast({ tokens }); + }).to.throw('tokens list must not contain more than 500 items'); }); const invalidDryRun = [null, NaN, 0, 1, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop]; invalidDryRun.forEach((dryRun) => { it(`should throw given invalid dryRun parameter: ${JSON.stringify(dryRun)}`, () => { expect(() => { - messaging.send({token: 'a'}, dryRun as any); + messaging.sendMulticast({ tokens: ['a'] }, dryRun as any); }).to.throw('dryRun must be a boolean'); }); }); - const invalidTopics = ['/topics/', '/foo/bar', 'foo bar']; - invalidTopics.forEach((topic) => { - it(`should throw given invalid topic name: ${JSON.stringify(topic)}`, () => { - expect(() => { - messaging.send({topic}); - }).to.throw('Malformed topic name'); - }); + 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 }) + .then((response: BatchResponse) => { + expect(response).to.deep.equal(mockResponse); + expect(stub).to.have.been.calledOnce; + const messages: Message[] = stub!.args[0][0]; + expect(messages.length).to.equal(3); + expect(stub!.args[0][1]).to.be.undefined; + messages.forEach((message, 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; + expect(message.notification).to.be.undefined; + expect(message.webpush).to.be.undefined; + }); + }); }); - const targetMessages = [ - {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)}`, () => { - mockedRequests.push(mockSendRequest()); - return messaging.send( - message, - ).should.eventually.equal('projects/projec_id/messages/message_id'); + it('should create multiple messages using the multicast payload', () => { + stub = sinon.stub(messaging, 'sendAll').resolves(mockResponse); + 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' }, + }; + return messaging.sendMulticast(multicast) + .then((response: BatchResponse) => { + expect(response).to.deep.equal(mockResponse); + expect(stub).to.have.been.calledOnce; + const messages: Message[] = stub!.args[0][0]; + expect(messages.length).to.equal(3); + expect(stub!.args[0][1]).to.be.undefined; + messages.forEach((message, 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); + expect(message.notification).to.deep.equal(multicast.notification); + expect(message.webpush).to.deep.equal(multicast.webpush); + expect(message.fcmOptions).to.deep.equal(multicast.fcmOptions); + }); + }); + }); + + it('should pass dryRun argument through', () => { + stub = sinon.stub(messaging, 'sendAll').resolves(mockResponse); + const tokens = ['a', 'b', 'c']; + return messaging.sendMulticast({ tokens }, true) + .then((response: BatchResponse) => { + expect(response).to.deep.equal(mockResponse); + expect(stub).to.have.been.calledOnce; + expect(stub!.args[0][1]).to.be.true; + }); + }); + + it('should be fulfilled with a BatchResponse given valid message', () => { + const messageIds = [ + 'projects/projec_id/messages/1', + 'projects/projec_id/messages/2', + 'projects/projec_id/messages/3', + ]; + 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' } }, + }).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; + }); }); }); - targetMessages.forEach((message) => { - it(`should be fulfilled with a message ID in dryRun mode: ${JSON.stringify(message)}`, () => { - mockedRequests.push(mockSendRequest()); - return messaging.send( - message, - true, - ).should.eventually.equal('projects/projec_id/messages/message_id'); + + it('should be fulfilled with a BatchResponse given valid message in dryRun mode', () => { + const messageIds = [ + 'projects/projec_id/messages/1', + 'projects/projec_id/messages/2', + 'projects/projec_id/messages/3', + ]; + 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' } }, + }, true).then((response: BatchResponse) => { + expect(response.successCount).to.equal(3); + expect(response.failureCount).to.equal(0); + expect(response.responses.length).to.equal(3); + response.responses.forEach((resp, idx) => { + checkSendResponseSuccess(resp, messageIds[idx]); + }); }); }); + it('should be fulfilled with a BatchResponse when the response contains some errors', () => { + const messageIds = [ + 'projects/projec_id/messages/1', + 'projects/projec_id/messages/2', + ]; + const errors = [ + { + error: { + status: 'INVALID_ARGUMENT', + message: 'test error message', + }, + }, + ]; + mockedRequests.push(mockBatchRequestWithErrors(messageIds, errors)); + return messaging.sendMulticast({ tokens: ['a', 'b'] }) + .then((response: BatchResponse) => { + expect(response.successCount).to.equal(2); + expect(response.failureCount).to.equal(1); + expect(response.responses.length).to.equal(3); + + const responses = response.responses; + checkSendResponseSuccess(responses[0], messageIds[0]); + checkSendResponseSuccess(responses[1], messageIds[1]); + checkSendResponseFailure( + responses[2], 'messaging/invalid-argument', 'test error message'); + }); + }); + + it('should expose the FCM error code via BatchResponse', () => { + const messageIds = [ + 'projects/projec_id/messages/1', + ]; + const errors = [ + { + error: { + status: 'INVALID_ARGUMENT', + message: 'test error message', + details: [ + { + '@type': 'type.googleapis.com/google.firebase.fcm.v1.FcmError', + 'errorCode': 'UNREGISTERED', + }, + ], + }, + }, + ]; + mockedRequests.push(mockBatchRequestWithErrors(messageIds, errors)); + return messaging.sendMulticast({ tokens: ['a', 'b'] }) + .then((response: BatchResponse) => { + expect(response.successCount).to.equal(1); + expect(response.failureCount).to.equal(1); + expect(response.responses.length).to.equal(2); + + const responses = response.responses; + checkSendResponseSuccess(responses[0], messageIds[0]); + checkSendResponseFailure( + responses[1], 'messaging/registration-token-not-registered'); + }); + }); + it('should fail when the backend server returns a detailed error', () => { const resp = { error: { @@ -412,11 +1604,31 @@ describe('Messaging', () => { message: 'test error message', }, }; - mockedRequests.push(mockSendError(400, 'json', resp)); - return messaging.send( - {token: 'mock-token'}, + mockedRequests.push(mockBatchError(400, 'json', resp)); + return messaging.sendMulticast( + { tokens: ['a'] }, + ).should.eventually.be.rejectedWith('test error message') + .and.have.property('code', 'messaging/invalid-argument'); + }); + + it('should fail when the backend server returns a detailed error with FCM error code', () => { + const resp = { + error: { + status: 'INVALID_ARGUMENT', + message: 'test error message', + details: [ + { + '@type': 'type.googleapis.com/google.firebase.fcm.v1.FcmError', + 'errorCode': 'UNREGISTERED', + }, + ], + }, + }; + mockedRequests.push(mockBatchError(404, 'json', resp)); + return messaging.sendMulticast( + { tokens: ['a'] }, ).should.eventually.be.rejectedWith('test error message') - .and.have.property('code', 'messaging/invalid-argument'); + .and.have.property('code', 'messaging/registration-token-not-registered'); }); it('should map server error code to client-side error', () => { @@ -426,28 +1638,49 @@ describe('Messaging', () => { message: 'test error message', }, }; - mockedRequests.push(mockSendError(404, 'json', resp)); - return messaging.send( - {token: 'mock-token'}, + mockedRequests.push(mockBatchError(404, 'json', resp)); + return messaging.sendMulticast( + { tokens: ['a'] }, ).should.eventually.be.rejectedWith('test error message') - .and.have.property('code', 'messaging/registration-token-not-registered'); + .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'}; - mockedRequests.push(mockSendError(400, 'json', resp)); - return messaging.send( - {token: 'mock-token'}, + const resp = { error: 'test error message' }; + mockedRequests.push(mockBatchError(400, 'json', resp)); + return messaging.sendMulticast( + { tokens: ['a'] }, ).should.eventually.be.rejected.and.have.property('code', 'messaging/unknown-error'); }); it('should fail when the backend server returns a non-json error', () => { // Error code will be determined based on the status code. - mockedRequests.push(mockSendError(400, 'text', 'foo bar')); - return messaging.send( - {token: 'mock-token'}, + mockedRequests.push(mockBatchError(400, 'text', 'foo bar')); + return messaging.sendMulticast( + { 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'] }, + ).should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + function checkSendResponseSuccess(response: SendResponse, messageId: string): void { + expect(response.success).to.be.true; + expect(response.messageId).to.equal(messageId); + expect(response.error).to.be.undefined; + } + + function checkSendResponseFailure(response: SendResponse, code: string, msg?: string): void { + expect(response.success).to.be.false; + expect(response.messageId).to.be.undefined; + expect(response.error).to.have.property('code', code); + if (msg) { + expect(response.error!.toString()).to.contain(msg); + } + } }); describe('sendToDevice()', () => { @@ -466,7 +1699,7 @@ describe('Messaging', () => { it('should throw given no registration token(s) argument', () => { expect(() => { - messaging.sendToDevice(undefined as string, mocks.messaging.payloadDataOnly); + messaging.sendToDevice(undefined as any, mocks.messaging.payloadDataOnly); }).to.throw(invalidArgumentError); }); @@ -556,6 +1789,7 @@ describe('Messaging', () => { _.forEach(STATUS_CODE_TO_ERROR_MAP, (expectedError, statusCode) => { it(`should be rejected given a ${ statusCode } text server response`, () => { mockedRequests.push(mockSendRequestWithError(parseInt(statusCode, 10), 'text')); + disableRetries(messaging); return messaging.sendToDevice( mocks.messaging.registrationToken, @@ -658,10 +1892,11 @@ describe('Messaging', () => { mocks.messaging.registrationToken + '2', ], mocks.messaging.payload, - ).then((response: MessagingDevicesResponse) => { + ).then((response: MessagingDevicesResponse | MessagingDeviceGroupResponse) => { expect(response).to.have.keys([ 'failureCount', 'successCount', 'canonicalRegistrationTokenCount', 'multicastId', 'results', ]); + response = response as MessagingDevicesResponse; expect(response.failureCount).to.equal(2); expect(response.successCount).to.equal(1); expect(response.canonicalRegistrationTokenCount).to.equal(1); @@ -682,17 +1917,15 @@ describe('Messaging', () => { // Wait for the initial getToken() call to complete before stubbing https.request. return mockApp.INTERNAL.getToken() .then(() => { - httpsRequestStub = sinon.stub(https, 'request'); - httpsRequestStub.callsArgWith(1, mockResponse).returns(mockRequestStream); - + httpsRequestStub = sinon.stub(HttpClient.prototype, 'send').resolves(emptyResponse); return messaging.sendToDevice( mocks.messaging.registrationToken, mocks.messaging.payload, ); }) .then(() => { - expect(requestWriteSpy).to.have.been.calledOnce; - const requestData = JSON.parse(requestWriteSpy.args[0][0]); + expect(httpsRequestStub).to.have.been.calledOnce; + const requestData = httpsRequestStub.args[0][0].data; expect(requestData).to.deep.equal({ to: mocks.messaging.registrationToken, data: mocks.messaging.payload.data, @@ -711,17 +1944,15 @@ describe('Messaging', () => { // Wait for the initial getToken() call to complete before stubbing https.request. return mockApp.INTERNAL.getToken() .then(() => { - httpsRequestStub = sinon.stub(https, 'request'); - httpsRequestStub.callsArgWith(1, mockResponse).returns(mockRequestStream); - + httpsRequestStub = sinon.stub(HttpClient.prototype, 'send').resolves(emptyResponse); return messaging.sendToDevice( registrationTokens, mocks.messaging.payload, ); }) .then(() => { - expect(requestWriteSpy).to.have.been.calledOnce; - const requestData = JSON.parse(requestWriteSpy.args[0][0]); + expect(httpsRequestStub).to.have.been.calledOnce; + const requestData = httpsRequestStub.args[0][0].data; expect(requestData).to.deep.equal({ registration_ids: registrationTokens, data: mocks.messaging.payload.data, @@ -743,6 +1974,9 @@ describe('Messaging', () => { mocks.messaging.registrationToken + '0', mocks.messaging.registrationToken + '1', ], + canonicalRegistrationTokenCount: -1, + multicastId: -1, + results: [], }); }); @@ -790,7 +2024,7 @@ describe('Messaging', () => { it('should throw given no notification key argument', () => { expect(() => { - messaging.sendToDeviceGroup(undefined as string, mocks.messaging.payloadDataOnly); + messaging.sendToDeviceGroup(undefined as any, mocks.messaging.payloadDataOnly); }).to.throw(invalidArgumentError); }); @@ -853,9 +2087,10 @@ describe('Messaging', () => { _.forEach(STATUS_CODE_TO_ERROR_MAP, (expectedError, statusCode) => { it(`should be rejected given a ${ statusCode } text server response`, () => { mockedRequests.push(mockSendRequestWithError(parseInt(statusCode, 10), 'text')); + disableRetries(messaging); return messaging.sendToDeviceGroup( - mocks.messaging.notificationKey, + mocks.messaging.notificationKey, mocks.messaging.payload, ).should.eventually.be.rejected.and.have.property('code', expectedError); }); @@ -962,16 +2197,14 @@ describe('Messaging', () => { // Wait for the initial getToken() call to complete before stubbing https.request. return mockApp.INTERNAL.getToken() .then(() => { - httpsRequestStub = sinon.stub(https, 'request'); - httpsRequestStub.callsArgWith(1, mockResponse).returns(mockRequestStream); - + httpsRequestStub = sinon.stub(HttpClient.prototype, 'send').resolves(emptyResponse); return messaging.sendToDeviceGroup( mocks.messaging.notificationKey, mocks.messaging.payload, ); }).then(() => { - expect(requestWriteSpy).to.have.been.calledOnce; - const requestData = JSON.parse(requestWriteSpy.args[0][0]); + expect(httpsRequestStub).to.have.been.calledOnce; + const requestData = httpsRequestStub.args[0][0].data; expect(requestData).to.deep.equal({ to: mocks.messaging.notificationKey, data: mocks.messaging.payload.data, @@ -994,6 +2227,7 @@ describe('Messaging', () => { results: [ { messageId: `0:${ mocks.messaging.messageId }` }, ], + failedRegistrationTokens: [], }); }); @@ -1039,7 +2273,7 @@ describe('Messaging', () => { it('should throw given no topic argument', () => { expect(() => { - messaging.sendToTopic(undefined as string, mocks.messaging.payload); + messaging.sendToTopic(undefined as any, mocks.messaging.payload); }).to.throw(invalidArgumentError); }); @@ -1105,9 +2339,10 @@ describe('Messaging', () => { _.forEach(STATUS_CODE_TO_ERROR_MAP, (expectedError, statusCode) => { it(`should be rejected given a ${ statusCode } text server response`, () => { mockedRequests.push(mockSendRequestWithError(parseInt(statusCode, 10), 'text')); + disableRetries(messaging); return messaging.sendToTopic( - mocks.messaging.topic, + mocks.messaging.topic, mocks.messaging.payload, ).should.eventually.be.rejected.and.have.property('code', expectedError); }); @@ -1186,16 +2421,14 @@ describe('Messaging', () => { // Wait for the initial getToken() call to complete before stubbing https.request. return mockApp.INTERNAL.getToken() .then(() => { - httpsRequestStub = sinon.stub(https, 'request'); - httpsRequestStub.callsArgWith(1, mockResponse).returns(mockRequestStream); - + httpsRequestStub = sinon.stub(HttpClient.prototype, 'send').resolves(emptyResponse); return messaging.sendToTopic( mocks.messaging.topic, mocks.messaging.payload, ); }).then(() => { - expect(requestWriteSpy).to.have.been.calledOnce; - const requestData = JSON.parse(requestWriteSpy.args[0][0]); + expect(httpsRequestStub).to.have.been.calledOnce; + const requestData = httpsRequestStub.args[0][0].data; expect(requestData).to.deep.equal({ to: mocks.messaging.topicWithPrefix, data: mocks.messaging.payload.data, @@ -1208,17 +2441,15 @@ describe('Messaging', () => { // Wait for the initial getToken() call to complete before stubbing https.request. return mockApp.INTERNAL.getToken() .then(() => { - httpsRequestStub = sinon.stub(https, 'request'); - httpsRequestStub.callsArgWith(1, mockResponse).returns(mockRequestStream); - + httpsRequestStub = sinon.stub(HttpClient.prototype, 'send').resolves(emptyResponse); return messaging.sendToTopic( mocks.messaging.topicWithPrefix, mocks.messaging.payload, ); }) .then(() => { - expect(requestWriteSpy).to.have.been.calledOnce; - const requestData = JSON.parse(requestWriteSpy.args[0][0]); + expect(httpsRequestStub).to.have.been.calledOnce; + const requestData = httpsRequestStub.args[0][0].data; expect(requestData).to.deep.equal({ to: mocks.messaging.topicWithPrefix, data: mocks.messaging.payload.data, @@ -1269,7 +2500,7 @@ describe('Messaging', () => { it('should throw given no condition argument', () => { expect(() => { - messaging.sendToCondition(undefined as string, mocks.messaging.payloadDataOnly); + messaging.sendToCondition(undefined as any, mocks.messaging.payloadDataOnly); }).to.throw(invalidArgumentError); }); @@ -1327,9 +2558,10 @@ describe('Messaging', () => { _.forEach(STATUS_CODE_TO_ERROR_MAP, (expectedError, statusCode) => { it(`should be rejected given a ${ statusCode } text server response`, () => { mockedRequests.push(mockSendRequestWithError(parseInt(statusCode, 10), 'text')); + disableRetries(messaging); return messaging.sendToCondition( - mocks.messaging.condition, + mocks.messaging.condition, mocks.messaging.payload, ).should.eventually.be.rejected.and.have.property('code', expectedError); }); @@ -1390,17 +2622,15 @@ describe('Messaging', () => { // Wait for the initial getToken() call to complete before stubbing https.request. return mockApp.INTERNAL.getToken() .then(() => { - httpsRequestStub = sinon.stub(https, 'request'); - httpsRequestStub.callsArgWith(1, mockResponse).returns(mockRequestStream); - + httpsRequestStub = sinon.stub(HttpClient.prototype, 'send').resolves(emptyResponse); return messaging.sendToCondition( mocks.messaging.condition, mocks.messaging.payload, ); }) .then(() => { - expect(requestWriteSpy).to.have.been.calledOnce; - const requestData = JSON.parse(requestWriteSpy.args[0][0]); + expect(httpsRequestStub).to.have.been.calledOnce; + const requestData = httpsRequestStub.args[0][0].data; expect(requestData).to.deep.equal({ condition: mocks.messaging.condition, data: mocks.messaging.payload.data, @@ -1442,19 +2672,19 @@ describe('Messaging', () => { invalidPayloads.forEach((invalidPayload) => { it(`should throw given invalid type for payload argument: ${ JSON.stringify(invalidPayload) }`, () => { expect(() => { - messaging.sendToDevice(mocks.messaging.registrationToken, invalidPayload as MessagingPayload); + messaging.sendToDevice(mocks.messaging.registrationToken, invalidPayload as any); }).to.throw('Messaging payload must be an object'); expect(() => { - messaging.sendToDeviceGroup(mocks.messaging.notificationKey, invalidPayload as MessagingPayload); + messaging.sendToDeviceGroup(mocks.messaging.notificationKey, invalidPayload as any); }).to.throw('Messaging payload must be an object'); expect(() => { - messaging.sendToTopic(mocks.messaging.topic, invalidPayload as MessagingPayload); + messaging.sendToTopic(mocks.messaging.topic, invalidPayload as any); }).to.throw('Messaging payload must be an object'); expect(() => { - messaging.sendToCondition(mocks.messaging.condition, invalidPayload as MessagingPayload); + messaging.sendToCondition(mocks.messaging.condition, invalidPayload as any); }).to.throw('Messaging payload must be an object'); }); }); @@ -1578,17 +2808,15 @@ describe('Messaging', () => { // Wait for the initial getToken() call to complete before stubbing https.request. return mockApp.INTERNAL.getToken() .then(() => { - httpsRequestStub = sinon.stub(https, 'request'); - httpsRequestStub.callsArgWith(1, mockResponse).returns(mockRequestStream); - + httpsRequestStub = sinon.stub(HttpClient.prototype, 'send').resolves(emptyResponse); return messaging.sendToDevice( mocks.messaging.registrationToken, mocks.messaging.payload, ); }) .then(() => { - expect(requestWriteSpy).to.have.been.calledOnce; - const requestData = JSON.parse(requestWriteSpy.args[0][0]); + expect(httpsRequestStub).to.have.been.calledOnce; + const requestData = httpsRequestStub.args[0][0].data; expect(requestData).to.have.keys(['to', 'data', 'notification']); expect(requestData.data).to.deep.equal(mocks.messaging.payload.data); expect(requestData.notification).to.deep.equal(mocks.messaging.payload.notification); @@ -1599,9 +2827,7 @@ describe('Messaging', () => { // Wait for the initial getToken() call to complete before stubbing https.request. return mockApp.INTERNAL.getToken() .then(() => { - httpsRequestStub = sinon.stub(https, 'request'); - httpsRequestStub.callsArgWith(1, mockResponse).returns(mockRequestStream); - + httpsRequestStub = sinon.stub(HttpClient.prototype, 'send').resolves(emptyResponse); return messaging.sendToDevice( mocks.messaging.registrationToken, { @@ -1616,8 +2842,8 @@ describe('Messaging', () => { }, ); }).then(() => { - expect(requestWriteSpy).to.have.been.calledOnce; - const requestData = JSON.parse(requestWriteSpy.args[0][0]); + expect(httpsRequestStub).to.have.been.calledOnce; + const requestData = httpsRequestStub.args[0][0].data; expect(requestData.notification).to.deep.equal({ body_loc_args: 'one', body_loc_key: 'two', @@ -1633,9 +2859,7 @@ describe('Messaging', () => { // Wait for the initial getToken() call to complete before stubbing https.request. return mockApp.INTERNAL.getToken() .then(() => { - httpsRequestStub = sinon.stub(https, 'request'); - httpsRequestStub.callsArgWith(1, mockResponse).returns(mockRequestStream); - + httpsRequestStub = sinon.stub(HttpClient.prototype, 'send').resolves(emptyResponse); return messaging.sendToDevice( mocks.messaging.registrationToken, { @@ -1647,8 +2871,8 @@ describe('Messaging', () => { ); }) .then(() => { - expect(requestWriteSpy).to.have.been.calledOnce; - const requestData = JSON.parse(requestWriteSpy.args[0][0]); + expect(httpsRequestStub).to.have.been.calledOnce; + const requestData = httpsRequestStub.args[0][0].data; expect(requestData.notification.body_loc_args).to.equal('foo'); }); }); @@ -1666,6 +2890,21 @@ describe('Messaging', () => { }); }); + const invalidImages = ['', 'a', 'foo', 'image.jpg']; + invalidImages.forEach((imageUrl) => { + it(`should throw given an invalid imageUrl: ${imageUrl}`, () => { + const message: Message = { + condition: 'topic-name', + notification: { + imageUrl, + }, + }; + expect(() => { + messaging.send(message); + }).to.throw('notification.imageUrl must be a valid URL string'); + }); + }); + const invalidTtls = ['', 'abc', '123', '-123s', '1.2.3s', 'As', 's', '1s', -1]; invalidTtls.forEach((ttl) => { it(`should throw given an invalid ttl: ${ ttl }`, () => { @@ -1674,56 +2913,184 @@ describe('Messaging', () => { android: { ttl: (ttl as any), }, - }; - expect(() => { - messaging.send(message); - }).to.throw('TTL must be a non-negative duration in milliseconds'); - }); + }; + expect(() => { + messaging.send(message); + }).to.throw('TTL must be a non-negative duration in milliseconds'); + }); + }); + + const invalidColors = ['', 'foo', '123', '#AABBCX', '112233', '#11223']; + invalidColors.forEach((color) => { + it(`should throw given an invalid color: ${ color }`, () => { + const message: Message = { + condition: 'topic-name', + android: { + notification: { + color, + }, + }, + }; + expect(() => { + messaging.send(message); + }).to.throw('android.notification.color must be in the form #RRGGBB'); + }); + }); + + invalidImages.forEach((imageUrl) => { + it(`should throw given an invalid imageUrl: ${ imageUrl }`, () => { + const message: Message = { + condition: 'topic-name', + android: { + notification: { + imageUrl, + }, + }, + }; + expect(() => { + messaging.send(message); + }).to.throw('android.notification.imageUrl must be a valid URL string'); + }); + }); + + it('should throw given android titleLocArgs without titleLocKey', () => { + const message: Message = { + condition: 'topic-name', + android: { + notification: { + titleLocArgs: ['foo'], + }, + }, + }; + expect(() => { + messaging.send(message); + }).to.throw('titleLocKey is required when specifying titleLocArgs'); + }); + + it('should throw given android bodyLocArgs without bodyLocKey', () => { + const message: Message = { + condition: 'topic-name', + android: { + notification: { + bodyLocArgs: ['foo'], + }, + }, + }; + expect(() => { + messaging.send(message); + }).to.throw('bodyLocKey is required when specifying bodyLocArgs'); + }); + + const invalidVibrateTimings = [[null, 500], [-100]]; + invalidVibrateTimings.forEach((vibrateTimingsMillisMaybeNull) => { + const vibrateTimingsMillis = vibrateTimingsMillisMaybeNull as number[]; + it(`should throw given an null or negative vibrateTimingsMillis: ${ vibrateTimingsMillis }`, () => { + const message: Message = { + condition: 'topic-name', + android: { + notification: { + vibrateTimingsMillis, + }, + }, + }; + expect(() => { + messaging.send(message); + }).to.throw('android.notification.vibrateTimingsMillis must be non-negative durations in milliseconds'); + }); + }); + + it('should throw given an empty vibrateTimingsMillis array', () => { + const message: Message = { + condition: 'topic-name', + android: { + notification: { + vibrateTimingsMillis: [], + }, + }, + }; + expect(() => { + messaging.send(message); + }).to.throw('android.notification.vibrateTimingsMillis must be a non-empty array of numbers'); }); - const invalidColors = ['', 'foo', '123', '#AABBCX', '112233', '#11223']; invalidColors.forEach((color) => { it(`should throw given an invalid color: ${ color }`, () => { const message: Message = { condition: 'topic-name', android: { notification: { - color, + lightSettings: { + color, + lightOnDurationMillis: 100, + lightOffDurationMillis: 800, + }, }, }, }; expect(() => { messaging.send(message); - }).to.throw('android.notification.color must be in the form #RRGGBB'); + }).to.throw('android.notification.lightSettings.color must be in the form #RRGGBB or #RRGGBBAA format'); }); }); - it('should throw given android titleLocArgs without titleLocKey', () => { + it('should throw given a negative light on duration', () => { const message: Message = { condition: 'topic-name', android: { notification: { - titleLocArgs: ['foo'], + lightSettings: { + color: '#aabbcc', + lightOnDurationMillis: -1, + lightOffDurationMillis: 800, + }, }, }, }; expect(() => { messaging.send(message); - }).to.throw('titleLocKey is required when specifying titleLocArgs'); + }).to.throw( + 'android.notification.lightSettings.lightOnDurationMillis must be a non-negative duration in milliseconds'); }); - it('should throw given android bodyLocArgs without bodyLocKey', () => { + it('should throw given a negative light off duration', () => { const message: Message = { condition: 'topic-name', android: { notification: { - bodyLocArgs: ['foo'], + lightSettings: { + color: '#aabbcc', + lightOnDurationMillis: 100, + lightOffDurationMillis: -800, + }, }, }, }; expect(() => { messaging.send(message); - }).to.throw('bodyLocKey is required when specifying bodyLocArgs'); + }).to.throw( + 'android.notification.lightSettings.lightOffDurationMillis must be a non-negative duration in milliseconds'); + }); + + const invalidVolumes = [-0.1, 1.1]; + invalidVolumes.forEach((volume) => { + it(`should throw given invalid apns sound volume: ${volume}`, () => { + const message: Message = { + condition: 'topic-name', + apns: { + payload: { + aps: { + sound: { + name: 'default', + volume, + }, + }, + }, + }, + }; + expect(() => { + messaging.send(message); + }).to.throw('volume must be in the interval [0, 1]'); + }); }); it('should throw given apns titleLocArgs without titleLocKey', () => { @@ -1744,6 +3111,24 @@ describe('Messaging', () => { }).to.throw('titleLocKey is required when specifying titleLocArgs'); }); + it('should throw given apns subtitleLocArgs without subtitleLocKey', () => { + const message: Message = { + condition: 'topic-name', + apns: { + payload: { + aps: { + alert: { + subtitleLocArgs: ['foo'], + }, + }, + }, + }, + }; + expect(() => { + messaging.send(message); + }).to.throw('subtitleLocKey is required when specifying subtitleLocArgs'); + }); + it('should throw given apns locArgs without locKey', () => { const message: Message = { condition: 'topic-name', @@ -1766,41 +3151,67 @@ 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' }); + }).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' }); + }).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' }); + }).to.throw('fcmOptions must be a non-null object'); + }); + }); + + invalidImages.forEach((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'); + }); }); - 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}}}}, + 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 } } } }, ]; invalidDataMessages.forEach((config) => { it(`should throw given data with non-string value: ${config.label}`, () => { @@ -1812,30 +3223,68 @@ describe('Messaging', () => { }); }); - const invalidApnsPayloads: any = [null, '', 'payload', true, 1.23]; + const invalidApnsPayloads: any[] = [null, '', 'payload', true, 1.23]; 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'); }); }); + it('should throw given APNS payload with duplicate fields', () => { + expect(() => { + messaging.send({ + apns: { + payload: { + aps: { 'mutableContent': true, 'mutable-content': 1 }, + }, + }, + token: 'token', + }); + }).to.throw('Multiple specifications for mutableContent in Aps'); + }); - const invalidApnsAlerts: any = [null, [], true, 1.23]; + const invalidApnsAlerts: any[] = [null, [], true, 1.23]; 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'); }); }); + + const invalidApnsSounds: any[] = ['', null, [], true, 1.23]; + 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' }); + }).to.throw('apns.payload.aps.sound must be a non-empty string or a non-null object'); + }); + }); + invalidApnsSounds.forEach((name) => { + it(`should throw given invalid APNS critical sound name: ${name}`, () => { + const message: Message = { + condition: 'topic-name', + apns: { + payload: { + aps: { + sound: { name }, + }, + }, + }, + }; + expect(() => { + messaging.send(message); + }).to.throw('apns.payload.aps.sound.name must be a non-empty string'); + }); + }); }); describe('Options validation', () => { @@ -1890,9 +3339,9 @@ describe('Messaging', () => { const whitelistedOptionsKeys: { [name: string]: { - type: string, - underscoreCasedKey?: string, - }, + type: string; + underscoreCasedKey?: string; + }; } = { dryRun: { type: 'boolean', underscoreCasedKey: 'dry_run' }, priority: { type: 'string' }, @@ -1904,8 +3353,8 @@ describe('Messaging', () => { }; _.forEach(whitelistedOptionsKeys, ({ type, underscoreCasedKey }, camelCasedKey) => { - let validValue; - let invalidValues; + let validValue: any; + let invalidValues: Array<{value: any; text: string}>; if (type === 'string') { invalidValues = [ { value: true, text: 'non-string' }, @@ -1997,9 +3446,7 @@ describe('Messaging', () => { // Wait for the initial getToken() call to complete before stubbing https.request. return mockApp.INTERNAL.getToken() .then(() => { - httpsRequestStub = sinon.stub(https, 'request'); - httpsRequestStub.callsArgWith(1, mockResponse).returns(mockRequestStream); - + httpsRequestStub = sinon.stub(HttpClient.prototype, 'send').resolves(emptyResponse); return messaging.sendToDevice( mocks.messaging.registrationToken, mocks.messaging.payloadDataOnly, @@ -2007,8 +3454,8 @@ describe('Messaging', () => { ); }) .then(() => { - expect(requestWriteSpy).to.have.been.calledOnce; - const requestData = JSON.parse(requestWriteSpy.args[0][0]); + expect(httpsRequestStub).to.have.been.calledOnce; + const requestData = httpsRequestStub.args[0][0].data; expect(requestData).to.have.keys(['to', 'data', 'dry_run', 'collapse_key']); expect(requestData.dry_run).to.equal(mockOptionsClone.dryRun); expect(requestData.collapse_key).to.equal(mockOptionsClone.collapseKey); @@ -2035,6 +3482,22 @@ describe('Messaging', () => { notification: { title: 'test.title', body: 'test.body', + imageUrl: 'https://example.com/image.png', + }, + }, + expectedReq: { + notification: { + title: 'test.title', + body: 'test.body', + image: 'https://example.com/image.png', + }, + }, + }, + { + label: 'Generic fcmOptions message', + req: { + fcmOptions: { + analyticsLabel: 'test.analytics', }, }, }, @@ -2060,6 +3523,26 @@ describe('Messaging', () => { color: '#112233', sound: 'test.sound', tag: 'test.tag', + imageUrl: 'https://example.com/image.png', + ticker: 'test.ticker', + sticky: true, + visibility: 'private', + }, + }, + }, + expectedReq: { + android: { + notification: { + title: 'test.title', + body: 'test.body', + icon: 'test.icon', + color: '#112233', + sound: 'test.sound', + tag: 'test.tag', + image: 'https://example.com/image.png', + ticker: 'test.ticker', + sticky: true, + visibility: 'PRIVATE', }, }, }, @@ -2076,6 +3559,20 @@ describe('Messaging', () => { titleLocArgs: ['arg1', 'arg2'], bodyLocKey: 'body.loc.key', bodyLocArgs: ['arg1', 'arg2'], + channelId: 'test.channel', + eventTimestamp: new Date('2019-10-20T12:00:00-06:30'), + localOnly: true, + priority: 'high', + vibrateTimingsMillis: [100, 50, 250], + defaultVibrateTimings: false, + defaultSound: true, + lightSettings: { + color: '#AABBCCDD', + lightOnDurationMillis: 200, + lightOffDurationMillis: 300, + }, + defaultLightSettings: false, + notificationCount: 1, }, }, }, @@ -2089,6 +3586,25 @@ describe('Messaging', () => { title_loc_args: ['arg1', 'arg2'], body_loc_key: 'body.loc.key', body_loc_args: ['arg1', 'arg2'], + channel_id: 'test.channel', + event_time: '2019-10-20T18:30:00.000Z', + local_only: true, + notification_priority: 'PRIORITY_HIGH', + vibrate_timings: ['0.100000000s', '0.050000000s', '0.250000000s'], + default_vibrate_timings: false, + default_sound: true, + light_settings: { + color: { + red: 0.6666666666666666, + green: 0.7333333333333333, + blue: 0.8, + alpha: 0.8666666666666667, + }, + light_on_duration: '0.200000000s', + light_off_duration: '0.300000000s', + }, + default_light_settings: false, + notification_count: 1, }, }, }, @@ -2131,11 +3647,32 @@ describe('Messaging', () => { color: '#112233', sound: 'test.sound', tag: 'test.tag', + imageUrl: 'https://example.com/image.png', clickAction: 'test.click.action', titleLocKey: 'title.loc.key', titleLocArgs: ['arg1', 'arg2'], bodyLocKey: 'body.loc.key', bodyLocArgs: ['arg1', 'arg2'], + channelId: 'test.channel', + ticker: 'test.ticker', + sticky: true, + visibility: 'private', + eventTimestamp: new Date('2019-10-20T12:00:00-06:30'), + localOnly: true, + priority: 'high', + vibrateTimingsMillis: [100, 50, 250], + defaultVibrateTimings: false, + defaultSound: true, + lightSettings: { + color: '#AABBCC', + lightOnDurationMillis: 200, + lightOffDurationMillis: 300, + }, + defaultLightSettings: false, + notificationCount: 1, + }, + fcmOptions: { + analyticsLabel: 'test.analytics', }, }, }, @@ -2156,11 +3693,37 @@ describe('Messaging', () => { color: '#112233', sound: 'test.sound', tag: 'test.tag', + image: 'https://example.com/image.png', click_action: 'test.click.action', title_loc_key: 'title.loc.key', title_loc_args: ['arg1', 'arg2'], body_loc_key: 'body.loc.key', body_loc_args: ['arg1', 'arg2'], + channel_id: 'test.channel', + ticker: 'test.ticker', + sticky: true, + visibility: 'PRIVATE', + event_time: '2019-10-20T18:30:00.000Z', + local_only: true, + notification_priority: 'PRIORITY_HIGH', + vibrate_timings: ['0.100000000s', '0.050000000s', '0.250000000s'], + default_vibrate_timings: false, + default_sound: true, + light_settings: { + color: { + red: 0.6666666666666666, + green: 0.7333333333333333, + blue: 0.8, + alpha: 1, + }, + light_on_duration: '0.200000000s', + light_off_duration: '0.300000000s', + }, + default_light_settings: false, + notification_count: 1, + }, + fcmOptions: { + analyticsLabel: 'test.analytics', }, }, }, @@ -2204,6 +3767,25 @@ describe('Messaging', () => { title: 'test.title', body: 'test.body', icon: 'test.icon', + actions: [{ + action: 'test.action.1', + title: 'test.action.1.title', + icon: 'test.action.1.icon', + }, { + action: 'test.action.2', + title: 'test.action.2.title', + icon: 'test.action.2.icon', + }], + badge: 'test.badge', + data: { + key: 'value', + }, + dir: 'auto', + image: 'test.image', + requireInteraction: true, + }, + fcmOptions: { + link: 'https://example.com', }, }, }, @@ -2242,20 +3824,31 @@ describe('Messaging', () => { payload: { aps: { alert: { + title: 'title', + subtitle: 'subtitle', + body: 'body', titleLocKey: 'title.loc.key', titleLocArgs: ['arg1', 'arg2'], + subtitleLocKey: 'subtitle.loc.key', + subtitleLocArgs: ['arg1', 'arg2'], locKey: 'body.loc.key', locArgs: ['arg1', 'arg2'], actionLocKey: 'action.loc.key', + launchImage: 'image', }, badge: 42, sound: 'test.sound', category: 'test.category', contentAvailable: true, + mutableContent: true, threadId: 'thread.id', }, customKey1: 'custom.value', - customKey2: {nested: 'value'}, + customKey2: { nested: 'value' }, + }, + fcmOptions: { + analyticsLabel: 'test.analytics', + imageUrl: 'https://example.com/image.png', }, }, }, @@ -2268,20 +3861,113 @@ describe('Messaging', () => { payload: { aps: { 'alert': { + 'title': 'title', + 'subtitle': 'subtitle', + 'body': 'body', 'title-loc-key': 'title.loc.key', 'title-loc-args': ['arg1', 'arg2'], + 'subtitle-loc-key': 'subtitle.loc.key', + 'subtitle-loc-args': ['arg1', 'arg2'], 'loc-key': 'body.loc.key', 'loc-args': ['arg1', 'arg2'], 'action-loc-key': 'action.loc.key', + 'launch-image': 'image', }, 'badge': 42, 'sound': 'test.sound', 'category': 'test.category', 'content-available': 1, + 'mutable-content': 1, 'thread-id': 'thread.id', }, customKey1: 'custom.value', - customKey2: {nested: 'value'}, + customKey2: { nested: 'value' }, + }, + fcmOptions: { + analyticsLabel: 'test.analytics', + image: 'https://example.com/image.png', + }, + }, + }, + }, + { + label: 'APNS critical sound', + req: { + apns: { + payload: { + aps: { + sound: { + critical: true, + name: 'test.sound', + volume: 0.5, + }, + }, + }, + }, + }, + expectedReq: { + apns: { + payload: { + aps: { + sound: { + critical: 1, + name: 'test.sound', + volume: 0.5, + }, + }, + }, + }, + }, + }, + { + label: 'APNS critical sound name only', + req: { + apns: { + payload: { + aps: { + sound: { + name: 'test.sound', + }, + }, + }, + }, + }, + expectedReq: { + apns: { + payload: { + aps: { + sound: { + name: 'test.sound', + }, + }, + }, + }, + }, + }, + { + label: 'APNS critical sound explicitly false', + req: { + apns: { + payload: { + aps: { + sound: { + critical: false, + name: 'test.sound', + volume: 0.5, + }, + }, + }, + }, + }, + expectedReq: { + apns: { + payload: { + aps: { + sound: { + name: 'test.sound', + volume: 0.5, + }, + }, }, }, }, @@ -2305,6 +3991,67 @@ describe('Messaging', () => { }, }, }, + { + label: 'APNS content-available set explicitly', + req: { + apns: { + payload: { + aps: { + 'content-available': 1, + }, + }, + }, + }, + expectedReq: { + apns: { + payload: { + aps: { 'content-available': 1 }, + }, + }, + }, + }, + { + label: 'APNS mutableContent explicitly false', + req: { + apns: { + payload: { + aps: { + mutableContent: false, + }, + }, + }, + }, + expectedReq: { + apns: { + payload: { + aps: {}, + }, + }, + }, + }, + { + label: 'APNS custom fields', + req: { + apns: { + payload: { + aps: { + k1: 'v1', + k2: true, + }, + }, + }, + }, + expectedReq: { + apns: { + payload: { + aps: { + k1: 'v1', + k2: true, + }, + }, + }, + }, + }, ]; validMessages.forEach((config) => { @@ -2312,18 +4059,22 @@ describe('Messaging', () => { // Wait for the initial getToken() call to complete before stubbing https.request. return mockApp.INTERNAL.getToken() .then(() => { - httpsRequestStub = sinon.stub(https, 'request'); - httpsRequestStub.callsArgWith(1, mockResponse).returns(mockRequestStream); + const resp = utils.responseFrom({ message: 'test' }); + httpsRequestStub = sinon.stub(HttpClient.prototype, 'send').resolves(resp); const req = config.req; req.token = 'mock-token'; return messaging.send(req); }) .then(() => { - expect(requestWriteSpy).to.have.been.calledOnce; - const requestData = JSON.parse(requestWriteSpy.args[0][0]); const expectedReq = config.expectedReq || config.req; expectedReq.token = 'mock-token'; - expect(requestData.message).to.deep.equal(expectedReq); + expect(httpsRequestStub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + data: { message: expectedReq }, + timeout: 15000, + url: 'https://fcm.googleapis.com/v1/projects/project_id/messages:send', + headers: expectedHeaders, + }); }); }); }); @@ -2331,14 +4082,14 @@ describe('Messaging', () => { it('should not throw when the message is addressed to the prefixed topic name', () => { return mockApp.INTERNAL.getToken() .then(() => { - httpsRequestStub = sinon.stub(https, 'request'); - httpsRequestStub.callsArgWith(1, mockResponse).returns(mockRequestStream); - return messaging.send({topic: '/topics/mock-topic'}); + const resp = utils.responseFrom({ message: 'test' }); + httpsRequestStub = sinon.stub(HttpClient.prototype, 'send').resolves(resp); + return messaging.send({ topic: '/topics/mock-topic' }); }) .then(() => { - expect(requestWriteSpy).to.have.been.calledOnce; - const requestData = JSON.parse(requestWriteSpy.args[0][0]); - const expectedReq = {topic: 'mock-topic'}; + expect(httpsRequestStub).to.have.been.calledOnce; + const requestData = httpsRequestStub.args[0][0].data; + const expectedReq = { topic: 'mock-topic' }; expect(requestData.message).to.deep.equal(expectedReq); }); }); @@ -2347,9 +4098,7 @@ describe('Messaging', () => { // Wait for the initial getToken() call to complete before stubbing https.request. return mockApp.INTERNAL.getToken() .then(() => { - httpsRequestStub = sinon.stub(https, 'request'); - httpsRequestStub.callsArgWith(1, mockResponse).returns(mockRequestStream); - + httpsRequestStub = sinon.stub(HttpClient.prototype, 'send').resolves(emptyResponse); return messaging.sendToDevice( mocks.messaging.registrationToken, mocks.messaging.payloadDataOnly, @@ -2365,8 +4114,8 @@ describe('Messaging', () => { ); }) .then(() => { - expect(requestWriteSpy).to.have.been.calledOnce; - const requestData = JSON.parse(requestWriteSpy.args[0][0]); + expect(httpsRequestStub).to.have.been.calledOnce; + const requestData = httpsRequestStub.args[0][0].data; expect(requestData).to.have.keys([ 'to', 'data', 'dry_run', 'time_to_live', 'collapse_key', 'mutable_content', 'content_available', 'restricted_package_name', 'otherKey', @@ -2378,9 +4127,7 @@ describe('Messaging', () => { // Wait for the initial getToken() call to complete before stubbing https.request. return mockApp.INTERNAL.getToken() .then(() => { - httpsRequestStub = sinon.stub(https, 'request'); - httpsRequestStub.callsArgWith(1, mockResponse).returns(mockRequestStream); - + httpsRequestStub = sinon.stub(HttpClient.prototype, 'send').resolves(emptyResponse); return messaging.sendToDevice( mocks.messaging.registrationToken, mocks.messaging.payloadDataOnly, @@ -2391,8 +4138,8 @@ describe('Messaging', () => { ); }) .then(() => { - expect(requestWriteSpy).to.have.been.calledOnce; - const requestData = JSON.parse(requestWriteSpy.args[0][0]); + expect(httpsRequestStub).to.have.been.calledOnce; + const requestData = httpsRequestStub.args[0][0].data; expect(requestData.dry_run).to.be.true; }); }); @@ -2412,7 +4159,7 @@ describe('Messaging', () => { }); }); - function tokenSubscriptionTests(methodName) { + function tokenSubscriptionTests(methodName: string): void { const invalidRegistrationTokensArgumentError = 'Registration token(s) provided to ' + `${methodName}() must be a non-empty string or a non-empty array`; @@ -2421,36 +4168,36 @@ describe('Messaging', () => { it('should throw given invalid type for registration token(s) argument: ' + JSON.stringify(invalidRegistrationToken), () => { expect(() => { - messaging[methodName](invalidRegistrationToken as string, mocks.messaging.topic); + messagingService[methodName](invalidRegistrationToken as string, mocks.messaging.topic); }).to.throw(invalidRegistrationTokensArgumentError); }); }); it('should throw given no registration token(s) argument', () => { expect(() => { - messaging[methodName](undefined as string, mocks.messaging.topic); + messagingService[methodName](undefined as any, mocks.messaging.topic); }).to.throw(invalidRegistrationTokensArgumentError); }); it('should throw given empty string for registration token(s) argument', () => { expect(() => { - messaging[methodName]('', mocks.messaging.topic); + messagingService[methodName]('', mocks.messaging.topic); }).to.throw(invalidRegistrationTokensArgumentError); }); it('should throw given empty array for registration token(s) argument', () => { expect(() => { - messaging[methodName]([], mocks.messaging.topic); + messagingService[methodName]([], mocks.messaging.topic); }).to.throw(invalidRegistrationTokensArgumentError); }); it('should be rejected given empty string within array for registration token(s) argument', () => { - return messaging[methodName](['foo', 'bar', ''], mocks.messaging.topic) + return messagingService[methodName](['foo', 'bar', ''], mocks.messaging.topic) .should.eventually.be.rejected.and.have.property('code', 'messaging/invalid-argument'); }); it('should be rejected given non-string value within array for registration token(s) argument', () => { - return messaging[methodName](['foo', true as any, 'bar'], mocks.messaging.topic) + return messagingService[methodName](['foo', true as any, 'bar'], mocks.messaging.topic) .should.eventually.be.rejected.and.have.property('code', 'messaging/invalid-argument'); }); @@ -2460,12 +4207,12 @@ describe('Messaging', () => { // Create an array of exactly 1,000 registration tokens const registrationTokens = (Array(1000) as any).fill(mocks.messaging.registrationToken); - return messaging[methodName](registrationTokens, mocks.messaging.topic) + return messagingService[methodName](registrationTokens, mocks.messaging.topic) .then(() => { // Push the array of registration tokens over 1,000 items registrationTokens.push(mocks.messaging.registrationToken); - return messaging[methodName](registrationTokens, mocks.messaging.topic) + return messagingService[methodName](registrationTokens, mocks.messaging.topic) .should.eventually.be.rejected.and.have.property('code', 'messaging/invalid-argument'); }); }); @@ -2476,27 +4223,27 @@ describe('Messaging', () => { invalidTopics.forEach((invalidTopic) => { it(`should throw given invalid type for topic argument: ${ JSON.stringify(invalidTopic) }`, () => { expect(() => { - messaging[methodName](mocks.messaging.registrationToken, invalidTopic as string); + messagingService[methodName](mocks.messaging.registrationToken, invalidTopic as string); }).to.throw(invalidTopicArgumentError); }); }); it('should throw given no topic argument', () => { expect(() => { - messaging[methodName](mocks.messaging.registrationToken, undefined as string); + messagingService[methodName](mocks.messaging.registrationToken, undefined as any); }).to.throw(invalidTopicArgumentError); }); it('should throw given empty string for topic argument', () => { expect(() => { - messaging[methodName](mocks.messaging.registrationToken, ''); + messagingService[methodName](mocks.messaging.registrationToken, ''); }).to.throw(invalidTopicArgumentError); }); const topicsWithInvalidCharacters = ['f*o*o', '/topics/f+o+o', 'foo/topics/foo', '$foo', '/topics/foo&']; topicsWithInvalidCharacters.forEach((invalidTopic) => { it(`should be rejected given topic argument which has invalid characters: ${ invalidTopic }`, () => { - return messaging[methodName](mocks.messaging.registrationToken, invalidTopic) + return messagingService[methodName](mocks.messaging.registrationToken, invalidTopic) .should.eventually.be.rejected.and.have.property('code', 'messaging/invalid-argument'); }); }); @@ -2504,7 +4251,7 @@ describe('Messaging', () => { it('should be rejected given a 200 JSON server response with a known error', () => { mockedRequests.push(mockTopicSubscriptionRequestWithError(methodName, 200, 'json')); - return messaging[methodName]( + return messagingService[methodName]( mocks.messaging.registrationToken, mocks.messaging.topic, ).should.eventually.be.rejected.and.have.property('code', expectedErrorCodes.json); @@ -2513,7 +4260,7 @@ describe('Messaging', () => { it('should be rejected given a 200 JSON server response with an unknown error', () => { mockedRequests.push(mockTopicSubscriptionRequestWithError(methodName, 200, 'json', { error: 'Unknown' })); - return messaging[methodName]( + return messagingService[methodName]( mocks.messaging.registrationToken, mocks.messaging.topic, ).should.eventually.be.rejected.and.have.property('code', expectedErrorCodes.unknownError); @@ -2522,7 +4269,7 @@ describe('Messaging', () => { it('should be rejected given a non-2xx JSON server response', () => { mockedRequests.push(mockTopicSubscriptionRequestWithError(methodName, 400, 'json')); - return messaging[methodName]( + return messagingService[methodName]( mocks.messaging.registrationToken, mocks.messaging.topic, ).should.eventually.be.rejected.and.have.property('code', expectedErrorCodes.json); @@ -2531,7 +4278,7 @@ describe('Messaging', () => { it('should be rejected given a non-2xx JSON server response with an unknown error', () => { mockedRequests.push(mockTopicSubscriptionRequestWithError(methodName, 400, 'json', { error: 'Unknown' })); - return messaging[methodName]( + return messagingService[methodName]( mocks.messaging.registrationToken, mocks.messaging.topic, ).should.eventually.be.rejected.and.have.property('code', expectedErrorCodes.unknownError); @@ -2540,7 +4287,7 @@ describe('Messaging', () => { it('should be rejected given a non-2xx JSON server response without an error', () => { mockedRequests.push(mockTopicSubscriptionRequestWithError(methodName, 400, 'json', { foo: 'bar' })); - return messaging[methodName]( + return messagingService[methodName]( mocks.messaging.registrationToken, mocks.messaging.topic, ).should.eventually.be.rejected.and.have.property('code', expectedErrorCodes.unknownError); @@ -2549,8 +4296,9 @@ describe('Messaging', () => { _.forEach(STATUS_CODE_TO_ERROR_MAP, (expectedError, statusCode) => { it(`should be rejected given a ${ statusCode } text server response`, () => { mockedRequests.push(mockTopicSubscriptionRequestWithError(methodName, parseInt(statusCode, 10), 'text')); + disableRetries(messaging); - return messaging[methodName]( + return messagingService[methodName]( mocks.messaging.registrationToken, mocks.messaging.topic, ).should.eventually.be.rejected.and.have.property('code', expectedError); @@ -2558,21 +4306,21 @@ describe('Messaging', () => { }); it('should be rejected given an app which returns null access tokens', () => { - return nullAccessTokenMessaging[methodName]( + return nullAccessTokenMessagingService[methodName]( mocks.messaging.registrationToken, mocks.messaging.topic, ).should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); }); it('should be rejected given an app which returns invalid access tokens', () => { - return nullAccessTokenMessaging[methodName]( + return nullAccessTokenMessagingService[methodName]( mocks.messaging.registrationToken, mocks.messaging.topic, ).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 nullAccessTokenMessaging[methodName]( + return nullAccessTokenMessagingService[methodName]( mocks.messaging.registrationToken, mocks.messaging.topic, ).should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); @@ -2582,7 +4330,7 @@ describe('Messaging', () => { 'with "/topics/")', () => { mockedRequests.push(mockTopicSubscriptionRequest(methodName, /* successCount */ 1)); - return messaging[methodName]( + return messagingService[methodName]( mocks.messaging.registrationToken, mocks.messaging.topic, ); @@ -2592,7 +4340,7 @@ describe('Messaging', () => { 'with "/topics/")', () => { mockedRequests.push(mockTopicSubscriptionRequest(methodName, /* successCount */ 1)); - return messaging[methodName]( + return messagingService[methodName]( mocks.messaging.registrationToken, mocks.messaging.topicWithPrefix, ); @@ -2602,7 +4350,7 @@ describe('Messaging', () => { 'prefixed with "/topics/")', () => { mockedRequests.push(mockTopicSubscriptionRequest(methodName, /* successCount */ 3)); - return messaging[methodName]( + return messagingService[methodName]( [ mocks.messaging.registrationToken + '0', mocks.messaging.registrationToken + '1', @@ -2616,7 +4364,7 @@ describe('Messaging', () => { 'prefixed with "/topics/")', () => { mockedRequests.push(mockTopicSubscriptionRequest(methodName, /* successCount */ 3)); - return messaging[methodName]( + return messagingService[methodName]( [ mocks.messaging.registrationToken + '0', mocks.messaging.registrationToken + '1', @@ -2630,7 +4378,7 @@ describe('Messaging', () => { '(topic name not prefixed with "/topics/")', () => { mockedRequests.push(mockTopicSubscriptionRequest(methodName, /* successCount */ 1)); - return messaging[methodName]( + return messagingService[methodName]( mocks.messaging.registrationToken, mocks.messaging.topic, ).should.eventually.deep.equal({ @@ -2644,7 +4392,7 @@ describe('Messaging', () => { '(topic name prefixed with "/topics/")', () => { mockedRequests.push(mockTopicSubscriptionRequest(methodName, /* successCount */ 1)); - return messaging[methodName]( + return messagingService[methodName]( mocks.messaging.registrationToken, mocks.messaging.topicWithPrefix, ).should.eventually.deep.equal({ @@ -2658,7 +4406,7 @@ describe('Messaging', () => { 'and topic (topic name not prefixed with "/topics/")', () => { mockedRequests.push(mockTopicSubscriptionRequest(methodName, /* successCount */ 1, /* failureCount */ 2)); - return messaging[methodName]( + return messagingService[methodName]( [ mocks.messaging.registrationToken + '0', mocks.messaging.registrationToken + '1', @@ -2683,7 +4431,7 @@ describe('Messaging', () => { 'and topic (topic name prefixed with "/topics/")', () => { mockedRequests.push(mockTopicSubscriptionRequest(methodName, /* successCount */ 1, /* failureCount */ 2)); - return messaging[methodName]( + return messagingService[methodName]( [ mocks.messaging.registrationToken + '0', mocks.messaging.registrationToken + '1', @@ -2709,21 +4457,19 @@ describe('Messaging', () => { // Wait for the initial getToken() call to complete before stubbing https.request. return mockApp.INTERNAL.getToken() .then(() => { - httpsRequestStub = sinon.stub(https, 'request'); - httpsRequestStub.callsArgWith(1, mockResponse).returns(mockRequestStream); - - return messaging[methodName]( + httpsRequestStub = sinon.stub(HttpClient.prototype, 'send').resolves(emptyResponse); + return messagingService[methodName]( mocks.messaging.registrationToken, mocks.messaging.topic, ); }) .then(() => { - expect(requestWriteSpy).to.have.been.calledOnce; - const requestData = JSON.parse(requestWriteSpy.args[0][0]); - expect(requestData).to.deep.equal({ + const expectedReq = { to: mocks.messaging.topicWithPrefix, registration_tokens: [mocks.messaging.registrationToken], - }); + }; + expect(httpsRequestStub).to.have.been.calledOnce; + expect(httpsRequestStub.args[0][0].data).to.deep.equal(expectedReq); }); }); @@ -2732,21 +4478,19 @@ describe('Messaging', () => { // Wait for the initial getToken() call to complete before stubbing https.request. return mockApp.INTERNAL.getToken() .then(() => { - httpsRequestStub = sinon.stub(https, 'request'); - httpsRequestStub.callsArgWith(1, mockResponse).returns(mockRequestStream); - - return messaging[methodName]( + httpsRequestStub = sinon.stub(HttpClient.prototype, 'send').resolves(emptyResponse); + return messagingService[methodName]( mocks.messaging.registrationToken, mocks.messaging.topicWithPrefix, ); }) .then(() => { - expect(requestWriteSpy).to.have.been.calledOnce; - const requestData = JSON.parse(requestWriteSpy.args[0][0]); - expect(requestData).to.deep.equal({ + const expectedReq = { to: mocks.messaging.topicWithPrefix, registration_tokens: [mocks.messaging.registrationToken], - }); + }; + expect(httpsRequestStub).to.have.been.calledOnce; + expect(httpsRequestStub.args[0][0].data).to.deep.equal(expectedReq); }); }); @@ -2761,21 +4505,19 @@ describe('Messaging', () => { // Wait for the initial getToken() call to complete before stubbing https.request. return mockApp.INTERNAL.getToken() .then(() => { - httpsRequestStub = sinon.stub(https, 'request'); - httpsRequestStub.callsArgWith(1, mockResponse).returns(mockRequestStream); - - return messaging[methodName]( + httpsRequestStub = sinon.stub(HttpClient.prototype, 'send').resolves(emptyResponse); + return messagingService[methodName]( registrationTokens, mocks.messaging.topic, ); }) .then(() => { - expect(requestWriteSpy).to.have.been.calledOnce; - const requestData = JSON.parse(requestWriteSpy.args[0][0]); - expect(requestData).to.deep.equal({ + const expectedReq = { to: mocks.messaging.topicWithPrefix, registration_tokens: registrationTokens, - }); + }; + expect(httpsRequestStub).to.have.been.calledOnce; + expect(httpsRequestStub.args[0][0].data).to.deep.equal(expectedReq); }); }); @@ -2790,21 +4532,19 @@ describe('Messaging', () => { // Wait for the initial getToken() call to complete before stubbing https.request. return mockApp.INTERNAL.getToken() .then(() => { - httpsRequestStub = sinon.stub(https, 'request'); - httpsRequestStub.callsArgWith(1, mockResponse).returns(mockRequestStream); - - return messaging[methodName]( + httpsRequestStub = sinon.stub(HttpClient.prototype, 'send').resolves(emptyResponse); + return messagingService[methodName]( registrationTokens, mocks.messaging.topicWithPrefix, ); }) .then(() => { - expect(requestWriteSpy).to.have.been.calledOnce; - const requestData = JSON.parse(requestWriteSpy.args[0][0]); - expect(requestData).to.deep.equal({ + const expectedReq = { to: mocks.messaging.topicWithPrefix, registration_tokens: registrationTokens, - }); + }; + expect(httpsRequestStub).to.have.been.calledOnce; + expect(httpsRequestStub.args[0][0].data).to.deep.equal(expectedReq); }); }); } @@ -2816,10 +4556,4 @@ describe('Messaging', () => { describe('unsubscribeFromTopic()', () => { tokenSubscriptionTests('unsubscribeFromTopic'); }); - - describe('INTERNAL.delete()', () => { - it('should delete Messaging instance', () => { - messaging.INTERNAL.delete().should.eventually.be.fulfilled; - }); - }); }); diff --git a/test/unit/project-management/android-app.spec.ts b/test/unit/project-management/android-app.spec.ts new file mode 100644 index 0000000000..94ccacbbd3 --- /dev/null +++ b/test/unit/project-management/android-app.spec.ts @@ -0,0 +1,415 @@ +/*! + * Copyright 2018 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 _ from 'lodash'; +import * as sinon from 'sinon'; +import { FirebaseApp } from '../../../src/app/firebase-app'; +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'; +import { + AndroidApp, AndroidAppMetadata, AppPlatform, ShaCertificate, +} from '../../../src/project-management/index'; + +const expect = chai.expect; + +const APP_ID = 'test-app-id'; +const EXPECTED_ERROR = new FirebaseProjectManagementError('internal-error', 'message'); + +const VALID_SHA_1_HASH = '0123456789abcdefABCDEF012345678901234567'; +const VALID_SHA_256_HASH = '0123456789abcdefABCDEF01234567890123456701234567890123456789abcd'; + +describe('AndroidApp', () => { + // Stubs used to simulate underlying api calls. + let stubs: sinon.SinonStub[] = []; + + let androidApp: AndroidApp; + let requestHandler: ProjectManagementRequestHandler; + let mockApp: FirebaseApp; + + beforeEach(() => { + mockApp = mocks.app(); + requestHandler = new ProjectManagementRequestHandler(mockApp); + androidApp = new AndroidApp(APP_ID, requestHandler); + }); + + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + return mockApp.delete(); + }); + + describe('Constructor', () => { + const invalidAppIds = [null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidAppIds.forEach((invalidAppId) => { + it('should throw given invalid app ID: ' + JSON.stringify(invalidAppId), () => { + expect(() => { + const androidAppAny: any = AndroidApp; + return new androidAppAny(invalidAppId); + }).to.throw('appId must be a non-empty string.'); + }); + }); + + it('should throw given no appId', () => { + expect(() => { + const androidAppAny: any = AndroidApp; + return new androidAppAny(); + }).to.throw('appId must be a non-empty string.'); + }); + + it('should not throw given a valid app ID', () => { + expect(() => { + return new AndroidApp(APP_ID, requestHandler); + }).not.to.throw(); + }); + }); + + describe('getMetadata', () => { + const VALID_ANDROID_APP_METADATA_API_RESPONSE = { + name: 'test-resource-name', + appId: APP_ID, + displayName: 'test-display-name', + projectId: 'test-project-id', + packageName: 'test-package-name', + }; + + const VALID_ANDROID_APP_METADATA: AndroidAppMetadata = { + platform: AppPlatform.ANDROID, + resourceName: VALID_ANDROID_APP_METADATA_API_RESPONSE.name, + appId: APP_ID, + displayName: VALID_ANDROID_APP_METADATA_API_RESPONSE.displayName, + projectId: VALID_ANDROID_APP_METADATA_API_RESPONSE.projectId, + packageName: VALID_ANDROID_APP_METADATA_API_RESPONSE.packageName, + }; + + it('should propagate API errors', () => { + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'getResource') + .returns(Promise.reject(EXPECTED_ERROR)); + stubs.push(stub); + return androidApp.getMetadata().should.eventually.be.rejected.and.equal(EXPECTED_ERROR); + }); + + it('should throw with null API response', () => { + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'getResource') + .resolves(null as any); + stubs.push(stub); + return androidApp.getMetadata() + .should.eventually.be.rejected + .and.have.property( + 'message', + 'getMetadata()\'s responseData must be a non-null object. Response data: null'); + }); + + const requiredFieldsList = ['name', 'appId', 'projectId', 'packageName']; + requiredFieldsList.forEach((requiredField) => { + it(`should throw with API response missing ${requiredField}`, () => { + const partialApiResponse: any = deepCopy(VALID_ANDROID_APP_METADATA_API_RESPONSE); + delete partialApiResponse[requiredField]; + + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'getResource') + .returns(Promise.resolve(partialApiResponse)); + stubs.push(stub); + return androidApp.getMetadata() + .should.eventually.be.rejected + .and.have.property( + 'message', + `getMetadata()'s responseData.${requiredField} must be a non-empty string. ` + + `Response data: ${JSON.stringify(partialApiResponse, null, 2)}`); + }); + }); + + it('should resolve with metadata on success', () => { + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'getResource') + .returns(Promise.resolve(VALID_ANDROID_APP_METADATA_API_RESPONSE)); + stubs.push(stub); + return androidApp.getMetadata().should.eventually.deep.equal(VALID_ANDROID_APP_METADATA); + }); + }); + + describe('setDisplayName', () => { + const newDisplayName = 'test-new-display-name'; + + it('should propagate API errors', () => { + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'setDisplayName') + .returns(Promise.reject(EXPECTED_ERROR)); + stubs.push(stub); + return androidApp.setDisplayName(newDisplayName) + .should.eventually.be.rejected.and.equal(EXPECTED_ERROR); + }); + + it('should resolve on success', () => { + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'setDisplayName') + .returns(Promise.resolve()); + stubs.push(stub); + return androidApp.setDisplayName(newDisplayName).should.eventually.be.fulfilled; + }); + }); + + describe('getShaCertificates', () => { + const testResourceName1 = 'test-resource-name-1'; + const testResourceName2 = 'test-resource-name-2'; + const VALID_ANDROID_CERTS_API_RESPONSE = { + certificates: [ + { + name: testResourceName1, + shaHash: VALID_SHA_1_HASH, + }, + { + name: testResourceName2, + shaHash: VALID_SHA_256_HASH, + }, + ], + }; + + const VALID_ANDROID_CERTS: ShaCertificate[] = [ + new ShaCertificate(VALID_SHA_1_HASH, testResourceName1), + new ShaCertificate(VALID_SHA_256_HASH, testResourceName2), + ]; + + it('should propagate API errors', () => { + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'getAndroidShaCertificates') + .returns(Promise.reject(EXPECTED_ERROR)); + stubs.push(stub); + return androidApp.getShaCertificates() + .should.eventually.be.rejected.and.equal(EXPECTED_ERROR); + }); + + it('should throw with null API response', () => { + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'getAndroidShaCertificates') + .resolves(null as any); + stubs.push(stub); + return androidApp.getShaCertificates() + .should.eventually.be.rejected + .and.have.property( + 'message', + 'getShaCertificates()\'s responseData must be a non-null object. Response data: ' + + 'null'); + }); + + it('should return empty array when API response missing "certificates" field', () => { + const partialApiResponse = {}; + + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'getAndroidShaCertificates') + .returns(Promise.resolve(partialApiResponse)); + stubs.push(stub); + return androidApp.getShaCertificates() + .should.eventually.deep.equal([]); + }); + + it('should throw when API response has non-array "certificates" field', () => { + const partialApiResponse = { certificates: 'none' }; + + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'getAndroidShaCertificates') + .returns(Promise.resolve(partialApiResponse)); + stubs.push(stub); + return androidApp.getShaCertificates() + .should.eventually.be.rejected + .and.have.property( + 'message', + '"certificates" field must be present in the getShaCertificates() response data. ' + + 'Response data: ' + JSON.stringify(partialApiResponse, null, 2)); + }); + + const requiredFieldsList = ['name', 'shaHash']; + requiredFieldsList.forEach((requiredField) => { + it(`should throw with API response missing "certificates[].${requiredField}" field`, () => { + const partialApiResponse: any = deepCopy(VALID_ANDROID_CERTS_API_RESPONSE); + delete partialApiResponse.certificates[1][requiredField]; + + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'getAndroidShaCertificates') + .returns(Promise.resolve(partialApiResponse)); + stubs.push(stub); + return androidApp.getShaCertificates() + .should.eventually.be.rejected + .and.have.property( + 'message', + `getShaCertificates()'s responseData.certificates[].${requiredField} must be a ` + + 'non-empty string. Response data: ' + + JSON.stringify(partialApiResponse, null, 2)); + }); + }); + + it('should resolve with metadata on success', () => { + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'getAndroidShaCertificates') + .returns(Promise.resolve(VALID_ANDROID_CERTS_API_RESPONSE)); + stubs.push(stub); + return androidApp.getShaCertificates() + .should.eventually.deep.equal(VALID_ANDROID_CERTS); + }); + }); + + describe('addShaCertificate', () => { + const certificateToAdd = new ShaCertificate(VALID_SHA_1_HASH); + + it('should propagate API errors', () => { + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'addAndroidShaCertificate') + .returns(Promise.reject(EXPECTED_ERROR)); + stubs.push(stub); + return androidApp.addShaCertificate(certificateToAdd) + .should.eventually.be.rejected.and.equal(EXPECTED_ERROR); + }); + + it('should resolve on success', () => { + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'addAndroidShaCertificate') + .returns(Promise.resolve()); + stubs.push(stub); + return androidApp.addShaCertificate(certificateToAdd).should.eventually.be.fulfilled; + }); + }); + + describe('deleteShaCertificate', () => { + const certificateToDelete = new ShaCertificate(VALID_SHA_1_HASH); + const certificateToDeleteWithResourceName = + new ShaCertificate(VALID_SHA_1_HASH, 'resource/name'); + + it('should propagate API errors', () => { + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'deleteResource') + .returns(Promise.reject(EXPECTED_ERROR)); + stubs.push(stub); + return androidApp + .deleteShaCertificate(certificateToDeleteWithResourceName) + .should.eventually.be.rejected.and.equal(EXPECTED_ERROR); + }); + + it('should fail on certificate without resourceName', () => { + expect(() => androidApp.deleteShaCertificate(certificateToDelete)) + .to.throw(FirebaseProjectManagementError) + .with.property('code', 'project-management/invalid-argument'); + }); + + it('should resolve on success', () => { + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'deleteResource') + .returns(Promise.resolve()); + stubs.push(stub); + return androidApp + .deleteShaCertificate(certificateToDeleteWithResourceName) + .should.eventually.be.fulfilled; + }); + }); + + describe('getConfig', () => { + const VALID_ANDROID_CONFIG_API_RESPONSE = { + configFileContents: 'QmFzZTY0IHRlc3Qu', + }; + const VALID_ANDROID_CONFIG = 'Base64 test.'; + + it('should propagate API errors', () => { + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'getConfig') + .returns(Promise.reject(EXPECTED_ERROR)); + stubs.push(stub); + return androidApp.getConfig().should.eventually.be.rejected.and.equal(EXPECTED_ERROR); + }); + + it('should throw with null API response', () => { + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'getConfig') + .resolves(null as any); + stubs.push(stub); + return androidApp.getConfig() + .should.eventually.be.rejected + .and.have.property( + 'message', + 'getConfig()\'s responseData must be a non-null object. Response data: null'); + }); + + it('should throw with non-base64 response.configFileContents', () => { + const apiResponse = deepCopy(VALID_ANDROID_CONFIG_API_RESPONSE); + apiResponse.configFileContents = '1' + apiResponse.configFileContents; + + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'getConfig') + .returns(Promise.resolve(apiResponse)); + stubs.push(stub); + return androidApp.getConfig() + .should.eventually.be.rejected + .and.have.property( + 'message', + 'getConfig()\'s responseData.configFileContents must be a base64 string. ' + + `Response data: ${JSON.stringify(apiResponse, null, 2)}`); + }); + + it('should resolve with metadata on success', () => { + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'getConfig') + .returns(Promise.resolve(VALID_ANDROID_CONFIG_API_RESPONSE)); + stubs.push(stub); + return androidApp.getConfig().should.eventually.deep.equal(VALID_ANDROID_CONFIG); + }); + }); +}); + +describe('ShaCertificate', () => { + describe('Constructor', () => { + const invalidShaHashes = [ + null, + undefined, + '0123456789', + 123456789, + '0123456789abcdefABCDEF01234567890123456', + '0123456789abcdefABCDEF0123456789012345670123456789012345678', + ]; + invalidShaHashes.forEach((invalidShaHash) => { + it('should throw given invalid SHA hash: ' + JSON.stringify(invalidShaHash), () => { + expect(() => { + const shaCertificateAny: any = ShaCertificate; + return new shaCertificateAny(invalidShaHash); + }).to.throw('shaHash must be either a sha256 hash or a sha1 hash.'); + }); + }); + + it('should throw given no SHA hash', () => { + expect(() => { + const shaCertificateAny: any = ShaCertificate; + return new shaCertificateAny(); + }).to.throw('shaHash must be either a sha256 hash or a sha1 hash.'); + }); + + it('should not throw given a valid SHA1 hash', () => { + expect(() => { + return new ShaCertificate(VALID_SHA_1_HASH); + }).not.to.throw(); + }); + + it('should not throw given a valid SHA256 hash', () => { + expect(() => { + return new ShaCertificate(VALID_SHA_256_HASH); + }).not.to.throw(); + }); + }); +}); diff --git a/test/unit/project-management/index.spec.ts b/test/unit/project-management/index.spec.ts new file mode 100644 index 0000000000..6848494c96 --- /dev/null +++ b/test/unit/project-management/index.spec.ts @@ -0,0 +1,75 @@ +/*! + * @license + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +import * as chai from 'chai'; +import * as sinonChai from 'sinon-chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +import * as mocks from '../../resources/mocks'; +import { App } from '../../../src/app/index'; +import { getProjectManagement, ProjectManagement } from '../../../src/project-management/index'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +describe('ProjectManagement', () => { + let mockApp: App; + let mockCredentialApp: App; + + const noProjectIdError = 'Failed to determine project ID. Initialize the SDK ' + + 'with service account credentials, or set project ID as an app option. Alternatively, set the ' + + 'GOOGLE_CLOUD_PROJECT environment variable.'; + + beforeEach(() => { + mockApp = mocks.app(); + mockCredentialApp = mocks.mockCredentialApp(); + }); + + describe('getProjectManagement()', () => { + it('should throw when default app is not available', () => { + expect(() => { + return getProjectManagement(); + }).to.throw('The default Firebase app does not exist.'); + }); + + it('should reject given an invalid credential without project ID', () => { + // Project ID not set in the environment. + delete process.env.GOOGLE_CLOUD_PROJECT; + delete process.env.GCLOUD_PROJECT; + const client = getProjectManagement(mockCredentialApp); + return client.listAndroidApps() + .should.eventually.rejectedWith(noProjectIdError); + }); + + it('should not throw given a valid app', () => { + expect(() => { + return getProjectManagement(mockApp); + }).not.to.throw(); + }); + + it('should return the same instance for a given app instance', () => { + const client1: ProjectManagement = getProjectManagement(mockApp); + const client2: ProjectManagement = getProjectManagement(mockApp); + expect(client1).to.equal(client2); + }); + }); +}); diff --git a/test/unit/project-management/ios-app.spec.ts b/test/unit/project-management/ios-app.spec.ts new file mode 100644 index 0000000000..f250ed5f10 --- /dev/null +++ b/test/unit/project-management/ios-app.spec.ts @@ -0,0 +1,220 @@ +/*! + * Copyright 2018 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 _ from 'lodash'; +import * as sinon from 'sinon'; +import { FirebaseApp } from '../../../src/app/firebase-app'; +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'; +import { AppPlatform, IosApp, IosAppMetadata } from '../../../src/project-management/index'; + +const expect = chai.expect; + +const APP_ID = 'test-app-id'; +const EXPECTED_ERROR = new FirebaseProjectManagementError('internal-error', 'message'); + +describe('IosApp', () => { + // Stubs used to simulate underlying api calls. + let stubs: sinon.SinonStub[] = []; + + let iosApp: IosApp; + let requestHandler: ProjectManagementRequestHandler; + let mockApp: FirebaseApp; + + beforeEach(() => { + mockApp = mocks.app(); + requestHandler = new ProjectManagementRequestHandler(mockApp); + iosApp = new IosApp(APP_ID, requestHandler); + }); + + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + return mockApp.delete(); + }); + + describe('Constructor', () => { + const invalidAppIds = [null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidAppIds.forEach((invalidAppId) => { + it('should throw given invalid app ID: ' + JSON.stringify(invalidAppId), () => { + expect(() => { + const iosAppAny: any = IosApp; + return new iosAppAny(invalidAppId); + }).to.throw('appId must be a non-empty string.'); + }); + }); + + it('should throw given no appId', () => { + expect(() => { + const iosAppAny: any = IosApp; + return new iosAppAny(); + }).to.throw('appId must be a non-empty string.'); + }); + + it('should not throw given a valid app ID', () => { + expect(() => { + return new IosApp(APP_ID, requestHandler); + }).not.to.throw(); + }); + }); + + describe('getMetadata', () => { + const expectedError = new FirebaseProjectManagementError('internal-error', 'message'); + + const VALID_IOS_APP_METADATA_API_RESPONSE = { + name: 'test-resource-name', + appId: APP_ID, + displayName: 'test-display-name', + projectId: 'test-project-id', + bundleId: 'test-bundle-id', + }; + + const VALID_IOS_APP_METADATA: IosAppMetadata = { + platform: AppPlatform.IOS, + resourceName: VALID_IOS_APP_METADATA_API_RESPONSE.name, + appId: APP_ID, + displayName: VALID_IOS_APP_METADATA_API_RESPONSE.displayName, + projectId: VALID_IOS_APP_METADATA_API_RESPONSE.projectId, + bundleId: VALID_IOS_APP_METADATA_API_RESPONSE.bundleId, + }; + + it('should propagate API errors', () => { + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'getResource') + .returns(Promise.reject(expectedError)); + stubs.push(stub); + return iosApp.getMetadata().should.eventually.be.rejected.and.equal(expectedError); + }); + + it('should throw with null API response', () => { + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'getResource') + .resolves(null as any); + stubs.push(stub); + return iosApp.getMetadata() + .should.eventually.be.rejected + .and.have.property( + 'message', + 'getMetadata()\'s responseData must be a non-null object. Response data: null'); + }); + + const requiredFieldsList = ['name', 'appId', 'projectId', 'bundleId']; + requiredFieldsList.forEach((requiredField) => { + it(`should throw with API response missing ${requiredField}`, () => { + const partialApiResponse: any = deepCopy(VALID_IOS_APP_METADATA_API_RESPONSE); + delete partialApiResponse[requiredField]; + + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'getResource') + .returns(Promise.resolve(partialApiResponse)); + stubs.push(stub); + return iosApp.getMetadata() + .should.eventually.be.rejected + .and.have.property( + 'message', + `getMetadata()'s responseData.${requiredField} must be a non-empty string. ` + + `Response data: ${JSON.stringify(partialApiResponse, null, 2)}`); + }); + }); + + it('should resolve with metadata on success', () => { + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'getResource') + .returns(Promise.resolve(VALID_IOS_APP_METADATA_API_RESPONSE)); + stubs.push(stub); + return iosApp.getMetadata().should.eventually.deep.equal(VALID_IOS_APP_METADATA); + }); + }); + + describe('setDisplayName', () => { + const newDisplayName = 'test-new-display-name'; + + it('should propagate API errors', () => { + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'setDisplayName') + .returns(Promise.reject(EXPECTED_ERROR)); + stubs.push(stub); + return iosApp.setDisplayName(newDisplayName) + .should.eventually.be.rejected.and.equal(EXPECTED_ERROR); + }); + + it('should resolve on success', () => { + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'setDisplayName') + .returns(Promise.resolve()); + stubs.push(stub); + return iosApp.setDisplayName(newDisplayName).should.eventually.be.fulfilled; + }); + }); + + describe('getConfig', () => { + const VALID_IOS_CONFIG_API_RESPONSE = { + configFileContents: 'QmFzZTY0IHRlc3Qu', + }; + const VALID_IOS_CONFIG = 'Base64 test.'; + + it('should propagate API errors', () => { + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'getConfig') + .returns(Promise.reject(EXPECTED_ERROR)); + stubs.push(stub); + return iosApp.getConfig().should.eventually.be.rejected.and.equal(EXPECTED_ERROR); + }); + + it('should throw with null API response', () => { + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'getConfig') + .resolves(null as any); + stubs.push(stub); + return iosApp.getConfig() + .should.eventually.be.rejected + .and.have.property( + 'message', + 'getConfig()\'s responseData must be a non-null object. Response data: null'); + }); + + it('should throw with non-base64 response.configFileContents', () => { + const apiResponse = deepCopy(VALID_IOS_CONFIG_API_RESPONSE); + apiResponse.configFileContents = '1' + apiResponse.configFileContents; + + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'getConfig') + .returns(Promise.resolve(apiResponse)); + stubs.push(stub); + return iosApp.getConfig() + .should.eventually.be.rejected + .and.have.property( + 'message', + 'getConfig()\'s responseData.configFileContents must be a base64 string. ' + + `Response data: ${JSON.stringify(apiResponse, null, 2)}`); + }); + + it('should resolve with metadata on success', () => { + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'getConfig') + .returns(Promise.resolve(VALID_IOS_CONFIG_API_RESPONSE)); + stubs.push(stub); + return iosApp.getConfig().should.eventually.deep.equal(VALID_IOS_CONFIG); + }); + }); +}); diff --git a/test/unit/project-management/project-management-api-request.spec.ts b/test/unit/project-management/project-management-api-request.spec.ts new file mode 100644 index 0000000000..772a33ad4c --- /dev/null +++ b/test/unit/project-management/project-management-api-request.spec.ts @@ -0,0 +1,581 @@ +/*! + * Copyright 2018 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 chaiAsPromised from 'chai-as-promised'; +import * as _ from 'lodash'; +import * as sinon from 'sinon'; +import * as sinonChai from 'sinon-chai'; +import { FirebaseApp } from '../../../src/app/firebase-app'; +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'; +import { getSdkVersion } from '../../../src/utils/index'; +import { AppPlatform, ShaCertificate } from '../../../src/project-management/index'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +const VALID_SHA_1_HASH = '0123456789abcdefABCDEF012345678901234567'; + +describe('ProjectManagementRequestHandler', () => { + const HOST = 'firebase.googleapis.com'; + const PORT = 443; + const PROJECT_RESOURCE_NAME = 'projects/test-project-id'; + const APP_ID = 'test-app-id'; + const APP_ID_ANDROID = 'test-android-app-id'; + const APP_ID_IOS = 'test-ios-app-id'; + const ANDROID_APP_RESOURCE_NAME = `projects/-/androidApp/${APP_ID}`; + const IOS_APP_RESOURCE_NAME = `projects/-/iosApp/${APP_ID}`; + const PACKAGE_NAME = 'test-package-name'; + const BUNDLE_ID = 'test-bundle-id'; + const DISPLAY_NAME = 'test-display-name'; + const DISPLAY_NAME_ANDROID = 'test-display-name-android'; + const DISPLAY_NAME_IOS = 'test-display-name-ios'; + const OPERATION_RESOURCE_NAME = 'test-operation-resource-name'; + + const mockAccessToken: string = utils.generateRandomAccessToken(); + let stubs: sinon.SinonStub[] = []; + let getTokenStub: sinon.SinonStub; + let mockApp: FirebaseApp; + let expectedHeaders: object; + let requestHandler: ProjectManagementRequestHandler; + + before(() => { + getTokenStub = utils.stubGetAccessToken(mockAccessToken); + }); + + after(() => { + stubs = []; + getTokenStub.restore(); + }); + + beforeEach(() => { + mockApp = mocks.app(); + expectedHeaders = { + 'X-Client-Version': `Node/Admin/${getSdkVersion()}`, + 'Authorization': 'Bearer ' + mockAccessToken, + }; + requestHandler = new ProjectManagementRequestHandler(mockApp); + return mockApp.INTERNAL.getToken(); + }); + + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + return mockApp.delete(); + }); + + function testHttpErrors(callback: () => Promise): void { + const errorCodeMap: any = { + 400: 'project-management/invalid-argument', + 401: 'project-management/authentication-error', + 403: 'project-management/authentication-error', + 404: 'project-management/not-found', + 500: 'project-management/internal-error', + 503: 'project-management/service-unavailable', + }; + Object.keys(errorCodeMap).forEach((errorCode) => { + if (!Object.prototype.hasOwnProperty.call(errorCodeMap, errorCode)) { + return; + } + it(`should throw for HTTP ${errorCode} errors`, () => { + const stub = sinon.stub(HttpClient.prototype, 'send') + .rejects(utils.errorFrom({}, parseInt(errorCode, 10))); + stubs.push(stub); + + return callback() + .should.eventually.be.rejected + .and.have.property('code', errorCodeMap[errorCode]); + }); + }); + + it('should throw for HTTP unknown errors', () => { + const stub = sinon.stub(HttpClient.prototype, 'send') + .rejects(utils.errorFrom({}, 1337)); + stubs.push(stub); + + return callback() + .should.eventually.be.rejected + .and.have.property('code', 'project-management/unknown-error'); + }); + } + + describe('Constructor', () => { + it('should succeed with a FirebaseApp instance', () => { + expect(() => { + return new ProjectManagementRequestHandler(mockApp); + }).not.to.throw(Error); + }); + }); + + describe('listAndroidApps', () => { + testHttpErrors(() => requestHandler.listAndroidApps(PROJECT_RESOURCE_NAME)); + + it('should succeed', () => { + const expectedResult = { + apps: [{ + resourceName: ANDROID_APP_RESOURCE_NAME, + appId: APP_ID, + }], + }; + + const stub = sinon.stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(expectedResult)); + stubs.push(stub); + + const url = + `https://${HOST}:${PORT}/v1beta1/${PROJECT_RESOURCE_NAME}/androidApps?page_size=100`; + return requestHandler.listAndroidApps(PROJECT_RESOURCE_NAME) + .then((result) => { + expect(result).to.deep.equal(expectedResult); + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'GET', + url, + data: null, + headers: expectedHeaders, + timeout: 10000, + }); + }); + }); + }); + + describe('listIosApps', () => { + testHttpErrors(() => requestHandler.listIosApps(PROJECT_RESOURCE_NAME)); + + it('should succeed', () => { + const expectedResult = { + apps: [{ + resourceName: IOS_APP_RESOURCE_NAME, + appId: APP_ID, + }], + }; + + const stub = sinon.stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(expectedResult)); + stubs.push(stub); + + const url = `https://${HOST}:${PORT}/v1beta1/${PROJECT_RESOURCE_NAME}/iosApps?page_size=100`; + return requestHandler.listIosApps(PROJECT_RESOURCE_NAME) + .then((result) => { + expect(result).to.deep.equal(expectedResult); + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'GET', + url, + data: null, + headers: expectedHeaders, + timeout: 10000, + }); + }); + }); + }); + + describe('listAppMetadata', () => { + testHttpErrors(() => requestHandler.listAppMetadata(PROJECT_RESOURCE_NAME)); + + it('should succeed', () => { + const expectedResult = { + apps: [ + { + appId: APP_ID_ANDROID, + displayName: DISPLAY_NAME_ANDROID, + platform: AppPlatform.ANDROID, + }, + { + appId: APP_ID_IOS, + displayName: DISPLAY_NAME_IOS, + platform: AppPlatform.IOS, + }], + }; + + const stub = sinon.stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(expectedResult)); + stubs.push(stub); + + const url = + `https://${HOST}:${PORT}/v1beta1/${PROJECT_RESOURCE_NAME}:searchApps?page_size=100`; + return requestHandler.listAppMetadata(PROJECT_RESOURCE_NAME) + .then((result) => { + expect(result).to.deep.equal(expectedResult); + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'GET', + url, + data: null, + headers: expectedHeaders, + timeout: 10000, + }); + }); + }); + }); + + describe('createAndroidApp', () => { + testHttpErrors(() => requestHandler.createAndroidApp(PROJECT_RESOURCE_NAME, PACKAGE_NAME)); + + it('should throw when initial API responseData.name is null', () => { + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({})); + stubs.push(stub); + + return requestHandler.createAndroidApp(PROJECT_RESOURCE_NAME, PACKAGE_NAME, DISPLAY_NAME) + .should.eventually.be.rejected + .and.have.property( + 'message', + 'createAndroidApp\'s responseData.name must be a non-empty string. Response data: ' + + '{}'); + }); + + it('should propagate polling API response returned errors', () => { + const initialResult = { name: OPERATION_RESOURCE_NAME }; + const pollErrorResult = { + name: OPERATION_RESOURCE_NAME, + done: true, + error: { + code: 409, + message: 'Already exists', + }, + }; + + const stub = sinon.stub(HttpClient.prototype, 'send') + .onFirstCall().resolves(utils.responseFrom(initialResult)) + .onSecondCall().resolves(utils.responseFrom(pollErrorResult)); + stubs.push(stub); + + return requestHandler.createAndroidApp(PROJECT_RESOURCE_NAME, PACKAGE_NAME, DISPLAY_NAME) + .should.eventually.be.rejected + .and.have.property('code', 'project-management/already-exists'); + }); + + it('should propagate polling API response thrown errors', () => { + const initialResult = { name: OPERATION_RESOURCE_NAME }; + const pollError = 'second-poll-error'; + + const stub = sinon.stub(HttpClient.prototype, 'send') + .onFirstCall().resolves(utils.responseFrom(initialResult)) + .onSecondCall().returns(Promise.reject(pollError)); + stubs.push(stub); + + return requestHandler.createAndroidApp(PROJECT_RESOURCE_NAME, PACKAGE_NAME, DISPLAY_NAME) + .should.eventually.be.rejected + .and.equal(pollError); + }); + + it('should succeed after multiple polls', () => { + const initialResult = { name: OPERATION_RESOURCE_NAME }; + const firstPollResult = { name: OPERATION_RESOURCE_NAME }; + const expectedJsonResponse = '{"field1":"value1"}'; + const secondPollResult = { + name: OPERATION_RESOURCE_NAME, + done: true, + response: expectedJsonResponse, + }; + + const stub = sinon.stub(HttpClient.prototype, 'send') + .onFirstCall().resolves(utils.responseFrom(initialResult)) + .onSecondCall().resolves(utils.responseFrom(firstPollResult)) + .onThirdCall().resolves(utils.responseFrom(secondPollResult)); + stubs.push(stub); + + const initialUrl = `https://${HOST}:${PORT}/v1beta1/${PROJECT_RESOURCE_NAME}/androidApps`; + const initialData = { + packageName: PACKAGE_NAME, + displayName: DISPLAY_NAME, + }; + + const pollingUrl = `https://${HOST}:${PORT}/v1/${OPERATION_RESOURCE_NAME}`; + + return requestHandler.createAndroidApp(PROJECT_RESOURCE_NAME, PACKAGE_NAME, DISPLAY_NAME) + .then((result) => { + expect(result).to.equal(expectedJsonResponse); + expect(stub) + .to.have.been.calledThrice + .and.calledWith({ + method: 'POST', + url: initialUrl, + data: initialData, + headers: expectedHeaders, + timeout: 10000, + }) + .and.calledWith({ + method: 'GET', + url: pollingUrl, + data: null, + headers: expectedHeaders, + timeout: 10000, + }); + }); + }); + }); + + describe('createIosApp', () => { + testHttpErrors(() => requestHandler.createIosApp(PROJECT_RESOURCE_NAME, BUNDLE_ID)); + + it('should throw when initial API responseData.name is null', () => { + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({})); + stubs.push(stub); + + return requestHandler.createIosApp(PROJECT_RESOURCE_NAME, BUNDLE_ID, DISPLAY_NAME) + .should.eventually.be.rejected + .and.have.property( + 'message', + 'createIosApp\'s responseData.name must be a non-empty string. Response data: {}'); + }); + + it('should propagate polling API response returned errors', () => { + const initialResult = { name: OPERATION_RESOURCE_NAME }; + const pollErrorResult = { + name: OPERATION_RESOURCE_NAME, + done: true, + error: { + code: 409, + message: 'Already exists', + }, + }; + + const stub = sinon.stub(HttpClient.prototype, 'send') + .onFirstCall().resolves(utils.responseFrom(initialResult)) + .onSecondCall().resolves(utils.responseFrom(pollErrorResult)); + stubs.push(stub); + + return requestHandler.createIosApp(PROJECT_RESOURCE_NAME, BUNDLE_ID, DISPLAY_NAME) + .should.eventually.be.rejected + .and.have.property('code', 'project-management/already-exists'); + }); + + it('should propagate polling API response thrown errors', () => { + const initialResult = { name: OPERATION_RESOURCE_NAME }; + const pollError = 'second-poll-error'; + + const stub = sinon.stub(HttpClient.prototype, 'send') + .onFirstCall().resolves(utils.responseFrom(initialResult)) + .onSecondCall().returns(Promise.reject(pollError)); + stubs.push(stub); + + return requestHandler.createIosApp(PROJECT_RESOURCE_NAME, BUNDLE_ID, DISPLAY_NAME) + .should.eventually.be.rejected + .and.equal(pollError); + }); + + it('should succeed after multiple polls', () => { + const initialResult = { name: OPERATION_RESOURCE_NAME }; + const firstPollResult = { name: OPERATION_RESOURCE_NAME }; + const expectedJsonResponse = '{"field1":"value1"}'; + const secondPollResult = { + name: OPERATION_RESOURCE_NAME, + done: true, + response: expectedJsonResponse, + }; + + const stub = sinon.stub(HttpClient.prototype, 'send') + .onFirstCall().resolves(utils.responseFrom(initialResult)) + .onSecondCall().resolves(utils.responseFrom(firstPollResult)) + .onThirdCall().resolves(utils.responseFrom(secondPollResult)); + stubs.push(stub); + + const initialUrl = `https://${HOST}:${PORT}/v1beta1/${PROJECT_RESOURCE_NAME}/iosApps`; + const initialData = { + bundleId: BUNDLE_ID, + displayName: DISPLAY_NAME, + }; + + const pollingUrl = `https://${HOST}:${PORT}/v1/${OPERATION_RESOURCE_NAME}`; + + return requestHandler.createIosApp(PROJECT_RESOURCE_NAME, BUNDLE_ID, DISPLAY_NAME) + .then((result) => { + expect(result).to.equal(expectedJsonResponse); + expect(stub) + .to.have.been.calledThrice + .and.calledWith({ + method: 'POST', + url: initialUrl, + data: initialData, + headers: expectedHeaders, + timeout: 10000, + }) + .and.calledWith({ + method: 'GET', + url: pollingUrl, + data: null, + headers: expectedHeaders, + timeout: 10000, + }); + }); + }); + }); + + describe('setDisplayName', () => { + const newDisplayName = 'test-new-display-name'; + + testHttpErrors( + () => requestHandler.setDisplayName(ANDROID_APP_RESOURCE_NAME, newDisplayName)); + + it('should succeed', () => { + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({})); + stubs.push(stub); + + const url = + `https://${HOST}:${PORT}/v1beta1/${ANDROID_APP_RESOURCE_NAME}?update_mask=display_name`; + const requestData = { + displayName: newDisplayName, + }; + return requestHandler.setDisplayName(ANDROID_APP_RESOURCE_NAME, newDisplayName) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'PATCH', + url, + data: requestData, + headers: expectedHeaders, + timeout: 10000, + }); + }); + }); + }); + + describe('getAndroidShaCertificates', () => { + testHttpErrors(() => requestHandler.getAndroidShaCertificates(ANDROID_APP_RESOURCE_NAME)); + + it('should succeed', () => { + const expectedResult: any = { certificates: [] }; + + const stub = sinon.stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(expectedResult)); + stubs.push(stub); + + const url = `https://${HOST}:${PORT}/v1beta1/${ANDROID_APP_RESOURCE_NAME}/sha`; + return requestHandler.getAndroidShaCertificates(ANDROID_APP_RESOURCE_NAME) + .then((result) => { + expect(result).to.deep.equal(expectedResult); + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'GET', + url, + data: null, + headers: expectedHeaders, + timeout: 10000, + }); + }); + }); + }); + + describe('addAndroidShaCertificate', () => { + const certificateToAdd = new ShaCertificate(VALID_SHA_1_HASH); + + testHttpErrors( + () => requestHandler.addAndroidShaCertificate(ANDROID_APP_RESOURCE_NAME, certificateToAdd)); + + it('should succeed', () => { + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({})); + stubs.push(stub); + + const url = `https://${HOST}:${PORT}/v1beta1/${ANDROID_APP_RESOURCE_NAME}/sha`; + const requestData = { + shaHash: VALID_SHA_1_HASH, + certType: 'SHA_1', + }; + return requestHandler.addAndroidShaCertificate(ANDROID_APP_RESOURCE_NAME, certificateToAdd) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url, + data: requestData, + headers: expectedHeaders, + timeout: 10000, + }); + }); + }); + }); + + describe('getConfig', () => { + testHttpErrors(() => requestHandler.getConfig(ANDROID_APP_RESOURCE_NAME)); + + it('should succeed', () => { + const expectedResult = { + configFileContents: 'test-base64-string', + }; + + const stub = sinon.stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(expectedResult)); + stubs.push(stub); + + const url = `https://${HOST}:${PORT}/v1beta1/${ANDROID_APP_RESOURCE_NAME}/config`; + return requestHandler.getConfig(ANDROID_APP_RESOURCE_NAME) + .then((result) => { + expect(result).to.deep.equal(expectedResult); + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'GET', + url, + data: null, + headers: expectedHeaders, + timeout: 10000, + }); + }); + }); + }); + + describe('getResource', () => { + const resourceName = 'test-resource-name'; + + testHttpErrors(() => requestHandler.getResource(resourceName)); + + it('should succeed', () => { + const expectedResult = { success: true }; + + const stub = sinon.stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(expectedResult)); + stubs.push(stub); + + const url = `https://${HOST}:${PORT}/v1beta1/${resourceName}`; + return requestHandler.getResource(resourceName) + .then((result) => { + expect(result).to.deep.equal(expectedResult); + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'GET', + url, + data: null, + headers: expectedHeaders, + timeout: 10000, + }); + }); + }); + }); + + describe('deleteResource', () => { + const resourceName = 'test-resource-name'; + + testHttpErrors(() => requestHandler.deleteResource(resourceName)); + + it('should succeed', () => { + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(utils.responseFrom({})); + stubs.push(stub); + + const url = `https://${HOST}:${PORT}/v1beta1/${resourceName}`; + return requestHandler.deleteResource(resourceName) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'DELETE', + url, + data: null, + headers: expectedHeaders, + timeout: 10000, + }); + }); + }); + }); +}); diff --git a/test/unit/project-management/project-management.spec.ts b/test/unit/project-management/project-management.spec.ts new file mode 100644 index 0000000000..b818bdb1c8 --- /dev/null +++ b/test/unit/project-management/project-management.spec.ts @@ -0,0 +1,573 @@ +/*! + * Copyright 2018 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 _ from 'lodash'; +import * as sinon from 'sinon'; +import { FirebaseApp } from '../../../src/app/firebase-app'; +import { + ProjectManagementRequestHandler +} from '../../../src/project-management/project-management-api-request-internal'; +import { FirebaseProjectManagementError } from '../../../src/utils/error'; +import * as mocks from '../../resources/mocks'; +import { + AndroidApp, AppMetadata, AppPlatform, IosApp, ProjectManagement, +} from '../../../src/project-management/index'; + +const expect = chai.expect; + +const APP_ID = 'test-app-id'; +const APP_ID_ANDROID = 'test-app-id-android'; +const APP_ID_IOS = 'test-app-id-ios'; +const PACKAGE_NAME = 'test-package-name'; +const BUNDLE_ID = 'test-bundle-id'; +const DISPLAY_NAME_ANDROID = 'test-display-name-android'; +const DISPLAY_NAME_IOS = 'test-display-name-ios'; +const EXPECTED_ERROR = new FirebaseProjectManagementError('internal-error', 'message'); +const RESOURCE_NAME = 'projects/test/resources-name'; +const RESOURCE_NAME_ANDROID = 'projects/test/resources-name:android'; +const RESOURCE_NAME_IOS = 'projects/test/resources-name:ios'; + +const VALID_SHA_256_HASH = '0123456789abcdefABCDEF01234567890123456701234567890123456789abcd'; + +describe('ProjectManagement', () => { + // Stubs used to simulate underlying api calls. + let stubs: sinon.SinonStub[] = []; + + let projectManagement: ProjectManagement; + let mockApp: FirebaseApp; + let mockCredentialApp: FirebaseApp; + + const noProjectIdErrorMessage = 'Failed to determine project ID. Initialize the SDK with service ' + + 'account credentials, or set project ID as an app option. Alternatively, set the ' + + 'GOOGLE_CLOUD_PROJECT environment variable.'; + + beforeEach(() => { + mockApp = mocks.app(); + mockCredentialApp = mocks.mockCredentialApp(); + projectManagement = new ProjectManagement(mockApp); + }); + + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + 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 projectManagementAny: any = ProjectManagement; + return new projectManagementAny(invalidApp); + }).to.throw( + 'First argument passed to admin.projectManagement() must be a valid Firebase app ' + + 'instance.'); + }); + }); + + it('should throw given no app', () => { + expect(() => { + const projectManagementAny: any = ProjectManagement; + return new projectManagementAny(); + }).to.throw( + 'First argument passed to admin.projectManagement() 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 projectMgt = new ProjectManagement(mockCredentialApp); + return projectMgt.listIosApps() + .should.eventually.rejectedWith(noProjectIdErrorMessage); + }); + + it('should not throw given a valid app', () => { + expect(() => { + return new ProjectManagement(mockApp); + }).not.to.throw(); + }); + }); + + describe('app', () => { + it('returns the app from the constructor', () => { + // We expect referential equality here + expect(projectManagement.app).to.equal(mockApp); + }); + }); + + describe('listAndroidApps', () => { + it('should propagate API errors', () => { + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'listAndroidApps') + .returns(Promise.reject(EXPECTED_ERROR)); + stubs.push(stub); + return projectManagement.listAndroidApps() + .should.eventually.be.rejected.and.equal(EXPECTED_ERROR); + }); + + it('should throw with null API response', () => { + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'listAndroidApps') + .resolves(null as any); + stubs.push(stub); + return projectManagement.listAndroidApps() + .should.eventually.be.rejected + .and.have.property( + 'message', + 'listAndroidApps()\'s responseData must be a non-null object. Response data: null'); + }); + + it('should return empty array when API response missing "apps" field', () => { + const partialApiResponse = {}; + + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'listAndroidApps') + .returns(Promise.resolve(partialApiResponse)); + stubs.push(stub); + return projectManagement.listAndroidApps() + .should.eventually.deep.equal([]); + }); + + it('should throw when API response has non-array "apps" field', () => { + const partialApiResponse = { apps: 'none' }; + + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'listAndroidApps') + .returns(Promise.resolve(partialApiResponse)); + stubs.push(stub); + return projectManagement.listAndroidApps() + .should.eventually.be.rejected + .and.have.property( + 'message', + '"apps" field must be present in the listAndroidApps() response data. Response data: ' + + JSON.stringify(partialApiResponse, null, 2)); + }); + + it('should throw with API response missing "apps[].appId" field', () => { + const partialApiResponse = { + apps: [{}], + }; + + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'listAndroidApps') + .returns(Promise.resolve(partialApiResponse)); + stubs.push(stub); + return projectManagement.listAndroidApps() + .should.eventually.be.rejected + .and.have.property( + 'message', + '"apps[].appId" field must be present in the listAndroidApps() response data. ' + + `Response data: ${JSON.stringify(partialApiResponse, null, 2)}`); + }); + + it('should resolve with list of Android apps on success', () => { + const validAndroidApps: AndroidApp[] = [projectManagement.androidApp(APP_ID)]; + const validListAndroidAppsApiResponse = { + apps: [{ appId: APP_ID }], + }; + + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'listAndroidApps') + .returns(Promise.resolve(validListAndroidAppsApiResponse)); + stubs.push(stub); + return projectManagement.listAndroidApps() + .should.eventually.deep.equal(validAndroidApps); + }); + }); + + describe('listIosApps', () => { + const VALID_LIST_IOS_APPS_API_RESPONSE = { + apps: [{ appId: APP_ID }], + }; + + it('should propagate API errors', () => { + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'listIosApps') + .returns(Promise.reject(EXPECTED_ERROR)); + stubs.push(stub); + return projectManagement.listIosApps() + .should.eventually.be.rejected.and.equal(EXPECTED_ERROR); + }); + + it('should throw with null API response', () => { + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'listIosApps') + .resolves(null as any); + stubs.push(stub); + return projectManagement.listIosApps() + .should.eventually.be.rejected + .and.have.property( + 'message', + 'listIosApps()\'s responseData must be a non-null object. Response data: null'); + }); + + it('should return empty array when API response missing "apps" field', () => { + const partialApiResponse = {}; + + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'listIosApps') + .returns(Promise.resolve(partialApiResponse)); + stubs.push(stub); + return projectManagement.listIosApps() + .should.eventually.deep.equal([]); + }); + + it('should throw when API response has non-array "apps" field', () => { + const partialApiResponse = { apps: 'none' }; + + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'listIosApps') + .returns(Promise.resolve(partialApiResponse)); + stubs.push(stub); + return projectManagement.listIosApps() + .should.eventually.be.rejected + .and.have.property( + 'message', + '"apps" field must be present in the listIosApps() response data. Response data: ' + + JSON.stringify(partialApiResponse, null, 2)); + }); + + it('should throw with API response missing "apps[].appId" field', () => { + const partialApiResponse = { + apps: [{}], + }; + + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'listIosApps') + .returns(Promise.resolve(partialApiResponse)); + stubs.push(stub); + return projectManagement.listIosApps() + .should.eventually.be.rejected + .and.have.property( + 'message', + '"apps[].appId" field must be present in the listIosApps() response data. ' + + `Response data: ${JSON.stringify(partialApiResponse, null, 2)}`); + }); + + it('should resolve with list of Ios apps on success', () => { + const validIosApps: IosApp[] = [projectManagement.iosApp(APP_ID)]; + + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'listIosApps') + .returns(Promise.resolve(VALID_LIST_IOS_APPS_API_RESPONSE)); + stubs.push(stub); + return projectManagement.listIosApps() + .should.eventually.deep.equal(validIosApps); + }); + }); + + describe('androidApp', () => { + it('should successfully return an AndroidApp', () => { + return projectManagement.androidApp(APP_ID).appId.should.equal(APP_ID); + }); + }); + + describe('iosApp', () => { + it('should successfully return an IosApp', () => { + return projectManagement.iosApp(APP_ID).appId.should.equal(APP_ID); + }); + }); + + describe('shaCertificate', () => { + it('should successfully return a ShaCertificate', () => { + const shaCertificate = projectManagement.shaCertificate(VALID_SHA_256_HASH); + shaCertificate.shaHash.should.equal(VALID_SHA_256_HASH); + shaCertificate.certType.should.equal('sha256'); + }); + }); + + describe('createAndroidApp', () => { + it('should propagate intial API response errors', () => { + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'createAndroidApp') + .returns(Promise.reject(EXPECTED_ERROR)); + stubs.push(stub); + return projectManagement.createAndroidApp(PACKAGE_NAME) + .should.eventually.be.rejected.and.equal(EXPECTED_ERROR); + }); + + it('should throw when initial API response is null', () => { + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'createAndroidApp') + .resolves(null as any); + stubs.push(stub); + return projectManagement.createAndroidApp(PACKAGE_NAME) + .should.eventually.be.rejected + .and.have.property( + 'message', + 'createAndroidApp()\'s responseData must be a non-null object. Response data: null'); + }); + + it('should throw when initial API response.appId is undefined', () => { + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'createAndroidApp') + .returns(Promise.resolve({})); + stubs.push(stub); + return projectManagement.createAndroidApp(PACKAGE_NAME) + .should.eventually.be.rejected + .and.have.property( + 'message', + '"responseData.appId" field must be present in createAndroidApp()\'s response data. ' + + 'Response data: {}'); + }); + + it('should resolve with AndroidApp on success', () => { + const createdAndroidApp: AndroidApp = projectManagement.androidApp(APP_ID); + const validCreateAppResponse = { appId: APP_ID }; + + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'createAndroidApp') + .returns(Promise.resolve(validCreateAppResponse)); + stubs.push(stub); + return projectManagement.createAndroidApp(PACKAGE_NAME) + .should.eventually.deep.equal(createdAndroidApp); + }); + }); + + describe('createIosApp', () => { + it('should propagate intial API response errors', () => { + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'createIosApp') + .returns(Promise.reject(EXPECTED_ERROR)); + stubs.push(stub); + return projectManagement.createIosApp(BUNDLE_ID) + .should.eventually.be.rejected.and.equal(EXPECTED_ERROR); + }); + + it('should throw when initial API response is null', () => { + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'createIosApp') + .resolves(null as any); + stubs.push(stub); + return projectManagement.createIosApp(BUNDLE_ID) + .should.eventually.be.rejected + .and.have.property( + 'message', + 'createIosApp()\'s responseData must be a non-null object. Response data: null'); + }); + + it('should throw when initial API response.appId is undefined', () => { + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'createIosApp') + .returns(Promise.resolve({})); + stubs.push(stub); + return projectManagement.createIosApp(BUNDLE_ID) + .should.eventually.be.rejected + .and.have.property( + 'message', + '"responseData.appId" field must be present in createIosApp()\'s response data. ' + + 'Response data: {}'); + }); + + it('should resolve with IosApp on success', () => { + const createdIosApp: IosApp = projectManagement.iosApp(APP_ID); + const validCreateAppResponse = { appId: APP_ID }; + + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'createIosApp') + .returns(Promise.resolve(validCreateAppResponse)); + stubs.push(stub); + return projectManagement.createIosApp(BUNDLE_ID) + .should.eventually.deep.equal(createdIosApp); + }); + }); + + describe('listAppMetadata', () => { + const VALID_LIST_APP_METADATA_API_RESPONSE = { + apps: [ + { + appId: APP_ID_ANDROID, + displayName: DISPLAY_NAME_ANDROID, + platform: 'ANDROID', + name: RESOURCE_NAME_ANDROID, + }, + { + appId: APP_ID_IOS, + displayName: DISPLAY_NAME_IOS, + platform: 'IOS', + name: RESOURCE_NAME_IOS, + }, + { + appId: APP_ID, + platform: 'WEB', + name: RESOURCE_NAME, + }, + ], + }; + + it('should propagate API errors', () => { + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'listAppMetadata') + .returns(Promise.reject(EXPECTED_ERROR)); + stubs.push(stub); + return projectManagement.listAppMetadata() + .should.eventually.be.rejected.and.equal(EXPECTED_ERROR); + }); + + it('should throw with null API response', () => { + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'listAppMetadata') + .resolves(null as any); + stubs.push(stub); + return projectManagement.listAppMetadata() + .should.eventually.be.rejected + .and.have.property( + 'message', + 'listAppMetadata()\'s responseData must be a non-null object. Response data: null'); + }); + + it('should return empty array when API response missing "apps" field', () => { + const partialApiResponse = {}; + + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'listAppMetadata') + .returns(Promise.resolve(partialApiResponse)); + stubs.push(stub); + return projectManagement.listAppMetadata() + .should.eventually.deep.equal([]); + }); + + it('should throw when API response has non-array "apps" field', () => { + const partialApiResponse = { apps: 'none' }; + + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'listAppMetadata') + .returns(Promise.resolve(partialApiResponse)); + stubs.push(stub); + return projectManagement.listAppMetadata() + .should.eventually.be.rejected + .and.have.property( + 'message', + '"apps" field must be present in the listAppMetadata() response data. Response data: ' + + JSON.stringify(partialApiResponse, null, 2)); + }); + + it('should throw with API response missing "apps[].appId" field', () => { + const partialApiResponse = { + apps: [{}], + }; + + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'listAppMetadata') + .returns(Promise.resolve(partialApiResponse)); + stubs.push(stub); + return projectManagement.listAppMetadata() + .should.eventually.be.rejected + .and.have.property( + 'message', + '"apps[].appId" field must be present in the listAppMetadata() response data. ' + + `Response data: ${JSON.stringify(partialApiResponse, null, 2)}`); + }); + + it('should throw with API response missing "apps[].platform" field', () => { + const missingPlatformApiResponse = { + apps: [{ + appId: APP_ID, + }], + }; + + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'listAppMetadata') + .returns(Promise.resolve(missingPlatformApiResponse)); + stubs.push(stub); + return projectManagement.listAppMetadata() + .should.eventually.be.rejected + .and.have.property( + 'message', + '"apps[].platform" field must be present in the listAppMetadata() response data. ' + + `Response data: ${JSON.stringify(missingPlatformApiResponse, null, 2)}`); + }); + + it('should resolve with list of apps metadata on success', () => { + const expectedAppMetadata: AppMetadata[] = [ + { + appId: VALID_LIST_APP_METADATA_API_RESPONSE.apps[0].appId, + displayName: VALID_LIST_APP_METADATA_API_RESPONSE.apps[0].displayName, + platform: AppPlatform.ANDROID, + projectId: mocks.projectId, + resourceName: RESOURCE_NAME_ANDROID, + }, + { + appId: VALID_LIST_APP_METADATA_API_RESPONSE.apps[1].appId, + displayName: VALID_LIST_APP_METADATA_API_RESPONSE.apps[1].displayName, + platform: AppPlatform.IOS, + projectId: mocks.projectId, + resourceName: RESOURCE_NAME_IOS, + }, + { + appId: VALID_LIST_APP_METADATA_API_RESPONSE.apps[2].appId, + platform: AppPlatform.PLATFORM_UNKNOWN, + projectId: mocks.projectId, + resourceName: RESOURCE_NAME, + }, + ]; + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'listAppMetadata') + .returns(Promise.resolve(VALID_LIST_APP_METADATA_API_RESPONSE)); + stubs.push(stub); + return projectManagement.listAppMetadata() + .should.eventually.deep.equal(expectedAppMetadata); + }); + + it('should resolve with "apps[].platform" to be "PLATFORM_UNKNOWN" for web app', () => { + const webPlatformApiResponse = { + apps: [{ + appId: APP_ID, + platform: 'WEB', + name: RESOURCE_NAME, + }], + }; + const expectedAppMetadata: AppMetadata[] = [{ + appId: APP_ID, + platform: AppPlatform.PLATFORM_UNKNOWN, + projectId: mocks.projectId, + resourceName: RESOURCE_NAME, + }]; + + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'listAppMetadata') + .returns(Promise.resolve(webPlatformApiResponse)); + stubs.push(stub); + return projectManagement.listAppMetadata() + .should.eventually.deep.equal(expectedAppMetadata); + }); + }); + + describe('setDisplayName', () => { + it('should propagate API errors', () => { + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'setDisplayName') + .returns(Promise.reject(EXPECTED_ERROR)); + stubs.push(stub); + return projectManagement.setDisplayName(DISPLAY_NAME_ANDROID) + .should.eventually.be.rejected.and.equal(EXPECTED_ERROR); + }); + + it('should resolve on success', () => { + const stub = sinon + .stub(ProjectManagementRequestHandler.prototype, 'setDisplayName') + .returns(Promise.resolve()); + stubs.push(stub); + return projectManagement.setDisplayName(DISPLAY_NAME_ANDROID).should.eventually.be.fulfilled; + }); + }); +}); diff --git a/test/unit/remote-config/condition-evaluator.spec.ts b/test/unit/remote-config/condition-evaluator.spec.ts new file mode 100644 index 0000000000..fbf3ca4979 --- /dev/null +++ b/test/unit/remote-config/condition-evaluator.spec.ts @@ -0,0 +1,825 @@ +/*! + * Copyright 2024 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 { ConditionEvaluator } from '../../../src/remote-config/condition-evaluator-internal'; +import { + PercentConditionOperator, + PercentCondition +} from '../../../src/remote-config/remote-config-api'; +import { v4 as uuidv4 } from 'uuid'; +import { clone } from 'lodash'; +import * as farmhash from 'farmhash'; + +const expect = chai.expect; + + + +describe('ConditionEvaluator', () => { + let stubs: sinon.SinonStub[] = []; + + afterEach(() => { + for (const stub of stubs) { + stub.restore(); + } + stubs = []; + }); + + describe('evaluateConditions', () => { + it('should evaluate empty OR condition to false', () => { + const condition = { + name: 'is_enabled', + condition: { + orCondition: { + } + } + }; + const context = {} + const evaluator = new ConditionEvaluator(); + expect(evaluator.evaluateConditions([condition], context)).deep.equals( + new Map([['is_enabled', false]])); + }); + + it('should evaluate empty OR.AND condition to true', () => { + const condition = { + name: 'is_enabled', + condition: { + orCondition: { + conditions: [ + { + andCondition: { + } + } + ] + } + } + }; + const context = {} + const evaluator = new ConditionEvaluator(); + expect(evaluator.evaluateConditions([condition], context)).deep.equals( + new Map([['is_enabled', true]])); + }); + + it('should evaluate OR.AND.TRUE condition to true', () => { + const condition = { + name: 'is_enabled', + condition: { + orCondition: { + conditions: [ + { + andCondition: { + conditions: [ + { + true: { + } + } + ] + } + } + ] + } + } + }; + const context = {} + const evaluator = new ConditionEvaluator(); + expect(evaluator.evaluateConditions([condition], context)).deep.equals( + new Map([['is_enabled', true]])); + }); + + it('should evaluate OR.AND.FALSE condition to false', () => { + const condition = { + name: 'is_enabled', + condition: { + orCondition: { + conditions: [ + { + andCondition: { + conditions: [ + { + false: { + } + } + ] + } + } + ] + } + } + }; + const context = {} + const evaluator = new ConditionEvaluator(); + expect(evaluator.evaluateConditions([condition], context)).deep.equals( + new Map([['is_enabled', false]])); + }); + + it('should evaluate non-OR top-level condition', () => { + // The server wraps conditions in OR.AND, but the evaluation logic + // is more general. + const condition = { + name: 'is_enabled', + condition: { + true: { + } + } + }; + const context = {} + const evaluator = new ConditionEvaluator(); + expect(evaluator.evaluateConditions([condition], context)).deep.equals( + new Map([['is_enabled', true]])); + }); + + describe('percentCondition', () => { + it('should evaluate an unknown operator to false', () => { + // Verifies future operators won't trigger errors. + const condition = { + name: 'is_enabled', + condition: { + orCondition: { + conditions: [{ + andCondition: { + conditions: [{ + percent: { + percentOperator: PercentConditionOperator.UNKNOWN + } + }], + } + }] + } + } + }; + const context = { randomizationId: '123' } + const evaluator = new ConditionEvaluator(); + expect(evaluator.evaluateConditions([condition], context)).deep.equals( + new Map([['is_enabled', false]])); + }); + + it('should evaluate less or equal to max to true', () => { + const condition = { + name: 'is_enabled', + condition: { + orCondition: { + conditions: [{ + andCondition: { + conditions: [{ + percent: { + percentOperator: PercentConditionOperator.LESS_OR_EQUAL, + seed: 'abcdef', + microPercent: 100_000_000 + } + }], + } + }] + } + } + }; + const context = { randomizationId: '123' } + const evaluator = new ConditionEvaluator(); + expect(evaluator.evaluateConditions([condition], context)).deep.equals( + new Map([['is_enabled', true]])); + }); + + it('should evaluate less or equal to min to false', () => { + const condition = { + name: 'is_enabled', + condition: { + orCondition: { + conditions: [{ + andCondition: { + conditions: [{ + percent: { + percentOperator: PercentConditionOperator.LESS_OR_EQUAL, + seed: 'abcdef', + microPercent: 0 + } + }], + } + }] + } + } + }; + const context = { randomizationId: '123' } + const evaluator = new ConditionEvaluator(); + expect(evaluator.evaluateConditions([condition], context)).deep.equals( + new Map([['is_enabled', false]])); + }); + + it('should use zero for undefined microPercent', () => { + // Stubs ID hasher to return a number larger than zero. + const stub = sinon + .stub(farmhash, 'fingerprint64') + .returns('1'); + stubs.push(stub); + + const condition = { + name: 'is_enabled', + condition: { + orCondition: { + conditions: [{ + andCondition: { + conditions: [{ + percent: { + percentOperator: PercentConditionOperator.LESS_OR_EQUAL, + // Leaves microPercent undefined + } + }], + } + }] + } + } + }; + const context = { randomizationId: '123' } + const evaluator = new ConditionEvaluator(); + const actual = evaluator.evaluateConditions([condition], context) + .get('is_enabled'); + + // Evaluates false because 1 is not <= 0 + expect(actual).to.be.false; + }); + + it('should use zeros for undefined microPercentRange', () => { + // Stubs ID hasher to return a number in range. + const stub = sinon + .stub(farmhash, 'fingerprint64') + .returns('1'); + stubs.push(stub); + + const condition = { + name: 'is_enabled', + condition: { + orCondition: { + conditions: [{ + andCondition: { + conditions: [{ + percent: { + percentOperator: PercentConditionOperator.BETWEEN, + // Leaves microPercentRange undefined + } + }], + } + }] + } + } + }; + const context = { randomizationId: '123' } + const evaluator = new ConditionEvaluator(); + const actual = evaluator.evaluateConditions([condition], context) + .get('is_enabled'); + + // Evaluates false because 1 is not in (0,0] + expect(actual).to.be.false; + }); + + it('should use zero for undefined microPercentUpperBound', () => { + // Stubs ID hasher to return a number outside range. + const stub = sinon + .stub(farmhash, 'fingerprint64') + .returns('1'); + stubs.push(stub); + + const condition = { + name: 'is_enabled', + condition: { + orCondition: { + conditions: [{ + andCondition: { + conditions: [{ + percent: { + percentOperator: PercentConditionOperator.BETWEEN, + microPercentRange: { + microPercentLowerBound: 0 + // Leaves upper bound undefined + } + } + }], + } + }] + } + } + }; + const context = { randomizationId: '123' } + const evaluator = new ConditionEvaluator(); + const actual = evaluator.evaluateConditions([condition], context) + .get('is_enabled'); + + // Evaluates false because 1 is not in (0,0] + expect(actual).to.be.false; + }); + + it('should use zero for undefined microPercentLowerBound', () => { + // Stubs ID hasher to return a number in range. + const stub = sinon + .stub(farmhash, 'fingerprint64') + .returns('1'); + stubs.push(stub); + + const condition = { + name: 'is_enabled', + condition: { + orCondition: { + conditions: [{ + andCondition: { + conditions: [{ + percent: { + percentOperator: PercentConditionOperator.BETWEEN, + microPercentRange: { + microPercentUpperBound: 1 + // Leaves lower bound undefined + } + } + }], + } + }] + } + } + }; + const context = { randomizationId: '123' } + const evaluator = new ConditionEvaluator(); + const actual = evaluator.evaluateConditions([condition], context) + .get('is_enabled'); + + // Evaluates true because 1 is in (0,1] + expect(actual).to.be.true; + }); + + it('should evaluate 9 as less or equal to 10', () => { + const stub = sinon + .stub(farmhash, 'fingerprint64') + .returns('9'); + + stubs.push(stub); + const condition = { + name: 'is_enabled', + condition: { + orCondition: { + conditions: [{ + andCondition: { + conditions: [{ + percent: { + percentOperator: PercentConditionOperator.LESS_OR_EQUAL, + seed: 'abcdef', + microPercent: 10 + } + }], + } + }] + } + } + }; + const context = { randomizationId: '123' } + const evaluator = new ConditionEvaluator(); + const actual = evaluator.evaluateConditions([condition], context) + .get('is_enabled'); + expect(actual).to.be.true; + }); + + it('should evaluate 10 as less or equal to 10', () => { + const stub = sinon + .stub(farmhash, 'fingerprint64') + .returns('10'); + + stubs.push(stub); + const condition = { + name: 'is_enabled', + condition: { + orCondition: { + conditions: [{ + andCondition: { + conditions: [{ + percent: { + percentOperator: PercentConditionOperator.LESS_OR_EQUAL, + seed: 'abcdef', + microPercent: 10 + } + }], + } + }] + } + } + }; + const context = { randomizationId: '123' } + const evaluator = new ConditionEvaluator(); + const actual = evaluator.evaluateConditions([condition], context) + .get('is_enabled'); + expect(actual).to.be.true; + }); + + it('should evaluate 11 as not less or equal to 10', () => { + const stub = sinon + .stub(farmhash, 'fingerprint64') + .returns('11'); + + stubs.push(stub); + const condition = { + name: 'is_enabled', + condition: { + orCondition: { + conditions: [{ + andCondition: { + conditions: [{ + percent: { + percentOperator: PercentConditionOperator.LESS_OR_EQUAL, + seed: 'abcdef', + microPercent: 10 + } + }], + } + }] + } + } + }; + const context = { randomizationId: '123' } + const evaluator = new ConditionEvaluator(); + const actual = evaluator.evaluateConditions([condition], context) + .get('is_enabled'); + expect(actual).to.be.false; + }); + + it('should negate -11 to 11 and evaluate as not less or equal to 10', () => { + const stub = sinon + .stub(farmhash, 'fingerprint64') + .returns('-11'); + + stubs.push(stub); + const condition = { + name: 'is_enabled', + condition: { + orCondition: { + conditions: [{ + andCondition: { + conditions: [{ + percent: { + percentOperator: PercentConditionOperator.LESS_OR_EQUAL, + seed: 'abcdef', + microPercent: 10 + } + }], + } + }] + } + } + }; + const context = { randomizationId: '123' } + const evaluator = new ConditionEvaluator(); + const actual = evaluator.evaluateConditions([condition], context) + .get('is_enabled'); + expect(actual).to.be.false; + }); + + it('should evaluate greater than min to true', () => { + const condition = { + name: 'is_enabled', + condition: { + orCondition: { + conditions: [{ + andCondition: { + conditions: [{ + percent: { + percentOperator: PercentConditionOperator.GREATER_THAN, + seed: 'abcdef', + microPercent: 0 + } + }], + } + }] + } + } + }; + const context = { randomizationId: '123' } + const evaluator = new ConditionEvaluator(); + expect(evaluator.evaluateConditions([condition], context)).deep.equals( + new Map([['is_enabled', true]])); + }); + + it('should evaluate 11M as greater than 10M', () => { + const stub = sinon + .stub(farmhash, 'fingerprint64') + .returns('11'); + + stubs.push(stub); + const condition = { + name: 'is_enabled', + condition: { + orCondition: { + conditions: [{ + andCondition: { + conditions: [{ + percent: { + percentOperator: PercentConditionOperator.GREATER_THAN, + seed: 'abcdef', + microPercent: 10 + } + }], + } + }] + } + } + }; + const context = { randomizationId: '123' } + const evaluator = new ConditionEvaluator(); + const actual = evaluator.evaluateConditions([condition], context) + .get('is_enabled'); + expect(actual).to.be.true; + }); + + it('should evaluate 9 as not greater than 10', () => { + const stub = sinon + .stub(farmhash, 'fingerprint64') + .returns('9'); + stubs.push(stub); + + const condition = { + name: 'is_enabled', + condition: { + orCondition: { + conditions: [{ + andCondition: { + conditions: [{ + percent: { + percentOperator: PercentConditionOperator.GREATER_THAN, + seed: 'abcdef', + microPercent: 10 + } + }], + } + }] + } + } + }; + const context = { randomizationId: '123' } + const evaluator = new ConditionEvaluator(); + const actual = evaluator.evaluateConditions([condition], context) + .get('is_enabled'); + expect(actual).to.be.false; + }); + + it('should evaluate greater than max to false', () => { + const condition = { + name: 'is_enabled', + condition: { + orCondition: { + conditions: [{ + andCondition: { + conditions: [{ + percent: { + percentOperator: PercentConditionOperator.GREATER_THAN, + seed: 'abcdef', + microPercent: 100_000_000 + } + }], + } + }] + } + } + }; + const context = { randomizationId: '123' } + const evaluator = new ConditionEvaluator(); + expect(evaluator.evaluateConditions([condition], context)).deep.equals( + new Map([['is_enabled', false]])); + }); + + it('should evaluate between min and max to true', () => { + const condition = { + name: 'is_enabled', + condition: { + orCondition: { + conditions: [{ + andCondition: { + conditions: [{ + percent: { + percentOperator: PercentConditionOperator.BETWEEN, + seed: 'abcdef', + microPercentRange: { + microPercentLowerBound: 0, + microPercentUpperBound: 100_000_000 + } + } + }], + } + }] + } + } + }; + const context = { randomizationId: '123' } + const evaluator = new ConditionEvaluator(); + expect(evaluator.evaluateConditions([condition], context)).deep.equals( + new Map([['is_enabled', true]])); + }); + + it('should evaluate 10 as between 9 and 11', () => { + const stub = sinon + .stub(farmhash, 'fingerprint64') + .returns('10'); + stubs.push(stub); + + const condition = { + name: 'is_enabled', + condition: { + orCondition: { + conditions: [{ + andCondition: { + conditions: [{ + percent: { + percentOperator: PercentConditionOperator.BETWEEN, + seed: 'abcdef', + microPercentRange: { + microPercentLowerBound: 9, + microPercentUpperBound: 11 + } + } + }], + } + }] + } + } + }; + const context = { randomizationId: '123' } + const evaluator = new ConditionEvaluator(); + const actual = evaluator.evaluateConditions([condition], context) + .get('is_enabled'); + expect(actual).to.be.true; + }); + + it('should evaluate between equal bounds to false', () => { + const condition = { + name: 'is_enabled', + condition: { + orCondition: { + conditions: [{ + andCondition: { + conditions: [{ + percent: { + percentOperator: PercentConditionOperator.BETWEEN, + seed: 'abcdef', + microPercentRange: { + microPercentLowerBound: 50000000, + microPercentUpperBound: 50000000 + } + } + }], + } + }] + } + } + }; + const context = { randomizationId: '123' } + const evaluator = new ConditionEvaluator(); + expect(evaluator.evaluateConditions([condition], context)).deep.equals( + new Map([['is_enabled', false]])); + }); + + it('should evaluate 12 as not between 9 and 11', () => { + const stub = sinon + .stub(farmhash, 'fingerprint64') + .returns('12'); + stubs.push(stub); + + const condition = { + name: 'is_enabled', + condition: { + orCondition: { + conditions: [{ + andCondition: { + conditions: [{ + percent: { + percentOperator: PercentConditionOperator.BETWEEN, + seed: 'abcdef', + microPercentRange: { + microPercentLowerBound: 9, + microPercentUpperBound: 11 + } + } + }], + } + }] + } + } + }; + const context = { randomizationId: '123' } + const evaluator = new ConditionEvaluator(); + const actual = evaluator.evaluateConditions([condition], context) + .get('is_enabled'); + expect(actual).to.be.false; + }); + + // The following tests are probablistic. They use tolerances based on + // standard deviations to balance accuracy and flakiness. Random IDs will + // hash to the target range + 3 standard deviations 99.7% of the time, + // which minimizes flakiness. + // Use python to calculate standard deviation. For example, for 100k + // trials with 50% probability: + // from scipy.stats import binom + // print(binom.std(100_000, 0.5) * 3) + it('should evaluate less or equal to 10% to approx 10%', () => { + const percentCondition = { + percentOperator: PercentConditionOperator.LESS_OR_EQUAL, + microPercent: 10_000_000 // 10% + }; + const evaluator = new ConditionEvaluator(); + const truthyAssignments = evaluateRandomAssignments(percentCondition, 100_000, evaluator); + // 284 is 3 standard deviations for 100k trials with 10% probability. + const tolerance = 284; + expect(truthyAssignments).to.be.greaterThanOrEqual(10000 - tolerance); + expect(truthyAssignments).to.be.lessThanOrEqual(10000 + tolerance); + }); + + it('should evaluate between 0 to 10% to approx 10%', () => { + const percentCondition = { + percentOperator: PercentConditionOperator.BETWEEN, + microPercentRange: { + microPercentLowerBound: 0, + microPercentUpperBound: 10_000_000 + } + }; + const evaluator = new ConditionEvaluator(); + const truthyAssignments = evaluateRandomAssignments(percentCondition, 100_000, evaluator); + // 284 is 3 standard deviations for 100k trials with 10% probability. + const tolerance = 284; + expect(truthyAssignments).to.be.greaterThanOrEqual(10000 - tolerance); + expect(truthyAssignments).to.be.lessThanOrEqual(10000 + tolerance); + }); + + it('should evaluate greater than 10% to approx 90%', () => { + const percentCondition = { + percentOperator: PercentConditionOperator.GREATER_THAN, + microPercent: 10_000_000 + }; + const evaluator = new ConditionEvaluator(); + const truthyAssignments = evaluateRandomAssignments(percentCondition, 100_000, evaluator); + // 284 is 3 standard deviations for 100k trials with 90% probability. + const tolerance = 284; + expect(truthyAssignments).to.be.greaterThanOrEqual(90000 - tolerance); + expect(truthyAssignments).to.be.lessThanOrEqual(90000 + tolerance); + }); + + it('should evaluate between 40% to 60% to approx 20%', () => { + const percentCondition = { + percentOperator: PercentConditionOperator.BETWEEN, + microPercentRange: { + microPercentLowerBound: 40_000_000, + microPercentUpperBound: 60_000_000 + } + }; + const evaluator = new ConditionEvaluator(); + const truthyAssignments = evaluateRandomAssignments(percentCondition, 100_000, evaluator); + // 379 is 3 standard deviations for 100k trials with 20% probability. + const tolerance = 379; + expect(truthyAssignments).to.be.greaterThanOrEqual(20000 - tolerance); + expect(truthyAssignments).to.be.lessThanOrEqual(20000 + tolerance); + }); + + it('should evaluate between interquartile range to approx 50%', () => { + const percentCondition = { + percentOperator: PercentConditionOperator.BETWEEN, + microPercentRange: { + microPercentLowerBound: 25_000_000, + microPercentUpperBound: 75_000_000 + } + }; + const evaluator = new ConditionEvaluator(); + const truthyAssignments = evaluateRandomAssignments(percentCondition, 100_000, evaluator); + // 474 is 3 standard deviations for 100k trials with 50% probability. + const tolerance = 474; + expect(truthyAssignments).to.be.greaterThanOrEqual(50000 - tolerance); + expect(truthyAssignments).to.be.lessThanOrEqual(50000 + tolerance); + }); + + // Returns the number of assignments which evaluate to true for the specified percent condition. + // This method randomly generates the ids for each assignment for this purpose. + function evaluateRandomAssignments( + condition: PercentCondition, + numOfAssignments: number, + conditionEvaluator: ConditionEvaluator): number { + + let evalTrueCount = 0; + for (let i = 0; i < numOfAssignments; i++) { + const clonedCondition = { + ...clone(condition), + seed: 'seed' + }; + const context = { randomizationId: uuidv4() } + if (conditionEvaluator.evaluateConditions([{ + name: 'is_enabled', + condition: { percent: clonedCondition } + }], context).get('is_enabled') == true) { evalTrueCount++ } + } + return evalTrueCount; + } + }); + }); +}); diff --git a/test/unit/remote-config/index.spec.ts b/test/unit/remote-config/index.spec.ts new file mode 100644 index 0000000000..f2fd51a141 --- /dev/null +++ b/test/unit/remote-config/index.spec.ts @@ -0,0 +1,75 @@ +/*! + * @license + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +import * as chai from 'chai'; +import * as sinonChai from 'sinon-chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +import * as mocks from '../../resources/mocks'; +import { App } from '../../../src/app/index'; +import { getRemoteConfig, RemoteConfig } from '../../../src/remote-config/index'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +describe('RemoteConfig', () => { + let mockApp: App; + let mockCredentialApp: App; + + const noProjectIdError = 'Failed to determine project ID. Initialize the SDK ' + + 'with service account credentials, or set project ID as an app option. Alternatively, set the ' + + 'GOOGLE_CLOUD_PROJECT environment variable.'; + + beforeEach(() => { + mockApp = mocks.app(); + mockCredentialApp = mocks.mockCredentialApp(); + }); + + describe('getRemoteConfig()', () => { + it('should throw when default app is not available', () => { + expect(() => { + return getRemoteConfig(); + }).to.throw('The default Firebase app does not exist.'); + }); + + it('should reject given an invalid credential without project ID', () => { + // Project ID not set in the environment. + delete process.env.GOOGLE_CLOUD_PROJECT; + delete process.env.GCLOUD_PROJECT; + const remoteConfig = getRemoteConfig(mockCredentialApp); + return remoteConfig.getTemplate() + .should.eventually.rejectedWith(noProjectIdError); + }); + + it('should not throw given a valid app', () => { + expect(() => { + return getRemoteConfig(mockApp); + }).not.to.throw(); + }); + + it('should return the same instance for a given app instance', () => { + const rc1: RemoteConfig = getRemoteConfig(mockApp); + const rc2: RemoteConfig = getRemoteConfig(mockApp); + expect(rc1).to.equal(rc2); + }); + }); +}); diff --git a/test/unit/remote-config/internal/value-impl.spec.ts b/test/unit/remote-config/internal/value-impl.spec.ts new file mode 100644 index 0000000000..b344d0c9d1 --- /dev/null +++ b/test/unit/remote-config/internal/value-impl.spec.ts @@ -0,0 +1,75 @@ +/*! + * Copyright 2024 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 { ValueImpl } from '../../../../src/remote-config/internal/value-impl'; + +const expect = chai.expect; + +describe('ValueImpl', () => { + describe('getSource', () => { + it('returns the source string', () => { + const value = new ValueImpl('static'); + expect(value.getSource()).to.equal('static'); + }); + }); + + describe('asString', () => { + it('returns string value as a string', () => { + const value = new ValueImpl('default', 'shiba'); + expect(value.asString()).to.equal('shiba'); + }); + + it('defaults to empty string', () => { + const value = new ValueImpl('static'); + expect(value.asString()).to.equal(ValueImpl.DEFAULT_VALUE_FOR_STRING); + }); + }); + + describe('asNumber', () => { + it('returns numeric value as a number', () => { + const value = new ValueImpl('default', '123'); + expect(value.asNumber()).to.equal(123); + }); + + it('defaults to zero for non-numeric value', () => { + const value = new ValueImpl('default', 'Hi, NaN!'); + expect(value.asNumber()).to.equal(ValueImpl.DEFAULT_VALUE_FOR_NUMBER); + }); + }); + + describe('asBoolean', () => { + it("returns true for any value in RC's list of truthy values", () => { + for (const truthyValue of ValueImpl.BOOLEAN_TRUTHY_VALUES) { + const value = new ValueImpl('default', truthyValue); + expect(value.asBoolean()).to.be.true; + } + }); + + it('is case-insensitive', () => { + const value = new ValueImpl('default', 'TRUE'); + expect(value.asBoolean()).to.be.true; + }); + + it("returns false for any value not in RC's list of truthy values", () => { + const value = new ValueImpl('default', "I'm falsy"); + expect(value.asBoolean()).to.be.false; + }); + }); +}); + diff --git a/test/unit/remote-config/remote-config-api-client.spec.ts b/test/unit/remote-config/remote-config-api-client.spec.ts new file mode 100644 index 0000000000..52abb968c1 --- /dev/null +++ b/test/unit/remote-config/remote-config-api-client.spec.ts @@ -0,0 +1,822 @@ +/*! + * 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 strict'; + +import * as _ from 'lodash'; +import * as chai from 'chai'; +import * as sinon from 'sinon'; +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'; +import { FirebaseAppError } from '../../../src/utils/error'; +import { FirebaseApp } from '../../../src/app/firebase-app'; +import { deepCopy } from '../../../src/utils/deep-copy'; +import { getSdkVersion } from '../../../src/utils/index'; +import { + RemoteConfigTemplate, Version, ListVersionsResult, +} from '../../../src/remote-config/index'; +import { ServerTemplateData } from '../../../src/remote-config/remote-config-api'; + +const expect = chai.expect; + +describe('RemoteConfigApiClient', () => { + + const ERROR_RESPONSE = { + error: { + code: 404, + message: 'Requested entity not found', + status: 'NOT_FOUND', + }, + }; + + 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' + ]; + + const EXPECTED_HEADERS = { + 'Authorization': 'Bearer mock-token', + 'X-Firebase-Client': `fire-admin-node/${getSdkVersion()}`, + 'Accept-Encoding': 'gzip', + }; + + const VERSION_INFO: Version = { + versionNumber: '86', + updateOrigin: 'ADMIN_SDK_NODE', + updateType: 'INCREMENTAL_UPDATE', + updateUser: { + email: 'firebase-adminsdk@gserviceaccount.com' + }, + description: 'production version', + updateTime: '2020-06-15T16:45:03.000Z', + } + + const TEST_RESPONSE = { + conditions: [{ name: 'ios', expression: 'exp' }], + parameters: { param: { defaultValue: { value: 'true' }, valueType: 'BOOLEAN' } }, + parameterGroups: { group: { parameters: { paramabc: { defaultValue: { value: 'true' } } }, } }, + version: VERSION_INFO, + }; + + const TEST_VERSIONS_RESULT: ListVersionsResult = { + versions: [ + { + versionNumber: '78', + updateTime: '2020-05-07T18:46:09.495Z', + updateUser: { + email: 'user@gmail.com', + imageUrl: 'https://photo.jpg' + }, + description: 'Rollback to version 76', + updateOrigin: 'REST_API', + updateType: 'ROLLBACK', + rollbackSource: '76' + }, + { + versionNumber: '77', + updateTime: '2020-05-07T18:44:41.555Z', + updateUser: { + email: 'user@gmail.com', + imageUrl: 'https://photo.jpg' + }, + updateOrigin: 'REST_API', + updateType: 'INCREMENTAL_UPDATE', + }, + ], + nextPageToken: '76' + } + + 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 mockOptions = { + credential: new mocks.MockCredential(), + projectId: 'test-project', + }; + + const clientWithoutProjectId = new RemoteConfigApiClient( + mocks.mockCredentialApp()); + + const REMOTE_CONFIG_TEMPLATE: RemoteConfigTemplate = { + conditions: [ + { + name: 'ios', + expression: 'device.os == \'ios\'', + tagColor: 'PINK', + }, + ], + parameters: { + holiday_promo_enabled: { + defaultValue: { value: 'true' }, + conditionalValues: { ios: { useInAppDefault: true } }, + description: 'this is a promo', + valueType: 'BOOLEAN' + }, + }, + parameterGroups: { + new_menu: { + description: 'Description of the group.', + parameters: { + pumpkin_spice_season: { + defaultValue: { value: 'A Gryffindor must love a pumpkin spice latte.' }, + conditionalValues: { + 'android_en': { value: 'A Droid must love a pumpkin spice latte.' }, + }, + description: 'Description of the parameter.', + }, + }, + }, + }, + etag: 'etag-123456789012-6', + version: { + description: 'production version' + } + }; + + // Stubs used to simulate underlying api calls. + let stubs: sinon.SinonStub[] = []; + let app: FirebaseApp; + let apiClient: RemoteConfigApiClient; + + beforeEach(() => { + app = mocks.appWithOptions(mockOptions); + apiClient = new RemoteConfigApiClient(app); + }); + + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + return app.delete(); + }); + + describe('Constructor', () => { + it('should reject when the app is null', () => { + expect(() => new RemoteConfigApiClient(null as unknown as FirebaseApp)) + .to.throw('First argument passed to admin.remoteConfig() must be a valid Firebase app instance.'); + }); + }); + + describe('getTemplate', () => { + it('should reject when project id is not available', () => { + return clientWithoutProjectId.getTemplate() + .should.eventually.be.rejectedWith(noProjectId); + }); + + // tests for api response validations + runEtagHeaderTests(() => apiClient.getTemplate()); + runErrorResponseTests(() => apiClient.getTemplate()); + + it('should resolve with the latest template on success', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(TEST_RESPONSE, 200, { etag: 'etag-123456789012-1' })); + stubs.push(stub); + return apiClient.getTemplate() + .then((resp) => { + expect(resp.conditions).to.deep.equal(TEST_RESPONSE.conditions); + expect(resp.parameters).to.deep.equal(TEST_RESPONSE.parameters); + expect(resp.parameterGroups).to.deep.equal(TEST_RESPONSE.parameterGroups); + expect(resp.etag).to.equal('etag-123456789012-1'); + expect(resp.version).to.deep.equal(TEST_RESPONSE.version); + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'GET', + url: 'https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/remoteConfig', + headers: EXPECTED_HEADERS, + }); + }); + }); + }); + + describe('getTemplateAtVersion', () => { + it('should reject when project id is not available', () => { + return clientWithoutProjectId.getTemplateAtVersion(65) + .should.eventually.be.rejectedWith(noProjectId); + }); + + // test for version number validations + runTemplateVersionNumberTests((v: string | number) => { apiClient.getTemplateAtVersion(v); }); + + // tests for api response validations + runEtagHeaderTests(() => apiClient.getTemplateAtVersion(65)); + runErrorResponseTests(() => apiClient.getTemplateAtVersion(65)); + + it('should convert version number to string', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(TEST_RESPONSE, 200, { etag: 'etag-123456789012-60' })); + stubs.push(stub); + return apiClient.getTemplateAtVersion(60) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'GET', + url: 'https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/remoteConfig', + headers: EXPECTED_HEADERS, + data: { versionNumber: '60' }, + }); + }); + }); + + it('should resolve with the requested template version on success', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(TEST_RESPONSE, 200, { etag: 'etag-123456789012-60' })); + stubs.push(stub); + return apiClient.getTemplateAtVersion('60') + .then((resp) => { + expect(resp.conditions).to.deep.equal(TEST_RESPONSE.conditions); + expect(resp.parameters).to.deep.equal(TEST_RESPONSE.parameters); + expect(resp.parameterGroups).to.deep.equal(TEST_RESPONSE.parameterGroups); + expect(resp.etag).to.equal('etag-123456789012-60'); + expect(resp.version).to.deep.equal(TEST_RESPONSE.version); + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'GET', + url: 'https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/remoteConfig', + headers: EXPECTED_HEADERS, + data: { versionNumber: '60' }, + }); + }); + }); + }); + + describe('validateTemplate', () => { + it('should reject when project id is not available', () => { + return clientWithoutProjectId.validateTemplate(REMOTE_CONFIG_TEMPLATE) + .should.eventually.be.rejectedWith(noProjectId); + }); + + // tests for input template validations + testInvalidInputTemplates((t: RemoteConfigTemplate) => apiClient.validateTemplate(t)); + + // tests for api response validations + runEtagHeaderTests(() => apiClient.validateTemplate(REMOTE_CONFIG_TEMPLATE)); + runErrorResponseTests(() => apiClient.validateTemplate(REMOTE_CONFIG_TEMPLATE)); + + it('should exclude output only parameters from version metadata', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(TEST_RESPONSE, 200, { etag: 'etag-123456789012-0' })); + stubs.push(stub); + const templateCopy = deepCopy(REMOTE_CONFIG_TEMPLATE); + templateCopy.version = VERSION_INFO; + return apiClient.validateTemplate(templateCopy) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'PUT', + url: 'https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/remoteConfig?validate_only=true', + headers: { ...EXPECTED_HEADERS, 'If-Match': REMOTE_CONFIG_TEMPLATE.etag }, + data: { + conditions: REMOTE_CONFIG_TEMPLATE.conditions, + parameters: REMOTE_CONFIG_TEMPLATE.parameters, + parameterGroups: REMOTE_CONFIG_TEMPLATE.parameterGroups, + version: { description: VERSION_INFO.description }, + } + }); + }); + }); + + it('should resolve with the requested template on success', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(TEST_RESPONSE, 200, { etag: 'etag-123456789012-0' })); + stubs.push(stub); + return apiClient.validateTemplate(REMOTE_CONFIG_TEMPLATE) + .then((resp) => { + expect(resp.conditions).to.deep.equal(TEST_RESPONSE.conditions); + expect(resp.parameters).to.deep.equal(TEST_RESPONSE.parameters); + expect(resp.parameterGroups).to.deep.equal(TEST_RESPONSE.parameterGroups); + // validate template returns an etag with the suffix -0 when successful. + // verify that the etag matches the original template etag. + expect(resp.etag).to.equal('etag-123456789012-6'); + expect(resp.version).to.deep.equal(TEST_RESPONSE.version); + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'PUT', + url: 'https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/remoteConfig?validate_only=true', + headers: { ...EXPECTED_HEADERS, 'If-Match': REMOTE_CONFIG_TEMPLATE.etag }, + data: { + conditions: REMOTE_CONFIG_TEMPLATE.conditions, + parameters: REMOTE_CONFIG_TEMPLATE.parameters, + parameterGroups: REMOTE_CONFIG_TEMPLATE.parameterGroups, + version: REMOTE_CONFIG_TEMPLATE.version, + } + }); + }); + }); + + [null, undefined, ''].forEach((etag) => { + it('should reject when the etag in template is null, undefined, or an empty string', () => { + expect(() => apiClient.validateTemplate({ + conditions: [], parameters: {}, parameterGroups: {}, etag: etag as any + })).to.throw('ETag must be a non-empty string.'); + }); + }); + + VALIDATION_ERROR_MESSAGES.forEach((message) => { + it('should reject with failed-precondition when a validation error occurres', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .rejects(utils.errorFrom({ + error: { + code: 400, + message: message, + status: 'FAILED_PRECONDITION' + } + }, 400)); + stubs.push(stub); + const expected = new FirebaseRemoteConfigError('failed-precondition', message); + return apiClient.validateTemplate(REMOTE_CONFIG_TEMPLATE) + .should.eventually.be.rejected.and.deep.include(expected); + }); + }); + }); + + describe('publishTemplate', () => { + it('should reject when project id is not available', () => { + return clientWithoutProjectId.publishTemplate(REMOTE_CONFIG_TEMPLATE) + .should.eventually.be.rejectedWith(noProjectId); + }); + + // tests for input template validations + testInvalidInputTemplates((t: RemoteConfigTemplate) => apiClient.publishTemplate(t)); + + // tests for api response validations + runEtagHeaderTests(() => apiClient.publishTemplate(REMOTE_CONFIG_TEMPLATE)); + runErrorResponseTests(() => apiClient.publishTemplate(REMOTE_CONFIG_TEMPLATE)); + + it('should exclude output only parameters from version metadata', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(TEST_RESPONSE, 200, { etag: 'etag-123456789012-6' })); + stubs.push(stub); + const templateCopy = deepCopy(REMOTE_CONFIG_TEMPLATE); + templateCopy.version = VERSION_INFO; + return apiClient.publishTemplate(templateCopy) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'PUT', + url: 'https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/remoteConfig', + headers: { ...EXPECTED_HEADERS, 'If-Match': REMOTE_CONFIG_TEMPLATE.etag }, + data: { + conditions: REMOTE_CONFIG_TEMPLATE.conditions, + parameters: REMOTE_CONFIG_TEMPLATE.parameters, + parameterGroups: REMOTE_CONFIG_TEMPLATE.parameterGroups, + version: { description: VERSION_INFO.description }, + } + }); + }); + }); + + const testOptions = [ + { options: undefined, etag: 'etag-123456789012-6' }, + { options: { force: true }, etag: '*' } + ]; + testOptions.forEach((option) => { + it('should resolve with the published template on success', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(TEST_RESPONSE, 200, { etag: 'etag-123456789012-6' })); + stubs.push(stub); + return apiClient.publishTemplate(REMOTE_CONFIG_TEMPLATE, option.options) + .then((resp) => { + expect(resp.conditions).to.deep.equal(TEST_RESPONSE.conditions); + expect(resp.parameters).to.deep.equal(TEST_RESPONSE.parameters); + expect(resp.parameterGroups).to.deep.equal(TEST_RESPONSE.parameterGroups); + expect(resp.etag).to.equal('etag-123456789012-6'); + expect(resp.version).to.deep.equal(TEST_RESPONSE.version); + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'PUT', + url: 'https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/remoteConfig', + headers: { ...EXPECTED_HEADERS, 'If-Match': option.etag }, + data: { + conditions: REMOTE_CONFIG_TEMPLATE.conditions, + parameters: REMOTE_CONFIG_TEMPLATE.parameters, + parameterGroups: REMOTE_CONFIG_TEMPLATE.parameterGroups, + version: REMOTE_CONFIG_TEMPLATE.version, + } + }); + }); + }); + }); + + [null, undefined, ''].forEach((etag) => { + it('should reject when the etag in template is null, undefined, or an empty string', () => { + expect(() => apiClient.publishTemplate({ + conditions: [], parameters: {}, parameterGroups: {}, etag: etag as any + })).to.throw('ETag must be a non-empty string.'); + }); + }); + + VALIDATION_ERROR_MESSAGES.forEach((message) => { + it('should reject with failed-precondition when a validation error occurs', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .rejects(utils.errorFrom({ + error: { + code: 400, + message: message, + status: 'FAILED_PRECONDITION' + } + }, 400)); + stubs.push(stub); + const expected = new FirebaseRemoteConfigError('failed-precondition', message); + return apiClient.publishTemplate(REMOTE_CONFIG_TEMPLATE) + .should.eventually.be.rejected.and.deep.include(expected); + }); + }); + }); + + describe('rollback', () => { + it('should reject when project id is not available', () => { + return clientWithoutProjectId.rollback('60') + .should.eventually.be.rejectedWith(noProjectId); + }); + + // test for version number validations + runTemplateVersionNumberTests((v: string | number) => { apiClient.rollback(v); }); + + // tests for api response validations + runEtagHeaderTests(() => apiClient.rollback(60)); + runErrorResponseTests(() => apiClient.rollback(60)); + + it('should convert version number to string', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(TEST_RESPONSE, 200, { etag: 'etag-123456789012-55' })); + stubs.push(stub); + return apiClient.rollback(55) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/remoteConfig:rollback', + headers: EXPECTED_HEADERS, + data: { + versionNumber: '55', + } + }); + }); + }); + + it('should resolve with the rollbacked template on success', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(TEST_RESPONSE, 200, { etag: 'etag-123456789012-60' })); + stubs.push(stub); + return apiClient.rollback('60') + .then((resp) => { + expect(resp.conditions).to.deep.equal(TEST_RESPONSE.conditions); + expect(resp.parameters).to.deep.equal(TEST_RESPONSE.parameters); + expect(resp.parameterGroups).to.deep.equal(TEST_RESPONSE.parameterGroups); + expect(resp.etag).to.equal('etag-123456789012-60'); + expect(resp.version).to.deep.equal(TEST_RESPONSE.version); + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/remoteConfig:rollback', + headers: EXPECTED_HEADERS, + data: { + versionNumber: '60', + } + }); + }); + }); + }); + + describe('listVersions', () => { + it('should reject when project id is not available', () => { + return clientWithoutProjectId.listVersions() + .should.eventually.be.rejectedWith(noProjectId); + }); + + // tests for api response validations + runErrorResponseTests(() => apiClient.listVersions()); + + [null, 'abc', '', [], true, 102, 1.2].forEach((invalidOption) => { + it(`should throw if options is ${invalidOption}`, () => { + expect(() => apiClient.listVersions(invalidOption as any)) + .to.throw('ListVersionsOptions must be a non-null object'); + }); + }); + + [null, 'abc', '', [], {}, true, NaN, 0, -100, 301, 450].forEach((invalidPageSize) => { + it(`should throw if pageSize is ${invalidPageSize}`, () => { + expect(() => apiClient.listVersions({ pageSize: invalidPageSize } as any)) + .to.throw(/^pageSize must be a (number.|number between 1 and 300 \(inclusive\).)$/); + }); + }); + + [null, '', 102, 1.2, [], {}, true, NaN].forEach((invalidPageToken) => { + it(`should throw if pageToken is ${invalidPageToken}`, () => { + expect(() => apiClient.listVersions({ pageToken: invalidPageToken } as any)) + .to.throw('pageToken must be a string value'); + }); + }); + + ['', 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 an integer or a string in int64 format$/); + }); + }); + + [null, '', 'abc', '2020-05-07T18:44:41.555Z', 102, 1.2, [], {}, true, NaN].forEach( + (invalidStartTime) => { + it(`should throw if startTime is ${invalidStartTime}`, () => { + expect(() => apiClient.listVersions({ startTime: invalidStartTime } as any)) + .to.throw('startTime must be a valid Date object or a UTC date string.'); + }); + }); + + [null, '', 'abc', '2020-05-07T18:44:41.555Z', 102, 1.2, [], {}, true, NaN].forEach( + (invalidEndTime) => { + it(`should throw if endTime is ${invalidEndTime}`, () => { + expect(() => apiClient.listVersions({ endTime: invalidEndTime } as any)) + .to.throw('endTime must be a valid Date object or a UTC date string.'); + }); + }); + + it('should convert input timestamps to ISO strings', () => { + const startTime = new Date(2020, 4, 2); + const endTime = 'Thu, 07 May 2020 18:44:41 GMT'; + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(TEST_VERSIONS_RESULT, 200)); + stubs.push(stub); + return apiClient.listVersions({ + startTime, + endTime, + }) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'GET', + url: 'https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/remoteConfig:listVersions', + headers: EXPECTED_HEADERS, + data: { + // timestamps should be converted to ISO strings + startTime: startTime.toISOString(), + endTime: new Date(endTime).toISOString(), + } + }); + }); + }); + + it('should convert endVersionNumber to string', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(TEST_VERSIONS_RESULT, 200)); + stubs.push(stub); + return apiClient.listVersions({ + endVersionNumber: 70 + }) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'GET', + url: 'https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/remoteConfig:listVersions', + headers: EXPECTED_HEADERS, + data: { + // endVersionNumber should be converted to string + endVersionNumber: '70' + } + }); + }); + }); + + it('should remove undefined fields from options', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(TEST_VERSIONS_RESULT, 200)); + stubs.push(stub); + return apiClient.listVersions({ + pageSize: undefined, + pageToken: undefined, + endVersionNumber: 70, + }) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'GET', + url: 'https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/remoteConfig:listVersions', + headers: EXPECTED_HEADERS, + data: { + endVersionNumber: '70' + } + }); + }); + }); + + it('should resolve with a list of template versions on success', () => { + const startTime = new Date(2020, 4, 2); + const endTime = 'Thu, 07 May 2020 18:44:41 GMT'; + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(TEST_VERSIONS_RESULT, 200)); + stubs.push(stub); + return apiClient.listVersions({ + pageSize: 2, + pageToken: '70', + endVersionNumber: '78', + startTime: startTime, + endTime: endTime, + }) + .then((resp) => { + expect(resp.versions).to.deep.equal(TEST_VERSIONS_RESULT.versions); + expect(resp.nextPageToken).to.equal(TEST_VERSIONS_RESULT.nextPageToken); + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'GET', + url: 'https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/remoteConfig:listVersions', + headers: EXPECTED_HEADERS, + data: { + pageSize: 2, + pageToken: '70', + endVersionNumber: '78', + startTime: startTime.toISOString(), + endTime: new Date(endTime).toISOString(), + } + }); + }); + }); + }); + + describe('getServerTemplate', () => { + it('should reject when project id is not available', () => { + return clientWithoutProjectId.getServerTemplate() + .should.eventually.be.rejectedWith(noProjectId); + }); + + // tests for api response validations + runEtagHeaderTests(() => apiClient.getServerTemplate()); + runErrorResponseTests(() => apiClient.getServerTemplate()); + + it('should resolve with the latest template on success', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(TEST_RESPONSE, 200, { etag: 'etag-123456789012-1' })); + stubs.push(stub); + return apiClient.getServerTemplate() + .then((resp) => { + expect(resp.conditions).to.deep.equal(TEST_RESPONSE.conditions); + expect(resp.parameters).to.deep.equal(TEST_RESPONSE.parameters); + expect(resp.etag).to.equal('etag-123456789012-1'); + expect(resp.version).to.deep.equal(TEST_RESPONSE.version); + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'GET', + url: 'https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/namespaces/firebase-server/serverRemoteConfig', + headers: EXPECTED_HEADERS, + }); + }); + }); + }); + + function runTemplateVersionNumberTests(rcOperation: (v: string | number) => any): void { + ['', 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 an integer or a string in int64 format$/); + }); + }); + } + + function runEtagHeaderTests(rcOperation: () => Promise): void { + it('should reject when the etag is not present in the response', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(TEST_RESPONSE)); + stubs.push(stub); + const expected = new FirebaseRemoteConfigError('invalid-argument', + 'ETag header is not present in the server response.'); + return rcOperation() + .should.eventually.be.rejected.and.deep.include(expected); + }); + } + + function runErrorResponseTests( + rcOperation: () => Promise): void { + 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 FirebaseRemoteConfigError('not-found', 'Requested entity not found'); + return rcOperation() + .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 FirebaseRemoteConfigError('unknown-error', 'Unknown server error: {}'); + return rcOperation() + .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 FirebaseRemoteConfigError( + 'unknown-error', 'Unexpected response with status: 404 and body: not json'); + return rcOperation() + .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 rcOperation() + .should.eventually.be.rejected.and.deep.include(expected); + }); + } + + function testInvalidInputTemplates(rcOperation: (t: RemoteConfigTemplate) => any): void { + const INVALID_PARAMETERS: any[] = [null, '', 'abc', 1, true, []]; + const INVALID_PARAMETER_GROUPS: any[] = [null, '', 'abc', 1, true, []]; + const INVALID_CONDITIONS: any[] = [null, '', 'abc', 1, true, {}]; + const INVALID_ETAG_TEMPLATES: any[] = [ + { parameters: {}, parameterGroups: {}, conditions: [], etag: '' }, + Object() + ]; + const INVALID_TEMPLATES: any[] = [null, 'abc', 123]; + const inputTemplate = deepCopy(REMOTE_CONFIG_TEMPLATE); + + INVALID_PARAMETERS.forEach((invalidParameter) => { + it(`should throw if the parameters is ${JSON.stringify(invalidParameter)}`, () => { + (inputTemplate as any).parameters = invalidParameter; + inputTemplate.conditions = []; + expect(() => rcOperation(inputTemplate)) + .to.throw('Remote Config parameters must be a non-null object'); + }); + }); + + INVALID_PARAMETER_GROUPS.forEach((invalidParameterGroup) => { + it(`should throw if the parameter groups is ${JSON.stringify(invalidParameterGroup)}`, () => { + (inputTemplate as any).parameterGroups = invalidParameterGroup; + inputTemplate.conditions = []; + inputTemplate.parameters = {}; + expect(() => rcOperation(inputTemplate)) + .to.throw('Remote Config parameter groups must be a non-null object'); + }); + }); + + INVALID_CONDITIONS.forEach((invalidConditions) => { + it(`should throw if the conditions is ${JSON.stringify(invalidConditions)}`, () => { + (inputTemplate as any).conditions = invalidConditions; + inputTemplate.parameters = {}; + inputTemplate.parameterGroups = {}; + expect(() => rcOperation(inputTemplate)) + .to.throw('Remote Config conditions must be an array'); + }); + }); + + INVALID_ETAG_TEMPLATES.forEach((invalidEtagTemplate) => { + it(`should throw if the template is ${JSON.stringify(invalidEtagTemplate)}`, () => { + expect(() => rcOperation(invalidEtagTemplate)) + .to.throw('ETag must be a non-empty string.'); + }); + }); + + INVALID_TEMPLATES.forEach((invalidTemplate) => { + it(`should throw if the template is ${JSON.stringify(invalidTemplate)}`, () => { + expect(() => rcOperation(invalidTemplate)) + .to.throw(`Invalid Remote Config template: ${JSON.stringify(invalidTemplate)}`); + }); + }); + } +}); diff --git a/test/unit/remote-config/remote-config.spec.ts b/test/unit/remote-config/remote-config.spec.ts new file mode 100644 index 0000000000..526dc0699e --- /dev/null +++ b/test/unit/remote-config/remote-config.spec.ts @@ -0,0 +1,1644 @@ +/*! + * 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 strict'; + +import * as _ from 'lodash'; +import * as chai from 'chai'; +import * as sinon from 'sinon'; +import { + ParameterValueType, + RemoteConfig, + RemoteConfigTemplate, + RemoteConfigCondition, + TagColor, + ListVersionsResult, +} from '../../../src/remote-config/index'; +import { FirebaseApp } from '../../../src/app/firebase-app'; +import * as mocks from '../../resources/mocks'; +import { + FirebaseRemoteConfigError, + RemoteConfigApiClient +} from '../../../src/remote-config/remote-config-api-client-internal'; +import { deepCopy } from '../../../src/utils/deep-copy'; +import { + NamedCondition, ServerTemplate, ServerTemplateData, Version +} from '../../../src/remote-config/remote-config-api'; + +const expect = chai.expect; + +describe('RemoteConfig', () => { + + const INTERNAL_ERROR = new FirebaseRemoteConfigError('internal-error', 'message'); + const PARAMETER_GROUPS = { + new_menu: { + description: 'Description of the group.', + parameters: { + pumpkin_spice_season: { + defaultValue: { value: 'A Gryffindor must love a pumpkin spice latte.' }, + conditionalValues: { + 'android_en': { value: 'A Droid must love a pumpkin spice latte.' }, + }, + description: 'Description of the parameter.', + valueType: 'STRING' as ParameterValueType, + }, + }, + }, + }; + + const VERSION_INFO = { + versionNumber: '86', + updateOrigin: 'ADMIN_SDK_NODE', + updateType: 'INCREMENTAL_UPDATE', + updateUser: { + email: 'firebase-adminsdk@gserviceaccount.com' + }, + description: 'production version', + updateTime: '2020-06-15T16:45:03.541527Z' + }; + + const REMOTE_CONFIG_RESPONSE: { + // This type is effectively a RemoteConfigTemplate, 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 RemoteConfigTemplateContent based + // on the needs of the test, as that would ensure type-safety. + conditions?: Array<{ name: string; expression: string; tagColor: TagColor }>; + parameters?: object | null; + parameterGroups?: object | null; + etag: string; + version?: object; + } = { + conditions: [ + { + name: 'ios', + expression: 'device.os == \'ios\'', + tagColor: 'BLUE', + }, + ], + parameters: { + holiday_promo_enabled: { + defaultValue: { value: 'true' }, + conditionalValues: { ios: { useInAppDefault: true } }, + description: 'this is a promo', + valueType: 'BOOLEAN', + }, + }, + parameterGroups: PARAMETER_GROUPS, + etag: 'etag-123456789012-5', + version: VERSION_INFO, + }; + + const SERVER_REMOTE_CONFIG_RESPONSE: { + // This type is effectively a RemoteConfigServerTemplate, but with mutable fields + // to allow easier use from within the tests. An improvement would be to + // alter this into a helper that creates customized RemoteConfigTemplateContent based + // on the needs of the test, as that would ensure type-safety. + conditions?: Array; + parameters?: object | null; + etag: string; + version?: object; + } = { + conditions: [ + { + name: 'ios', + condition: { + orCondition: { + conditions: [ + { + andCondition: { + conditions: [ + { true: {} } + ] + } + } + ] + } + } + }, + ], + parameters: { + holiday_promo_enabled: { + defaultValue: { value: 'true' }, + conditionalValues: { ios: { useInAppDefault: true } } + }, + }, + etag: 'etag-123456789012-5', + version: VERSION_INFO, + }; + + const REMOTE_CONFIG_TEMPLATE: RemoteConfigTemplate = { + conditions: [{ + name: 'ios', + expression: 'device.os == \'ios\'', + tagColor: 'PINK', + }], + parameters: { + holiday_promo_enabled: { + defaultValue: { value: 'true' }, + conditionalValues: { ios: { useInAppDefault: true } }, + description: 'this is a promo', + valueType: 'BOOLEAN', + }, + }, + parameterGroups: PARAMETER_GROUPS, + etag: 'etag-123456789012-6', + version: { + description: 'production version', + } + }; + + const REMOTE_CONFIG_LIST_VERSIONS_RESULT: ListVersionsResult = { + versions: [ + { + versionNumber: '78', + updateTime: '2020-05-07T18:46:09.495234Z', + updateUser: { + email: 'user@gmail.com', + imageUrl: 'https://photo.jpg' + }, + description: 'Rollback to version 76', + updateOrigin: 'REST_API', + updateType: 'ROLLBACK', + rollbackSource: '76' + }, + { + versionNumber: '77', + updateTime: '2020-05-07T18:44:41.555Z', + updateUser: { + email: 'user@gmail.com', + imageUrl: 'https://photo.jpg' + }, + updateOrigin: 'REST_API', + updateType: 'INCREMENTAL_UPDATE', + }, + ], + nextPageToken: '76' + } + + let remoteConfig: RemoteConfig; + + let mockApp: FirebaseApp; + let mockCredentialApp: FirebaseApp; + + // Stubs used to simulate underlying api calls. + let stubs: sinon.SinonStub[] = []; + + before(() => { + mockApp = mocks.app(); + mockCredentialApp = mocks.mockCredentialApp(); + remoteConfig = new RemoteConfig(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 remoteConfigAny: any = RemoteConfig; + return new remoteConfigAny(invalidApp); + }).to.throw( + 'First argument passed to admin.remoteConfig() must be a valid Firebase app ' + + 'instance.'); + }); + }); + + it('should throw given no app', () => { + expect(() => { + const remoteConfigAny: any = RemoteConfig; + return new remoteConfigAny(); + }).to.throw( + 'First argument passed to admin.remoteConfig() 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 remoteConfigWithoutProjectId = new RemoteConfig(mockCredentialApp); + return remoteConfigWithoutProjectId.getTemplate() + .should.eventually.rejectedWith(noProjectId); + }); + + it('should not throw given a valid app', () => { + expect(() => { + return new RemoteConfig(mockApp); + }).not.to.throw(); + }); + }); + + describe('app', () => { + it('returns the app from the constructor', () => { + // We expect referential equality here + expect(remoteConfig.app).to.equal(mockApp); + }); + }); + + describe('getTemplate', () => { + runInvalidResponseTests(() => remoteConfig.getTemplate(), 'getTemplate'); + runValidResponseTests(() => remoteConfig.getTemplate(), 'getTemplate'); + }); + + describe('getTemplateAtVersion', () => { + runInvalidResponseTests(() => remoteConfig.getTemplateAtVersion(65), 'getTemplateAtVersion'); + runValidResponseTests(() => remoteConfig.getTemplateAtVersion(65), 'getTemplateAtVersion'); + }); + + describe('validateTemplate', () => { + runInvalidResponseTests(() => remoteConfig.validateTemplate(REMOTE_CONFIG_TEMPLATE), + 'validateTemplate'); + runValidResponseTests(() => remoteConfig.validateTemplate(REMOTE_CONFIG_TEMPLATE), + 'validateTemplate'); + }); + + describe('publishTemplate', () => { + runInvalidResponseTests(() => remoteConfig.publishTemplate(REMOTE_CONFIG_TEMPLATE), + 'publishTemplate'); + runValidResponseTests(() => remoteConfig.publishTemplate(REMOTE_CONFIG_TEMPLATE), + 'publishTemplate'); + }); + + describe('rollback', () => { + runInvalidResponseTests(() => remoteConfig.rollback('5'), 'rollback'); + runValidResponseTests(() => remoteConfig.rollback('5'), 'rollback'); + }); + + describe('listVersions', () => { + it('should propagate API errors', () => { + const stub = sinon + .stub(RemoteConfigApiClient.prototype, 'listVersions') + .rejects(INTERNAL_ERROR); + stubs.push(stub); + return remoteConfig.listVersions() + .should.eventually.be.rejected.and.deep.equal(INTERNAL_ERROR); + }); + + ['', 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; + 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 an integer or a string in int64 format$/); + }); + }); + + ['', 123, 1.2, null, NaN, true, [], {}].forEach((invalidUpdateOrigin) => { + it(`should reject if the updateOrigin is: ${invalidUpdateOrigin}`, () => { + const response = deepCopy(REMOTE_CONFIG_LIST_VERSIONS_RESULT); + response.versions[0].updateOrigin = invalidUpdateOrigin as any; + const stub = sinon + .stub(RemoteConfigApiClient.prototype, 'listVersions') + .resolves(response); + stubs.push(stub); + return remoteConfig.listVersions() + .should.eventually.be.rejected.and.have.property('message', + 'Version update origin must be a non-empty string'); + }); + }); + + ['', 123, 1.2, null, NaN, true, [], {}].forEach((invalidUpdateType) => { + it(`should reject if the updateType is: ${invalidUpdateType}`, () => { + const response = deepCopy(REMOTE_CONFIG_LIST_VERSIONS_RESULT); + response.versions[0].updateType = invalidUpdateType as any; + const stub = sinon + .stub(RemoteConfigApiClient.prototype, 'listVersions') + .resolves(response); + stubs.push(stub); + return remoteConfig.listVersions() + .should.eventually.be.rejected.and.have.property('message', + 'Version update type must be a non-empty string'); + }); + }); + + ['', 'abc', 1.2, 123, null, NaN, true, []].forEach((invalidUpdateUser) => { + it(`should reject if the updateUser is: ${invalidUpdateUser}`, () => { + const response = deepCopy(REMOTE_CONFIG_LIST_VERSIONS_RESULT); + response.versions[0].updateUser = invalidUpdateUser as any; + const stub = sinon + .stub(RemoteConfigApiClient.prototype, 'listVersions') + .resolves(response); + stubs.push(stub); + return remoteConfig.listVersions() + .should.eventually.be.rejected.and.have.property('message', + 'Version update user must be a non-null object'); + }); + }); + + ['', 123, 1.2, null, NaN, true, [], {}].forEach((invalidDescription) => { + it(`should reject if the description is: ${invalidDescription}`, () => { + const response = deepCopy(REMOTE_CONFIG_LIST_VERSIONS_RESULT); + response.versions[0].description = invalidDescription as any; + const stub = sinon + .stub(RemoteConfigApiClient.prototype, 'listVersions') + .resolves(response); + stubs.push(stub); + return remoteConfig.listVersions() + .should.eventually.be.rejected.and.have.property('message', + 'Version description must be a non-empty string'); + }); + }); + + ['', 123, 1.2, null, NaN, true, [], {}].forEach((invalidRollbackSource) => { + it(`should reject if the rollbackSource is: ${invalidRollbackSource}`, () => { + const response = deepCopy(REMOTE_CONFIG_LIST_VERSIONS_RESULT); + response.versions[0].rollbackSource = invalidRollbackSource as any; + const stub = sinon + .stub(RemoteConfigApiClient.prototype, 'listVersions') + .resolves(response); + stubs.push(stub); + return remoteConfig.listVersions() + .should.eventually.be.rejected.and.have.property('message', + 'Version rollback source must be a non-empty string'); + }); + }); + + ['', 'abc', 123, 1.2, null, NaN, [], {}].forEach((invalidIsLegacy) => { + it(`should reject if the isLegacy is: ${invalidIsLegacy}`, () => { + const response = deepCopy(REMOTE_CONFIG_LIST_VERSIONS_RESULT); + response.versions[0].isLegacy = invalidIsLegacy as any; + const stub = sinon + .stub(RemoteConfigApiClient.prototype, 'listVersions') + .resolves(response); + stubs.push(stub); + return remoteConfig.listVersions() + .should.eventually.be.rejected.and.have.property('message', + 'Version.isLegacy must be a boolean'); + }); + }); + + ['', 'abc', 123, 1.2, null, NaN, [], {}].forEach((invalidUpdateTime) => { + it(`should reject if the updateTime is: ${invalidUpdateTime}`, () => { + const response = deepCopy(REMOTE_CONFIG_LIST_VERSIONS_RESULT); + response.versions[0].updateTime = invalidUpdateTime as any; + const stub = sinon + .stub(RemoteConfigApiClient.prototype, 'listVersions') + .resolves(response); + stubs.push(stub); + return remoteConfig.listVersions() + .should.eventually.be.rejected.and.have.property('message', + 'Version update time must be a valid date string'); + }); + }); + + it('should resolve with an empty versions list if no results are available for requested list options', () => { + const stub = sinon + .stub(RemoteConfigApiClient.prototype, 'listVersions') + .resolves({} as any); + stubs.push(stub); + return remoteConfig.listVersions({ + pageSize: 2, + endVersionNumber: 10, + }) + .then((response) => { + expect(response.versions.length).to.equal(0); + expect(response.nextPageToken).to.be.undefined; + }); + }); + + it('should resolve with template versions list on success', () => { + const stub = sinon + .stub(RemoteConfigApiClient.prototype, 'listVersions') + .resolves(REMOTE_CONFIG_LIST_VERSIONS_RESULT); + stubs.push(stub); + return remoteConfig.listVersions({ + pageSize: 2 + }) + .then((response) => { + expect(response.versions.length).to.equal(2); + expect(response.versions[0].updateTime).equals('Thu, 07 May 2020 18:46:09 GMT'); + expect(response.versions[1].updateTime).equals('Thu, 07 May 2020 18:44:41 GMT'); + expect(response.nextPageToken).to.equal('76'); + }); + }); + }); + + const INVALID_PARAMETERS: any[] = [null, '', 'abc', 1, true, []]; + const INVALID_PARAMETER_GROUPS: any[] = [null, '', 'abc', 1, true, []]; + const INVALID_CONDITIONS: any[] = [null, '', 'abc', 1, true, {}]; + + describe('createTemplateFromJSON', () => { + const INVALID_STRINGS: any[] = [null, undefined, '', 1, true, {}, []]; + const INVALID_JSON_STRINGS: any[] = ['abc', 'foo', 'a:a', '1:1']; + + INVALID_STRINGS.forEach((invalidJson) => { + it(`should throw if the json string is ${JSON.stringify(invalidJson)}`, () => { + expect(() => remoteConfig.createTemplateFromJSON(invalidJson)) + .to.throw('JSON string must be a valid non-empty string'); + }); + }); + + 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]*)\./); + }); + }); + + let sourceTemplate = deepCopy(REMOTE_CONFIG_RESPONSE); + INVALID_STRINGS.forEach((invalidEtag) => { + sourceTemplate.etag = invalidEtag; + const jsonString = JSON.stringify(sourceTemplate); + it(`should throw if the ETag is ${JSON.stringify(invalidEtag)}`, () => { + expect(() => remoteConfig.createTemplateFromJSON(jsonString)) + .to.throw(`Invalid Remote Config template: ${jsonString}`); + }); + }); + + sourceTemplate = deepCopy(REMOTE_CONFIG_RESPONSE); + INVALID_PARAMETERS.forEach((invalidParameter) => { + sourceTemplate.parameters = invalidParameter; + const jsonString = JSON.stringify(sourceTemplate); + it(`should throw if the parameters is ${JSON.stringify(invalidParameter)}`, () => { + expect(() => remoteConfig.createTemplateFromJSON(jsonString)) + .to.throw('Remote Config parameters must be a non-null object'); + }); + }); + + sourceTemplate = deepCopy(REMOTE_CONFIG_RESPONSE); + INVALID_PARAMETER_GROUPS.forEach((invalidParameterGroup) => { + sourceTemplate.parameterGroups = invalidParameterGroup; + const jsonString = JSON.stringify(sourceTemplate); + it(`should throw if the parameter groups are ${JSON.stringify(invalidParameterGroup)}`, + () => { + expect(() => remoteConfig.createTemplateFromJSON(jsonString)) + .to.throw('Remote Config parameter groups must be a non-null object'); + }); + }); + + sourceTemplate = deepCopy(REMOTE_CONFIG_RESPONSE); + INVALID_CONDITIONS.forEach((invalidConditions) => { + sourceTemplate.conditions = invalidConditions; + const jsonString = JSON.stringify(sourceTemplate); + it(`should throw if the conditions is ${JSON.stringify(invalidConditions)}`, () => { + expect(() => remoteConfig.createTemplateFromJSON(jsonString)) + .to.throw('Remote Config conditions must be an array'); + }); + }); + + it('should succeed when a valid json string is provided', () => { + const jsonString = JSON.stringify(REMOTE_CONFIG_RESPONSE); + const newTemplate = remoteConfig.createTemplateFromJSON(jsonString); + 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('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'; + }).to.throw( + 'Cannot set property etag of # which has only a getter'); + + const key = 'holiday_promo_enabled'; + const p1 = newTemplate.parameters[key]; + expect(p1.defaultValue).deep.equals({ value: 'true' }); + expect(p1.conditionalValues).deep.equals({ ios: { useInAppDefault: true } }); + expect(p1.description).equals('this is a promo'); + expect(p1.valueType).equals('BOOLEAN'); + + expect(newTemplate.parameterGroups).deep.equals(PARAMETER_GROUPS); + + const c = newTemplate.conditions.find((c) => c.name === 'ios'); + expect(c).to.be.not.undefined; + const cond = c as RemoteConfigCondition; + expect(cond.name).to.equal('ios'); + expect(cond.expression).to.equal('device.os == \'ios\''); + expect(cond.tagColor).to.equal('BLUE'); + }); + }); + + describe('getServerTemplate', () => { + const operationName = 'getServerTemplate'; + + it('should propagate API errors', () => { + const stub = sinon + .stub(RemoteConfigApiClient.prototype, operationName) + .rejects(INTERNAL_ERROR); + stubs.push(stub); + + return remoteConfig.getServerTemplate().should.eventually.be.rejected.and.deep.equal(INTERNAL_ERROR); + }); + + it('should resolve a server template on success', () => { + const stub = sinon + .stub(RemoteConfigApiClient.prototype, operationName) + .resolves(SERVER_REMOTE_CONFIG_RESPONSE as ServerTemplateData); + stubs.push(stub); + + return remoteConfig.getServerTemplate() + .then((template) => { + expect(template.toJSON().conditions.length).to.equal(1); + expect(template.toJSON().conditions[0].name).to.equal('ios'); + expect(template.toJSON().etag).to.equal('etag-123456789012-5'); + + const version = template.toJSON().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('Mon, 15 Jun 2020 16:45:03 GMT'); + + const key = 'holiday_promo_enabled'; + const p1 = template.toJSON().parameters[key]; + expect(p1.defaultValue).deep.equals({ value: 'true' }); + expect(p1.conditionalValues).deep.equals({ ios: { useInAppDefault: true } }); + + const c = template.toJSON().conditions.find((c) => c.name === 'ios'); + expect(c).to.be.not.undefined; + const cond = c as NamedCondition; + expect(cond.name).to.equal('ios'); + + const parsed = template.toJSON(); + const expectedTemplate = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE); + const expectedVersion = deepCopy(VERSION_INFO); + expectedVersion.updateTime = new Date(expectedVersion.updateTime).toUTCString(); + expectedTemplate.version = expectedVersion; + expect(parsed).deep.equals(expectedTemplate); + }); + }); + + it('should set defaultConfig when passed', () => { + // Defines template with no parameters to demonstrate + // default config will be used instead, + const template = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE) as ServerTemplateData; + template.parameters = {}; + + const stub = sinon + .stub(RemoteConfigApiClient.prototype, operationName) + .resolves(template); + stubs.push(stub); + + const defaultConfig = { + holiday_promo_enabled: false, + holiday_promo_discount: 20, + }; + + return remoteConfig.getServerTemplate({ defaultConfig }) + .then((template) => { + const config = template.evaluate(); + expect(config.getBoolean('holiday_promo_enabled')).to.equal( + defaultConfig.holiday_promo_enabled); + expect(config.getNumber('holiday_promo_discount')).to.equal( + defaultConfig.holiday_promo_discount); + }); + }); + }); + + describe('initServerTemplate', () => { + it('should set and instantiates template when passed', () => { + const template = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE) as ServerTemplateData; + template.parameters = { + dog_type: { + defaultValue: { + value: 'shiba' + } + } + }; + const initializedTemplate = remoteConfig.initServerTemplate({ template }); + const parsed = initializedTemplate.toJSON(); + const expectedVersion = deepCopy(VERSION_INFO); + expectedVersion.updateTime = new Date(expectedVersion.updateTime).toUTCString(); + template.version = expectedVersion as Version; + expect(parsed).deep.equals(deepCopy(template)); + }); + + it('should set and instantiates template when json string is passed', () => { + const template = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE) as ServerTemplateData; + template.parameters = { + dog_type: { + defaultValue: { + value: 'shiba' + }, + description: 'Type of dog breed', + valueType: 'STRING' + } + }; + const templateJson = JSON.stringify(template); + const initializedTemplate = remoteConfig.initServerTemplate({ template: templateJson }); + const parsed = initializedTemplate.toJSON(); + const expectedVersion = deepCopy(VERSION_INFO); + expectedVersion.updateTime = new Date(expectedVersion.updateTime).toUTCString(); + template.version = expectedVersion as Version; + expect(parsed).deep.equals(deepCopy(template)); + }); + + describe('should throw error if invalid template JSON is passed', () => { + const INVALID_PARAMETERS: any[] = [null, '', 'abc', 1, true, []]; + const INVALID_CONDITIONS: any[] = [null, '', 'abc', 1, true, {}]; + + let sourceTemplate = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE); + const jsonString = '{invalidJson: null}'; + it('should throw if template is an invalid JSON', () => { + expect(() => remoteConfig.initServerTemplate({ template: jsonString })) + .to.throw(/Failed to parse the JSON string: ([\D\w]*)\./); + }); + + INVALID_PARAMETERS.forEach((invalidParameter) => { + sourceTemplate.parameters = invalidParameter; + const jsonString = JSON.stringify(sourceTemplate); + it(`should throw if the parameters is ${JSON.stringify(invalidParameter)}`, () => { + expect(() => remoteConfig.initServerTemplate({ template: jsonString })) + .to.throw('Remote Config parameters must be a non-null object'); + }); + }); + + sourceTemplate = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE); + INVALID_CONDITIONS.forEach((invalidConditions) => { + sourceTemplate.conditions = invalidConditions; + const jsonString = JSON.stringify(sourceTemplate); + it(`should throw if the conditions is ${JSON.stringify(invalidConditions)}`, () => { + expect(() => remoteConfig.initServerTemplate({ template: jsonString })) + .to.throw('Remote Config conditions must be an array'); + }); + }); + }); + }); + + describe('RemoteConfigServerTemplate', () => { + const SERVER_REMOTE_CONFIG_RESPONSE_2 = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE); + SERVER_REMOTE_CONFIG_RESPONSE_2.parameters = { + dog_type: { + defaultValue: { + value: 'corgi' + } + }, + dog_type_enabled: { + defaultValue: { + value: 'true' + } + }, + dog_age: { + defaultValue: { + value: '22' + } + }, + dog_jsonified: { + defaultValue: { + value: '{"name":"Taro","breed":"Corgi","age":1,"fluffiness":100}' + } + }, + dog_use_inapp_default: { + defaultValue: { + useInAppDefault: true + } + }, + dog_no_remote_default_value: { + } + }; + + describe('load', () => { + const operationName = 'getServerTemplate'; + + it('should propagate API errors', () => { + const stub = sinon + .stub(RemoteConfigApiClient.prototype, operationName) + .rejects(INTERNAL_ERROR); + stubs.push(stub); + + return remoteConfig.getServerTemplate().should.eventually.be.rejected.and.deep.equal(INTERNAL_ERROR); + }); + + it('should reject when API response is invalid', () => { + const stub = sinon + .stub(RemoteConfigApiClient.prototype, operationName) + .resolves(undefined); + stubs.push(stub); + return remoteConfig.getServerTemplate().should.eventually.be.rejected.and.have.property( + 'message', 'Invalid Remote Config template: undefined'); + }); + + it('should reject when API response does not contain an ETag', () => { + const response = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE); + response.etag = ''; + const stub = sinon + .stub(RemoteConfigApiClient.prototype, operationName) + .resolves(response as ServerTemplateData); + stubs.push(stub); + return remoteConfig.getServerTemplate() + .should.eventually.be.rejected.and.have.property( + 'message', `Invalid Remote Config template: ${JSON.stringify(response)}`); + }); + + it('should reject when API response does not contain valid parameters', () => { + const response = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE); + response.parameters = null; + const stub = sinon + .stub(RemoteConfigApiClient.prototype, operationName) + .resolves(response as ServerTemplateData); + stubs.push(stub); + return remoteConfig.getServerTemplate() + .should.eventually.be.rejected.and.have.property( + 'message', 'Remote Config parameters must be a non-null object'); + }); + + it('should reject when API response does not contain valid conditions', () => { + const response = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE); + response.conditions = Object(); + const stub = sinon + .stub(RemoteConfigApiClient.prototype, operationName) + .resolves(response as ServerTemplateData); + stubs.push(stub); + return remoteConfig.getServerTemplate() + .should.eventually.be.rejected.and.have.property( + 'message', 'Remote Config conditions must be an array'); + }); + + it('should resolve with parameters:{} when no parameters present in the response', () => { + const response = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE); + response.parameters = undefined; + const stub = sinon + .stub(RemoteConfigApiClient.prototype, operationName) + .resolves(response as ServerTemplateData); + stubs.push(stub); + return remoteConfig.getServerTemplate() + .then((template) => { + // If parameters are not present in the response, we set it to an empty object. + expect(template.toJSON().parameters).deep.equals({}); + }); + }); + + it('should resolve with conditions:[] when no conditions present in the response', () => { + const response = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE); + response.conditions = undefined; + const stub = sinon + .stub(RemoteConfigApiClient.prototype, operationName) + .resolves(response as ServerTemplateData); + stubs.push(stub); + return remoteConfig.getServerTemplate() + .then((template) => { + // If conditions are not present in the response, we set it to an empty array. + expect(template.toJSON().conditions).deep.equals([]); + }); + }); + + it('should resolve a server template on success', () => { + const stub = sinon + .stub(RemoteConfigApiClient.prototype, operationName) + .resolves(SERVER_REMOTE_CONFIG_RESPONSE as ServerTemplateData); + stubs.push(stub); + + return remoteConfig.getServerTemplate() + .then((template) => { + expect(template.toJSON().conditions.length).to.equal(1); + expect(template.toJSON().conditions[0].name).to.equal('ios'); + expect(template.toJSON().etag).to.equal('etag-123456789012-5'); + + const version = template.toJSON().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('Mon, 15 Jun 2020 16:45:03 GMT'); + + const key = 'holiday_promo_enabled'; + const p1 = template.toJSON().parameters[key]; + expect(p1.defaultValue).deep.equals({ value: 'true' }); + expect(p1.conditionalValues).deep.equals({ ios: { useInAppDefault: true } }); + + const c = template.toJSON().conditions.find((c) => c.name === 'ios'); + expect(c).to.be.not.undefined; + const cond = c as NamedCondition; + expect(cond.name).to.equal('ios'); + expect(cond.condition).deep.equals({ + 'orCondition': { + 'conditions': [ + { + 'andCondition': { + 'conditions': [ + { + 'true': {} + } + ] + } + } + ] + } + }); + + const parsed = template.toJSON(); + const expectedTemplate = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE); + const expectedVersion = deepCopy(VERSION_INFO); + expectedVersion.updateTime = new Date(expectedVersion.updateTime).toUTCString(); + expectedTemplate.version = expectedVersion; + expect(parsed).deep.equals(expectedTemplate); + }); + }); + + it('should resolve with template when Version updateTime contains 3 digits in fractional seconds', () => { + const response = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE); + const versionInfo = deepCopy(VERSION_INFO); + versionInfo.updateTime = '2020-10-03T17:14:10.203Z'; + response.version = versionInfo; + const stub = sinon + .stub(RemoteConfigApiClient.prototype, operationName) + .resolves(response as ServerTemplateData); + stubs.push(stub); + + return remoteConfig.getServerTemplate() + .then((template) => { + expect(template.toJSON().etag).to.equal('etag-123456789012-5'); + + const version = template.toJSON().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('Sat, 03 Oct 2020 17:14:10 GMT'); + }); + }); + + it('should resolve with template when Version updateTime contains 6 digits in fractional seconds', () => { + const response = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE); + const versionInfo = deepCopy(VERSION_INFO); + versionInfo.updateTime = '2020-08-14T17:01:36.541527Z'; + response.version = versionInfo; + const stub = sinon + .stub(RemoteConfigApiClient.prototype, operationName) + .resolves(response as ServerTemplateData); + stubs.push(stub); + + return remoteConfig.getServerTemplate() + .then((template) => { + expect(template.toJSON().etag).to.equal('etag-123456789012-5'); + + const version = template.toJSON().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, 14 Aug 2020 17:01:36 GMT'); + }); + }); + + it('should resolve with template when Version updateTime contains 9 digits in fractional seconds', () => { + const response = deepCopy(SERVER_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 as ServerTemplateData); + stubs.push(stub); + + return remoteConfig.getServerTemplate() + .then((template) => { + expect(template.toJSON().etag).to.equal('etag-123456789012-5'); + + const version = template.toJSON().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'); + }); + }); + }); + + describe('set', () => { + it('should set template when passed', () => { + const template = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE) as ServerTemplateData; + template.parameters = { + dog_type: { + defaultValue: { + value: 'shiba' + }, + description: 'Type of dog breed', + valueType: 'STRING' + } + }; + template.version = { + ...deepCopy(VERSION_INFO), + updateTime: new Date(VERSION_INFO.updateTime).toUTCString() + } as Version; + const initializedTemplate = remoteConfig.initServerTemplate(); + initializedTemplate.set(template); + const parsed = initializedTemplate.toJSON(); + expect(parsed).deep.equals(template); + }); + + it('should set and instantiates template when json string is passed', () => { + const template = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE) as ServerTemplateData; + template.parameters = { + dog_type: { + defaultValue: { + value: 'shiba' + }, + description: 'Type of dog breed', + valueType: 'STRING' + } + }; + template.version = { + ...deepCopy(VERSION_INFO), + updateTime: new Date(VERSION_INFO.updateTime).toUTCString() + } as Version; + const templateJson = JSON.stringify(template); + const initializedTemplate = remoteConfig.initServerTemplate(); + initializedTemplate.set(templateJson); + const parsed = initializedTemplate.toJSON(); + expect(parsed).deep.equals(template); + }); + + describe('should throw error if there are any JSON or tempalte parsing errors', () => { + const INVALID_PARAMETERS: any[] = [null, '', 'abc', 1, true, []]; + const INVALID_CONDITIONS: any[] = [null, '', 'abc', 1, true, {}]; + + let sourceTemplate = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE); + const jsonString = '{invalidJson: null}'; + it('should throw if template is an invalid JSON', () => { + expect(() => remoteConfig.initServerTemplate({ template: jsonString })) + .to.throw(/Failed to parse the JSON string: ([\D\w]*)\./); + }); + + INVALID_PARAMETERS.forEach((invalidParameter) => { + sourceTemplate.parameters = invalidParameter; + const jsonString = JSON.stringify(sourceTemplate); + it(`should throw if the template is invalid - parameters is ${JSON.stringify(invalidParameter)}`, () => { + expect(() => remoteConfig.initServerTemplate({ template: jsonString })) + .to.throw('Remote Config parameters must be a non-null object'); + }); + }); + + sourceTemplate = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE); + INVALID_CONDITIONS.forEach((invalidConditions) => { + sourceTemplate.conditions = invalidConditions; + const jsonString = JSON.stringify(sourceTemplate); + it(`should throw if the template is invalid - conditions is ${JSON.stringify(invalidConditions)}`, () => { + expect(() => remoteConfig.initServerTemplate({ template: jsonString })) + .to.throw('Remote Config conditions must be an array'); + }); + }); + }); + + it('should throw if template is an invalid JSON', () => { + const jsonString = '{invalidJson: null}'; + const initializedTemplate = remoteConfig.initServerTemplate(); + expect(() => initializedTemplate.set(jsonString)) + .to.throw(/Failed to parse the JSON string: ([\D\w]*)\./); + }); + }); + + describe('evaluate', () => { + it('returns a config when template is present in cache', () => { + const stub = sinon + .stub(RemoteConfigApiClient.prototype, 'getServerTemplate') + .resolves(SERVER_REMOTE_CONFIG_RESPONSE_2 as ServerTemplateData); + stubs.push(stub); + return remoteConfig.getServerTemplate() + .then((template: ServerTemplate) => { + const config = template.evaluate!(); + expect(config.getString('dog_type')).to.equal('corgi'); + expect(config.getBoolean('dog_type_enabled')).to.equal(true); + expect(config.getNumber('dog_age')).to.equal(22); + }); + }); + + it('returns conditional value', () => { + const condition = { + name: 'is_true', + condition: { + orCondition: { + conditions: [ + { + andCondition: { + conditions: [ + { + name: '', + true: { + } + } + ] + } + } + ] + } + } + }; + const template = remoteConfig.initServerTemplate({ + template: { + conditions: [condition], + parameters: { + is_enabled: { + defaultValue: { value: 'false' }, + conditionalValues: { is_true: { value: 'true' } } + }, + }, + etag: '123' + } + }); + const config = template.evaluate(); + expect(config.getBoolean('is_enabled')).to.be.true; + }); + + it('honors condition order', () => { + const template = remoteConfig.initServerTemplate({ + template: { + conditions: [ + { + name: 'is_true', + condition: { + orCondition: { + conditions: [ + { + andCondition: { + conditions: [ + { + true: { + } + } + ] + } + } + ] + } + } + }, + { + name: 'is_true_too', + condition: { + orCondition: { + conditions: [ + { + andCondition: { + conditions: [ + { + true: { + } + } + ] + } + } + ] + } + } + }], + parameters: { + dog_type: { + defaultValue: { value: 'chihuahua' }, + conditionalValues: { + // The is_true and is_true_too conditions both return true, + // but is_true is first in the list, so the corresponding + // value is selected. + is_true_too: { value: 'dachshund' }, + is_true: { value: 'corgi' } + } + }, + }, + etag: '123' + } + }); + const config = template.evaluate(); + expect(config.getString('dog_type')).to.eq('corgi'); + }); + + it('uses local default if parameter not in template', () => { + const template = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE) as ServerTemplateData; + template.parameters = {}; + + const stub = sinon + .stub(RemoteConfigApiClient.prototype, 'getServerTemplate') + .resolves(template); + stubs.push(stub); + + const defaultConfig = { + dog_coat: 'blue merle', + }; + + return remoteConfig.getServerTemplate({ defaultConfig }) + .then((template: ServerTemplate) => { + const config = template.evaluate(); + expect(config.getString('dog_coat')).to.equal(defaultConfig.dog_coat); + }); + }); + + it('uses local default when parameter is in template but default value is undefined', () => { + const template = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE) as ServerTemplateData; + template.parameters = { + dog_no_remote_default_value: {} + }; + + const stub = sinon + .stub(RemoteConfigApiClient.prototype, 'getServerTemplate') + .resolves(template); + stubs.push(stub); + + const defaultConfig = { + dog_no_remote_default_value: 'local default' + }; + + return remoteConfig.getServerTemplate({ defaultConfig }) + .then((template: ServerTemplate) => { + const config = template.evaluate!(); + expect(config.getString('dog_no_remote_default_value')).to.equal( + defaultConfig.dog_no_remote_default_value); + }); + }); + + it('uses local default when in-app default value specified', () => { + const template = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE) as ServerTemplateData; + template.parameters = { + dog_no_remote_default_value: {} + }; + + const stub = sinon + .stub(RemoteConfigApiClient.prototype, 'getServerTemplate') + .resolves(template); + stubs.push(stub); + + const defaultConfig = { + dog_use_inapp_default: '🐕' + }; + + return remoteConfig.getServerTemplate({ defaultConfig }) + .then((template: ServerTemplate) => { + const config = template.evaluate!(); + expect(config.getString('dog_use_inapp_default')).to.equal( + defaultConfig.dog_use_inapp_default); + }); + }); + + it('uses local default when in-app default value specified after loading remote values', async () => { + // We had a bug caused by forgetting the first argument to + // Object.assign. This resulted in defaultConfig being overwritten + // by the remote values. So this test asserts we can use in-app + // default after loading remote values. + const template = remoteConfig.initServerTemplate({ + defaultConfig: { + dog_type: 'corgi' + } + }); + + const response = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE); + + response.parameters = { + dog_type: { + defaultValue: { + value: 'pug' + } + }, + } + + template.set(response as ServerTemplateData); + + let config = template.evaluate(); + + expect(config.getString('dog_type')).to.equal('pug'); + + response.parameters = { + dog_type: { + defaultValue: { + useInAppDefault: true + } + }, + } + + template.set(response as ServerTemplateData); + + config = template.evaluate(); + + expect(config.getString('dog_type')).to.equal('corgi'); + }); + + it('overrides local default when remote value exists', () => { + const response = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE); + response.parameters = { + dog_type_enabled: { + defaultValue: { + // Defines remote value + value: 'true' + } + }, + } + + const stub = sinon + .stub(RemoteConfigApiClient.prototype, 'getServerTemplate') + .resolves(response as ServerTemplateData); + stubs.push(stub); + + return remoteConfig.getServerTemplate({ + defaultConfig: { + // Defines local default + dog_type_enabled: false + } + }) + .then((template: ServerTemplate) => { + const config = template.evaluate(); + // Asserts remote value overrides local default. + expect(config.getBoolean('dog_type_enabled')).to.be.true; + }); + }); + }); + }); + + // Note the static source is set in the getValue() method, but the other sources + // are set in the evaluate() method, so these tests span a couple layers. + describe('ServerConfig', () => { + describe('getValue', () => { + it('should return static when default and remote are not defined', () => { + const templateData = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE) as ServerTemplateData; + // Omits remote parameter values. + templateData.parameters = { + }; + // Omits in-app default values. + const template = remoteConfig.initServerTemplate({ template: templateData }); + const config = template.evaluate(); + const value = config.getValue('dog_type'); + expect(value.asString()).to.equal(''); + expect(value.getSource()).to.equal('static'); + }); + + it('should return default value when it is defined', () => { + const templateData = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE) as ServerTemplateData; + // Omits remote parameter values. + templateData.parameters = { + }; + const template = remoteConfig.initServerTemplate({ + template: templateData, + // Defines in-app default values. + defaultConfig: { + dog_type: 'shiba' + } + }); + const config = template.evaluate(); + const value = config.getValue('dog_type'); + expect(value.asString()).to.equal('shiba'); + expect(value.getSource()).to.equal('default'); + }); + + it('should return remote value when it is defined', () => { + const templateData = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE) as ServerTemplateData; + // Defines remote parameter values. + templateData.parameters = { + dog_type: { + defaultValue: { + value: 'pug' + } + } + }; + const template = remoteConfig.initServerTemplate({ + template: templateData, + // Defines in-app default values. + defaultConfig: { + dog_type: 'shiba' + } + }); + const config = template.evaluate(); + const value = config.getValue('dog_type'); + expect(value.asString()).to.equal('pug'); + expect(value.getSource()).to.equal('remote'); + }); + }); + + describe('getString', () => { + it('returns a string value', () => { + const templateData = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE) as ServerTemplateData; + const template = remoteConfig.initServerTemplate({ + template: templateData, + defaultConfig: { + dog_type: 'shiba' + } + }); + const config = template.evaluate(); + expect(config.getString('dog_type')).to.equal('shiba'); + }); + }); + + describe('getNumber', () => { + it('returns a numeric value', () => { + const templateData = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE) as ServerTemplateData; + const template = remoteConfig.initServerTemplate({ + template: templateData, + defaultConfig: { + dog_age: 12 + } + }); + const config = template.evaluate(); + expect(config.getNumber('dog_age')).to.equal(12); + }); + }); + + describe('getBoolean', () => { + it('returns a boolean value', () => { + const templateData = deepCopy(SERVER_REMOTE_CONFIG_RESPONSE) as ServerTemplateData; + const template = remoteConfig.initServerTemplate({ + template: templateData, + defaultConfig: { + dog_is_cute: true + } + }); + const config = template.evaluate(); + expect(config.getBoolean('dog_is_cute')).to.be.true; + }); + }); + }); + + function runInvalidResponseTests(rcOperation: () => Promise, + operationName: any): void { + it('should propagate API errors', () => { + const stub = sinon + .stub(RemoteConfigApiClient.prototype, operationName) + .rejects(INTERNAL_ERROR); + stubs.push(stub); + return rcOperation() + .should.eventually.be.rejected.and.deep.equal(INTERNAL_ERROR); + }); + + it('should reject when API response is invalid', () => { + const stub = sinon + .stub(RemoteConfigApiClient.prototype, operationName) + .resolves(null); + stubs.push(stub); + return rcOperation() + .should.eventually.be.rejected.and.have.property( + 'message', 'Invalid Remote Config template: null'); + }); + + it('should reject when API response does not contain an ETag', () => { + const response = deepCopy(REMOTE_CONFIG_RESPONSE); + response.etag = ''; + const stub = sinon + .stub(RemoteConfigApiClient.prototype, operationName) + .resolves(response); + stubs.push(stub); + return rcOperation() + .should.eventually.be.rejected.and.have.property( + 'message', `Invalid Remote Config template: ${JSON.stringify(response)}`); + }); + + it('should reject when API response does not contain valid parameters', () => { + const response = deepCopy(REMOTE_CONFIG_RESPONSE); + response.parameters = null; + const stub = sinon + .stub(RemoteConfigApiClient.prototype, operationName) + .resolves(response); + stubs.push(stub); + return rcOperation() + .should.eventually.be.rejected.and.have.property( + 'message', 'Remote Config parameters must be a non-null object'); + }); + + it('should reject when API response does not contain valid parameter groups', () => { + const response = deepCopy(REMOTE_CONFIG_RESPONSE); + response.parameterGroups = null; + const stub = sinon + .stub(RemoteConfigApiClient.prototype, operationName) + .resolves(response); + stubs.push(stub); + return rcOperation() + .should.eventually.be.rejected.and.have.property( + 'message', 'Remote Config parameter groups must be a non-null object'); + }); + + it('should reject when API response does not contain valid conditions', () => { + const response = deepCopy(REMOTE_CONFIG_RESPONSE); + response.conditions = Object(); + const stub = sinon + .stub(RemoteConfigApiClient.prototype, operationName) + .resolves(response); + stubs.push(stub); + return rcOperation() + .should.eventually.be.rejected.and.have.property( + 'message', 'Remote Config conditions must be an array'); + }); + } + + function runValidResponseTests(rcOperation: () => Promise, + operationName: any): void { + it('should resolve with parameters:{} when no parameters present in the response', () => { + const response = deepCopy({ conditions: [], parameterGroups: {}, etag: '0-1010-2' }); + const stub = sinon + .stub(RemoteConfigApiClient.prototype, operationName) + .resolves(response); + stubs.push(stub); + return rcOperation() + .then((template) => { + expect(template.conditions).deep.equals([]); + // if parameters are not present in the response, we set it to an empty object. + expect(template.parameters).deep.equals({}); + expect(template.parameterGroups).deep.equals({}); + }); + }); + + it('should resolve with parameterGroups:{} when no parameter groups present in the response', + () => { + const response = deepCopy({ conditions: [], parameters: {}, etag: '0-1010-2' }); + const stub = sinon + .stub(RemoteConfigApiClient.prototype, operationName) + .resolves(response); + stubs.push(stub); + return rcOperation() + .then((template) => { + expect(template.conditions).deep.equals([]); + expect(template.parameters).deep.equals({}); + // if parameter groups are not present in the response, we set it to an empty object. + expect(template.parameterGroups).deep.equals({}); + }); + }); + + it('should resolve with conditions:[] when no conditions present in the response', () => { + const response = deepCopy({ parameters: {}, parameterGroups: {}, etag: '0-1010-2' }); + const stub = sinon + .stub(RemoteConfigApiClient.prototype, operationName) + .resolves(response); + stubs.push(stub); + return rcOperation() + .then((template) => { + // if conditions are not present in the response, we set it to an empty array. + expect(template.conditions).deep.equals([]); + expect(template.parameters).deep.equals({}); + expect(template.parameterGroups).deep.equals({}); + }); + }); + + it('should resolve with Remote Config template on success', () => { + const stub = sinon + .stub(RemoteConfigApiClient.prototype, operationName) + .resolves(REMOTE_CONFIG_RESPONSE); + stubs.push(stub); + + return rcOperation() + .then((template) => { + 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('BLUE'); + expect(template.etag).to.equal('etag-123456789012-5'); + // verify that etag is read-only + expect(() => { + (template as any).etag = 'new-etag'; + }).to.throw( + 'Cannot set property etag of # which has only a getter'); + + 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('Mon, 15 Jun 2020 16:45:03 GMT'); + + const key = 'holiday_promo_enabled'; + const p1 = template.parameters[key]; + expect(p1.defaultValue).deep.equals({ value: 'true' }); + expect(p1.conditionalValues).deep.equals({ ios: { useInAppDefault: true } }); + expect(p1.description).equals('this is a promo'); + expect(p1.valueType).equals('BOOLEAN'); + + expect(template.parameterGroups).deep.equals(PARAMETER_GROUPS); + + const c = template.conditions.find((c) => c.name === 'ios'); + expect(c).to.be.not.undefined; + const cond = c as RemoteConfigCondition; + expect(cond.name).to.equal('ios'); + expect(cond.expression).to.equal('device.os == \'ios\''); + expect(cond.tagColor).to.equal('BLUE'); + + const parsed = JSON.parse(JSON.stringify(template)); + const expectedTemplate = deepCopy(REMOTE_CONFIG_RESPONSE); + const expectedVersion = deepCopy(VERSION_INFO); + expectedVersion.updateTime = new Date(expectedVersion.updateTime).toUTCString(); + expectedTemplate.version = expectedVersion; + expect(parsed).deep.equals(expectedTemplate); + }); + }); + + 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-10-03T17:14:10.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('Sat, 03 Oct 2020 17:14:10 GMT'); + }); + }); + + 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-08-14T17: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, 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'); + }); + }); + } +}); diff --git a/test/unit/security-rules/index.spec.ts b/test/unit/security-rules/index.spec.ts new file mode 100644 index 0000000000..ad6a0b05de --- /dev/null +++ b/test/unit/security-rules/index.spec.ts @@ -0,0 +1,75 @@ +/*! + * @license + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +import * as chai from 'chai'; +import * as sinonChai from 'sinon-chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +import * as mocks from '../../resources/mocks'; +import { App } from '../../../src/app/index'; +import { getSecurityRules, SecurityRules } from '../../../src/security-rules/index'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +describe('SecurityRules', () => { + let mockApp: App; + let mockCredentialApp: App; + + const noProjectIdError = 'Failed to determine project ID. Initialize the SDK ' + + 'with service account credentials, or set project ID as an app option. Alternatively, set the ' + + 'GOOGLE_CLOUD_PROJECT environment variable.'; + + beforeEach(() => { + mockApp = mocks.app(); + mockCredentialApp = mocks.mockCredentialApp(); + }); + + describe('getSecurityRules()', () => { + it('should throw when default app is not available', () => { + expect(() => { + return getSecurityRules(); + }).to.throw('The default Firebase app does not exist.'); + }); + + it('should reject given an invalid credential without project ID', () => { + // Project ID not set in the environment. + delete process.env.GOOGLE_CLOUD_PROJECT; + delete process.env.GCLOUD_PROJECT; + const rules = getSecurityRules(mockCredentialApp); + return rules.getFirestoreRuleset() + .should.eventually.rejectedWith(noProjectIdError); + }); + + it('should not throw given a valid app', () => { + expect(() => { + return getSecurityRules(mockApp); + }).not.to.throw(); + }); + + it('should return the same instance for a given app instance', () => { + const rules1: SecurityRules = getSecurityRules(mockApp); + const rules2: SecurityRules = getSecurityRules(mockApp); + expect(rules1).to.equal(rules2); + }); + }); +}); diff --git a/test/unit/security-rules/security-rules-api-client.spec.ts b/test/unit/security-rules/security-rules-api-client.spec.ts new file mode 100644 index 0000000000..512bcc9a20 --- /dev/null +++ b/test/unit/security-rules/security-rules-api-client.spec.ts @@ -0,0 +1,737 @@ +/*! + * 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. + */ + +'use strict'; + +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-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'; +import { FirebaseAppError } from '../../../src/utils/error'; +import { FirebaseApp } from '../../../src/app/firebase-app'; +import { getSdkVersion } from '../../../src/utils/index'; + +const expect = chai.expect; + +describe('SecurityRulesApiClient', () => { + + const RULESET_NAME = 'ruleset-id'; + const RELEASE_NAME = 'test.service'; + 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 mockOptions = { + credential: new mocks.MockCredential(), + projectId: 'test-project', + }; + + const clientWithoutProjectId = new SecurityRulesApiClient( + mocks.mockCredentialApp()); + + // Stubs used to simulate underlying api calls. + let stubs: sinon.SinonStub[] = []; + let app: FirebaseApp; + let apiClient: SecurityRulesApiClient; + + beforeEach(() => { + app = mocks.appWithOptions(mockOptions); + apiClient = new SecurityRulesApiClient(app); + }); + + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + return app.delete(); + }); + + describe('Constructor', () => { + it('should throw when the app is null', () => { + expect(() => new SecurityRulesApiClient(null as unknown as FirebaseApp)) + .to.throw('First argument passed to admin.securityRules() must be a valid Firebase app'); + }); + }); + + describe('getRuleset', () => { + const INVALID_NAMES: any[] = [null, undefined, '', 1, true, {}, []]; + INVALID_NAMES.forEach((invalidName) => { + it(`should reject when called with: ${JSON.stringify(invalidName)}`, () => { + return apiClient.getRuleset(invalidName) + .should.eventually.be.rejected.and.have.property( + 'message', 'Ruleset name must be a non-empty string.'); + }); + }); + + 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', () => { + return clientWithoutProjectId.getRuleset(RULESET_NAME) + .should.eventually.be.rejectedWith(noProjectId); + }); + + it('should resolve with the requested ruleset on success', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({ name: 'bar' })); + stubs.push(stub); + return apiClient.getRuleset(RULESET_NAME) + .then((resp) => { + expect(resp.name).to.equal('bar'); + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'GET', + url: 'https://firebaserules.googleapis.com/v1/projects/test-project/rulesets/ruleset-id', + headers: EXPECTED_HEADERS, + }); + }); + }); + + it('should throw 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 FirebaseSecurityRulesError('not-found', 'Requested entity not found'); + return apiClient.getRuleset(RULESET_NAME) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should throw 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 FirebaseSecurityRulesError('unknown-error', 'Unknown server error: {}'); + return apiClient.getRuleset(RULESET_NAME) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should throw 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 FirebaseSecurityRulesError( + 'unknown-error', 'Unexpected response with status: 404 and body: not json'); + return apiClient.getRuleset(RULESET_NAME) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should throw 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.getRuleset(RULESET_NAME) + .should.eventually.be.rejected.and.deep.include(expected); + }); + }); + + describe('createRuleset', () => { + const RULES_FILE = { + name: 'test.rules', + content: 'test source {}', + }; + + const RULES_CONTENT: RulesetContent = { + source: { + files: [RULES_FILE], + }, + }; + + const invalidContent: any[] = [null, undefined, {}, { source: {} }]; + invalidContent.forEach((content) => { + it(`should reject when called with: ${JSON.stringify(content)}`, () => { + return apiClient.createRuleset(content) + .should.eventually.be.rejected.and.have.property( + 'message', 'Invalid rules content.'); + }); + }); + + it('should reject when project id is not available', () => { + return clientWithoutProjectId.createRuleset({ + source: { + files: [RULES_FILE], + }, + }).should.eventually.be.rejectedWith(noProjectId); + }); + + const invalidFiles: any[] = [null, undefined, 'test', {}, { name: 'test' }, { content: 'test' }]; + invalidFiles.forEach((file) => { + it(`should reject when called with: ${JSON.stringify(file)}`, () => { + const ruleset: RulesetContent = { + source: { + files: [file], + }, + }; + return apiClient.createRuleset(ruleset) + .should.eventually.be.rejected.and.have.property( + 'message', `Invalid rules file argument: ${JSON.stringify(file)}`); + }); + + it(`should reject when called with extra argument: ${JSON.stringify(file)}`, () => { + const ruleset: RulesetContent = { + source: { + files: [RULES_FILE, file], + }, + }; + return apiClient.createRuleset(ruleset) + .should.eventually.be.rejected.and.have.property( + 'message', `Invalid rules file argument: ${JSON.stringify(file)}`); + }); + }); + + it('should resolve with the created resource on success', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({ name: 'some-name', ...RULES_CONTENT })); + stubs.push(stub); + return apiClient.createRuleset(RULES_CONTENT) + .then((resp) => { + expect(resp.name).to.equal('some-name'); + expect(resp.source).to.not.be.undefined; + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://firebaserules.googleapis.com/v1/projects/test-project/rulesets', + data: RULES_CONTENT, + headers: EXPECTED_HEADERS, + }); + }); + }); + + it('should throw 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 FirebaseSecurityRulesError('not-found', 'Requested entity not found'); + return apiClient.createRuleset(RULES_CONTENT) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should throw when the rulesets limit reached', () => { + const resourceExhaustedError = { + error: { + code: 429, + message: 'The maximum number of Rulesets (2500) have already been created for the project.', + status: 'RESOURCE_EXHAUSTED', + }, + }; + const stub = sinon + .stub(HttpClient.prototype, 'send') + .rejects(utils.errorFrom(resourceExhaustedError, 429)); + stubs.push(stub); + const expected = new FirebaseSecurityRulesError('resource-exhausted', resourceExhaustedError.error.message); + return apiClient.createRuleset(RULES_CONTENT) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should throw 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 FirebaseSecurityRulesError('unknown-error', 'Unknown server error: {}'); + return apiClient.createRuleset(RULES_CONTENT) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should throw 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 FirebaseSecurityRulesError( + 'unknown-error', 'Unexpected response with status: 404 and body: not json'); + return apiClient.createRuleset(RULES_CONTENT) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should throw 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.createRuleset(RULES_CONTENT) + .should.eventually.be.rejected.and.deep.include(expected); + }); + }); + + describe('listRulesets', () => { + const LIST_RESPONSE = { + rulesets: [ + { + name: 'rs1', + createTime: 'date1', + }, + ], + nextPageToken: 'next', + }; + + const invalidPageSizes: any[] = [null, '', '10', true, {}, []]; + invalidPageSizes.forEach((invalidPageSize) => { + it(`should reject when called with invalid page size: ${JSON.stringify(invalidPageSize)}`, () => { + return apiClient.listRulesets(invalidPageSize) + .should.eventually.be.rejected.and.have.property( + 'message', 'Invalid page size.'); + }); + }); + + const outOfRangePageSizes: number[] = [-1, 0, 101]; + outOfRangePageSizes.forEach((invalidPageSize) => { + it(`should reject when called with invalid page size: ${invalidPageSize}`, () => { + return apiClient.listRulesets(invalidPageSize) + .should.eventually.be.rejected.and.have.property( + 'message', 'Page size must be between 1 and 100.'); + }); + }); + + const invalidPageTokens: any[] = [null, 0, '', true, {}, []]; + invalidPageTokens.forEach((invalidPageToken) => { + it(`should reject when called with invalid page token: ${JSON.stringify(invalidPageToken)}`, () => { + return apiClient.listRulesets(10, invalidPageToken) + .should.eventually.be.rejected.and.have.property( + 'message', 'Next page token must be a non-empty string.'); + }); + }); + + it('should reject when project id is not available', () => { + return clientWithoutProjectId.listRulesets() + .should.eventually.be.rejectedWith(noProjectId); + }); + + it('should resolve on success when called without any arguments', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(LIST_RESPONSE)); + stubs.push(stub); + return apiClient.listRulesets() + .then((resp) => { + expect(resp).to.deep.equal(LIST_RESPONSE); + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'GET', + url: 'https://firebaserules.googleapis.com/v1/projects/test-project/rulesets', + data: { pageSize: 100 }, + headers: EXPECTED_HEADERS, + }); + }); + }); + + it('should resolve on success when called with a page size', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(LIST_RESPONSE)); + stubs.push(stub); + return apiClient.listRulesets(50) + .then((resp) => { + expect(resp).to.deep.equal(LIST_RESPONSE); + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'GET', + url: 'https://firebaserules.googleapis.com/v1/projects/test-project/rulesets', + data: { pageSize: 50 }, + headers: EXPECTED_HEADERS, + }); + }); + }); + + it('should resolve on success when called with a page token', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(LIST_RESPONSE)); + stubs.push(stub); + return apiClient.listRulesets(50, 'next') + .then((resp) => { + expect(resp).to.deep.equal(LIST_RESPONSE); + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'GET', + url: 'https://firebaserules.googleapis.com/v1/projects/test-project/rulesets', + data: { pageSize: 50, pageToken: 'next' }, + headers: EXPECTED_HEADERS, + }); + }); + }); + + it('should throw 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 FirebaseSecurityRulesError('not-found', 'Requested entity not found'); + return apiClient.listRulesets() + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should throw 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 FirebaseSecurityRulesError('unknown-error', 'Unknown server error: {}'); + return apiClient.listRulesets() + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should throw 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 FirebaseSecurityRulesError( + 'unknown-error', 'Unexpected response with status: 404 and body: not json'); + return apiClient.listRulesets() + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should throw 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.listRulesets() + .should.eventually.be.rejected.and.deep.include(expected); + }); + }); + + describe('getRelease', () => { + it('should reject when project id is not available', () => { + return clientWithoutProjectId.getRelease(RELEASE_NAME) + .should.eventually.be.rejectedWith(noProjectId); + }); + + it('should resolve with the requested release on success', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({ name: 'bar' })); + stubs.push(stub); + return apiClient.getRelease(RELEASE_NAME) + .then((resp) => { + expect(resp.name).to.equal('bar'); + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'GET', + url: 'https://firebaserules.googleapis.com/v1/projects/test-project/releases/test.service', + headers: EXPECTED_HEADERS, + }); + }); + }); + + it('should throw 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 FirebaseSecurityRulesError('not-found', 'Requested entity not found'); + return apiClient.getRelease(RELEASE_NAME) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should throw 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 FirebaseSecurityRulesError('unknown-error', 'Unknown server error: {}'); + return apiClient.getRelease(RELEASE_NAME) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should throw 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 FirebaseSecurityRulesError( + 'unknown-error', 'Unexpected response with status: 404 and body: not json'); + return apiClient.getRelease(RELEASE_NAME) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should throw 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.getRelease(RELEASE_NAME) + .should.eventually.be.rejected.and.deep.include(expected); + }); + }); + + describe('updateOrCreateRelease', () => { + it('should propagate API errors', () => { + const EXPECTED_ERROR = new FirebaseSecurityRulesError('internal-error', 'message'); + const stub = sinon + .stub(SecurityRulesApiClient.prototype, 'updateRelease') + .rejects(EXPECTED_ERROR); + stubs.push(stub); + return apiClient.updateOrCreateRelease(RELEASE_NAME, RULESET_NAME) + .should.eventually.be.rejected.and.deep.include(EXPECTED_ERROR); + }); + + it('should create a new ruleset when update fails with a not-found error', () => { + const NOT_FOUND_ERROR = new FirebaseSecurityRulesError('not-found', 'message'); + const updateRelease = sinon + .stub(SecurityRulesApiClient.prototype, 'updateRelease') + .rejects(NOT_FOUND_ERROR); + const createRelease = sinon + .stub(SecurityRulesApiClient.prototype, 'createRelease') + .resolves(); + stubs.push(updateRelease, createRelease); + + return apiClient.updateOrCreateRelease(RELEASE_NAME, RULESET_NAME) + .then(() => { + expect(updateRelease).to.have.been.calledOnce.and.calledWith(RELEASE_NAME, RULESET_NAME); + expect(createRelease).to.have.been.called.calledOnce.and.calledWith(RELEASE_NAME, RULESET_NAME); + }); + }); + }); + + describe('updateRelease', () => { + it('should reject when project id is not available', () => { + return clientWithoutProjectId.updateRelease(RELEASE_NAME, RULESET_NAME) + .should.eventually.be.rejectedWith(noProjectId); + }); + + it('should resolve with the updated release on success', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({ name: 'bar' })); + stubs.push(stub); + return apiClient.updateRelease(RELEASE_NAME, RULESET_NAME) + .then((resp) => { + expect(resp.name).to.equal('bar'); + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'PATCH', + url: 'https://firebaserules.googleapis.com/v1/projects/test-project/releases/test.service', + data: { + release: { + name: 'projects/test-project/releases/test.service', + rulesetName: 'projects/test-project/rulesets/ruleset-id', + }, + }, + headers: EXPECTED_HEADERS, + }); + }); + }); + + it('should throw 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 FirebaseSecurityRulesError('not-found', 'Requested entity not found'); + return apiClient.updateRelease(RELEASE_NAME, RULESET_NAME) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should throw 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 FirebaseSecurityRulesError('unknown-error', 'Unknown server error: {}'); + return apiClient.updateRelease(RELEASE_NAME, RULESET_NAME) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should throw 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 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.include(expected); + }); + + it('should throw 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.updateRelease(RELEASE_NAME, RULESET_NAME) + .should.eventually.be.rejected.and.deep.include(expected); + }); + }); + + describe('createRelease', () => { + it('should reject when project id is not available', () => { + return clientWithoutProjectId.createRelease(RELEASE_NAME, RULESET_NAME) + .should.eventually.be.rejectedWith(noProjectId); + }); + + it('should resolve with the created release on success', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({ name: 'bar' })); + stubs.push(stub); + return apiClient.createRelease(RELEASE_NAME, RULESET_NAME) + .then((resp) => { + expect(resp.name).to.equal('bar'); + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://firebaserules.googleapis.com/v1/projects/test-project/releases', + data: { + name: 'projects/test-project/releases/test.service', + rulesetName: 'projects/test-project/rulesets/ruleset-id', + }, + headers: EXPECTED_HEADERS, + }); + }); + }); + + it('should throw 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 FirebaseSecurityRulesError('not-found', 'Requested entity not found'); + return apiClient.createRelease(RELEASE_NAME, RULESET_NAME) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should throw 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 FirebaseSecurityRulesError('unknown-error', 'Unknown server error: {}'); + return apiClient.createRelease(RELEASE_NAME, RULESET_NAME) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should throw 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 FirebaseSecurityRulesError( + 'unknown-error', 'Unexpected response with status: 404 and body: not json'); + return apiClient.createRelease(RELEASE_NAME, RULESET_NAME) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should throw 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.createRelease(RELEASE_NAME, RULESET_NAME) + .should.eventually.be.rejected.and.deep.include(expected); + }); + }); + + describe('deleteRuleset', () => { + const INVALID_NAMES: any[] = [null, undefined, '', 1, true, {}, []]; + INVALID_NAMES.forEach((invalidName) => { + it(`should reject when called with: ${JSON.stringify(invalidName)}`, () => { + return apiClient.deleteRuleset(invalidName) + .should.eventually.be.rejected.and.have.property( + 'message', 'Ruleset name must be a non-empty string.'); + }); + }); + + 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', () => { + return clientWithoutProjectId.deleteRuleset(RULESET_NAME) + .should.eventually.be.rejectedWith(noProjectId); + }); + + it('should resolve on success', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + stubs.push(stub); + return apiClient.deleteRuleset(RULESET_NAME) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'DELETE', + url: 'https://firebaserules.googleapis.com/v1/projects/test-project/rulesets/ruleset-id', + headers: EXPECTED_HEADERS, + }); + }); + }); + + it('should throw 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 FirebaseSecurityRulesError('not-found', 'Requested entity not found'); + return apiClient.deleteRuleset(RULESET_NAME) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should throw 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 FirebaseSecurityRulesError('unknown-error', 'Unknown server error: {}'); + return apiClient.deleteRuleset(RULESET_NAME) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should throw 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 FirebaseSecurityRulesError( + 'unknown-error', 'Unexpected response with status: 404 and body: not json'); + return apiClient.deleteRuleset(RULESET_NAME) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should throw 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.deleteRuleset(RULESET_NAME) + .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 new file mode 100644 index 0000000000..380583d4cd --- /dev/null +++ b/test/unit/security-rules/security-rules.spec.ts @@ -0,0 +1,848 @@ +/*! + * 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. + */ + +'use strict'; + +import * as _ from 'lodash'; +import * as chai from 'chai'; +import * as sinon from 'sinon'; +import { SecurityRules } from '../../../src/security-rules/index'; +import { FirebaseApp } from '../../../src/app/firebase-app'; +import * as mocks from '../../resources/mocks'; +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; + +describe('SecurityRules', () => { + + const EXPECTED_ERROR = new FirebaseSecurityRulesError('internal-error', 'message'); + const FIRESTORE_RULESET_RESPONSE = { + name: 'projects/test-project/rulesets/foo', + createTime: '2019-03-08T23:45:23.288047Z', + source: { + files: [ + { + name: 'firestore.rules', + content: 'service cloud.firestore{\n}\n', + }, + ], + }, + }; + 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( + 'invalid-argument', + 'ruleset must be a non-empty name or a RulesetMetadata object.', + ); + const INVALID_RULESETS: any[] = [null, undefined, '', 1, true, {}, [], { name: '' }]; + + const INVALID_BUCKET_ERROR = new FirebaseSecurityRulesError( + 'invalid-argument', + 'Bucket name not specified or invalid. Specify a default bucket name via the ' + + 'storageBucket option when initializing the app, or specify the bucket name ' + + 'explicitly when calling the rules API.', + ); + const INVALID_BUCKET_NAMES: any[] = [null, '', true, false, 1, 0, {}, []]; + + const INVALID_SOURCES: any[] = [null, undefined, '', 1, true, {}, []]; + const INVALID_SOURCE_ERROR = new FirebaseSecurityRulesError( + 'invalid-argument', 'Source must be a non-empty string or a Buffer.'); + + let securityRules: SecurityRules; + let mockApp: FirebaseApp; + let mockCredentialApp: FirebaseApp; + + // Stubs used to simulate underlying api calls. + let stubs: sinon.SinonStub[] = []; + + before(() => { + mockApp = mocks.app(); + mockCredentialApp = mocks.mockCredentialApp(); + securityRules = new SecurityRules(mockApp); + }); + + after(() => { + return mockApp.delete(); + }); + + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + function stubReleaseFromSource(): [sinon.SinonStub, sinon.SinonStub] { + const createRuleset = sinon + .stub(SecurityRulesApiClient.prototype, 'createRuleset') + .resolves(FIRESTORE_RULESET_RESPONSE); + const updateRelease = sinon + .stub(SecurityRulesApiClient.prototype, 'updateRelease') + .resolves(FIRESTORE_RULESET_RELEASE); + stubs.push(createRuleset, updateRelease); + return [createRuleset, updateRelease]; + } + + 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 securityRulesAny: any = SecurityRules; + return new securityRulesAny(invalidApp); + }).to.throw( + 'First argument passed to admin.securityRules() must be a valid Firebase app ' + + 'instance.'); + }); + }); + + it('should throw given no app', () => { + expect(() => { + const securityRulesAny: any = SecurityRules; + return new securityRulesAny(); + }).to.throw( + 'First argument passed to admin.securityRules() 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 rulesWithoutProjectId = new SecurityRules(mockCredentialApp); + return rulesWithoutProjectId.getRuleset('test') + .should.eventually.rejectedWith(noProjectId); + }); + + it('should not throw given a valid app', () => { + expect(() => { + return new SecurityRules(mockApp); + }).not.to.throw(); + }); + }); + + describe('app', () => { + it('returns the app from the constructor', () => { + // We expect referential equality here + expect(securityRules.app).to.equal(mockApp); + }); + }); + + describe('getRuleset', () => { + it('should propagate API errors', () => { + const stub = sinon + .stub(SecurityRulesApiClient.prototype, 'getRuleset') + .rejects(EXPECTED_ERROR); + stubs.push(stub); + return securityRules.getRuleset('foo') + .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 as any); + stubs.push(stub); + return securityRules.getRuleset('foo') + .should.eventually.be.rejected.and.have.property( + 'message', 'Invalid Ruleset response: null'); + }); + + it('should reject when API response does not contain a name', () => { + const response = deepCopy(FIRESTORE_RULESET_RESPONSE); + response.name = ''; + const stub = sinon + .stub(SecurityRulesApiClient.prototype, 'getRuleset') + .resolves(response); + stubs.push(stub); + return securityRules.getRuleset('foo') + .should.eventually.be.rejected.and.have.property( + 'message', `Invalid Ruleset response: ${JSON.stringify(response)}`); + }); + + it('should reject when API response does not contain a createTime', () => { + const response = deepCopy(FIRESTORE_RULESET_RESPONSE); + response.createTime = ''; + const stub = sinon + .stub(SecurityRulesApiClient.prototype, 'getRuleset') + .resolves(response); + stubs.push(stub); + return securityRules.getRuleset('foo') + .should.eventually.be.rejected.and.have.property( + 'message', `Invalid Ruleset response: ${JSON.stringify(response)}`); + }); + + it('should reject when API response does not contain a source', () => { + const response = deepCopy(FIRESTORE_RULESET_RESPONSE); + response.source = null as any; + const stub = sinon + .stub(SecurityRulesApiClient.prototype, 'getRuleset') + .resolves(response); + stubs.push(stub); + return securityRules.getRuleset('foo') + .should.eventually.be.rejected.and.have.property( + 'message', `Invalid Ruleset response: ${JSON.stringify(response)}`); + }); + + it('should resolve with Ruleset on success', () => { + const stub = sinon + .stub(SecurityRulesApiClient.prototype, 'getRuleset') + .resolves(FIRESTORE_RULESET_RESPONSE); + stubs.push(stub); + + return securityRules.getRuleset('foo') + .then((ruleset) => { + expect(ruleset.name).to.equal('foo'); + expect(ruleset.createTime).to.equal(CREATE_TIME_UTC); + expect(ruleset.source.length).to.equal(1); + + const file = ruleset.source[0]; + expect(file.name).equals('firestore.rules'); + expect(file.content).equals('service cloud.firestore{\n}\n'); + }); + }); + }); + + describe('getFirestoreRuleset', () => { + it('should propagate API errors', () => { + const stub = sinon + .stub(SecurityRulesApiClient.prototype, 'getRelease') + .rejects(EXPECTED_ERROR); + stubs.push(stub); + return securityRules.getFirestoreRuleset() + .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({} as any); + stubs.push(stub); + + return securityRules.getFirestoreRuleset() + .should.eventually.be.rejected.and.have.property( + 'message', 'Ruleset name not found for cloud.firestore.'); + }); + + it('should resolve with Ruleset on success', () => { + const getRelease = sinon + .stub(SecurityRulesApiClient.prototype, 'getRelease') + .resolves(FIRESTORE_RULESET_RELEASE); + const getRuleset = sinon + .stub(SecurityRulesApiClient.prototype, 'getRuleset') + .resolves(FIRESTORE_RULESET_RESPONSE); + stubs.push(getRelease, getRuleset); + + return securityRules.getFirestoreRuleset() + .then((ruleset) => { + expect(ruleset.name).to.equal('foo'); + expect(ruleset.createTime).to.equal(CREATE_TIME_UTC); + expect(ruleset.source.length).to.equal(1); + + const file = ruleset.source[0]; + expect(file.name).equals('firestore.rules'); + expect(file.content).equals('service cloud.firestore{\n}\n'); + + expect(getRelease).to.have.been.calledOnce.and.calledWith( + 'cloud.firestore'); + }); + }); + }); + + describe('getStorageRuleset', () => { + INVALID_BUCKET_NAMES.forEach((bucketName) => { + it(`should reject when called with: ${JSON.stringify(bucketName)}`, () => { + return securityRules.getStorageRuleset(bucketName) + .should.eventually.be.rejected.and.deep.include(INVALID_BUCKET_ERROR); + }); + }); + + it('should propagate API errors', () => { + const stub = sinon + .stub(SecurityRulesApiClient.prototype, 'getRelease') + .rejects(EXPECTED_ERROR); + stubs.push(stub); + return securityRules.getStorageRuleset() + .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({} as any); + stubs.push(stub); + + return securityRules.getStorageRuleset() + .should.eventually.be.rejected.and.have.property( + 'message', 'Ruleset name not found for firebase.storage/bucketName.appspot.com.'); + }); + + it('should resolve with Ruleset for the default bucket on success', () => { + const getRelease = sinon + .stub(SecurityRulesApiClient.prototype, 'getRelease') + .resolves(FIRESTORE_RULESET_RELEASE); + const getRuleset = sinon + .stub(SecurityRulesApiClient.prototype, 'getRuleset') + .resolves(FIRESTORE_RULESET_RESPONSE); + stubs.push(getRelease, getRuleset); + + return securityRules.getStorageRuleset() + .then((ruleset) => { + expect(ruleset.name).to.equal('foo'); + expect(ruleset.createTime).to.equal(CREATE_TIME_UTC); + expect(ruleset.source.length).to.equal(1); + + const file = ruleset.source[0]; + expect(file.name).equals('firestore.rules'); + expect(file.content).equals('service cloud.firestore{\n}\n'); + + expect(getRelease).to.have.been.calledOnce.and.calledWith( + 'firebase.storage/bucketName.appspot.com'); + }); + }); + + it('should resolve with Ruleset for the specified bucket on success', () => { + const getRelease = sinon + .stub(SecurityRulesApiClient.prototype, 'getRelease') + .resolves(FIRESTORE_RULESET_RELEASE); + const getRuleset = sinon + .stub(SecurityRulesApiClient.prototype, 'getRuleset') + .resolves(FIRESTORE_RULESET_RESPONSE); + stubs.push(getRelease, getRuleset); + + return securityRules.getStorageRuleset('other.appspot.com') + .then((ruleset) => { + expect(ruleset.name).to.equal('foo'); + expect(ruleset.createTime).to.equal(CREATE_TIME_UTC); + expect(ruleset.source.length).to.equal(1); + + const file = ruleset.source[0]; + expect(file.name).equals('firestore.rules'); + expect(file.content).equals('service cloud.firestore{\n}\n'); + + expect(getRelease).to.have.been.calledOnce.and.calledWith( + 'firebase.storage/other.appspot.com'); + }); + }); + }); + + describe('releaseFirestoreRuleset', () => { + INVALID_RULESETS.forEach((invalidRuleset) => { + it(`should reject when called with: ${JSON.stringify(invalidRuleset)}`, () => { + return securityRules.releaseFirestoreRuleset(invalidRuleset) + .should.eventually.be.rejected.and.deep.include(INVALID_RULESET_ERROR); + }); + }); + + it('should propagate API errors', () => { + const stub = sinon + .stub(SecurityRulesApiClient.prototype, 'updateRelease') + .rejects(EXPECTED_ERROR); + stubs.push(stub); + return securityRules.releaseFirestoreRuleset('foo') + .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(FIRESTORE_RULESET_RELEASE); + stubs.push(stub); + + return securityRules.releaseFirestoreRuleset('foo') + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith('cloud.firestore', 'foo'); + }); + }); + + it('should resolve on success when the ruleset specified as an object', () => { + const stub = sinon + .stub(SecurityRulesApiClient.prototype, 'updateRelease') + .resolves(FIRESTORE_RULESET_RELEASE); + stubs.push(stub); + + return securityRules.releaseFirestoreRuleset({ name: 'foo', createTime: 'time' }) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith('cloud.firestore', 'foo'); + }); + }); + }); + + describe('releaseFirestoreRulesetFromSource', () => { + const RULES_FILE = { + name: 'firestore.rules', + content: 'test source {}', + }; + + INVALID_SOURCES.forEach((invalidSource) => { + it(`should reject when called with: ${JSON.stringify(invalidSource)}`, () => { + return securityRules.releaseFirestoreRulesetFromSource(invalidSource) + .should.eventually.be.rejected.and.deep.include(INVALID_SOURCE_ERROR); + }); + }); + + it('should propagate API errors', () => { + const stub = sinon + .stub(SecurityRulesApiClient.prototype, 'createRuleset') + .rejects(EXPECTED_ERROR); + stubs.push(stub); + return securityRules.releaseFirestoreRulesetFromSource('foo') + .should.eventually.be.rejected.and.deep.include(EXPECTED_ERROR); + }); + + const sources: {[key: string]: string | Buffer} = { + string: RULES_FILE.content, + buffer: Buffer.from(RULES_FILE.content), + }; + Object.keys(sources).forEach((key) => { + it(`should resolve on success when source specified as a ${key}`, () => { + const [createRuleset, updateRelease] = stubReleaseFromSource(); + + return securityRules.releaseFirestoreRulesetFromSource(sources[key]) + .then((ruleset) => { + expect(ruleset.name).to.equal('foo'); + expect(ruleset.createTime).to.equal(CREATE_TIME_UTC); + expect(ruleset.source.length).to.equal(1); + + const file = ruleset.source[0]; + expect(file.name).equals('firestore.rules'); + expect(file.content).equals('service cloud.firestore{\n}\n'); + + const request: RulesetContent = { + source: { + files: [ + RULES_FILE, + ], + }, + }; + expect(createRuleset).to.have.been.called.calledOnce.and.calledWith(request); + expect(updateRelease).to.have.been.calledOnce.and.calledWith('cloud.firestore', ruleset.name); + }); + }); + }); + }); + + describe('releaseStorageRuleset', () => { + INVALID_RULESETS.forEach((invalidRuleset) => { + it(`should reject when called with: ${JSON.stringify(invalidRuleset)}`, () => { + return securityRules.releaseStorageRuleset(invalidRuleset) + .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.include(INVALID_BUCKET_ERROR); + }); + }); + + it('should propagate API errors', () => { + const stub = sinon + .stub(SecurityRulesApiClient.prototype, 'updateRelease') + .rejects(EXPECTED_ERROR); + stubs.push(stub); + return securityRules.releaseStorageRuleset('foo') + .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(FIRESTORE_RULESET_RELEASE); + stubs.push(stub); + + return securityRules.releaseStorageRuleset('foo') + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith( + 'firebase.storage/bucketName.appspot.com', 'foo'); + }); + }); + + it('should resolve on success when a custom bucket name is specified', () => { + const stub = sinon + .stub(SecurityRulesApiClient.prototype, 'updateRelease') + .resolves(FIRESTORE_RULESET_RELEASE); + stubs.push(stub); + + return securityRules.releaseStorageRuleset('foo', 'other.appspot.com') + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith( + 'firebase.storage/other.appspot.com', 'foo'); + }); + }); + + it('should resolve on success when the ruleset specified as an object', () => { + const stub = sinon + .stub(SecurityRulesApiClient.prototype, 'updateRelease') + .resolves(FIRESTORE_RULESET_RELEASE); + stubs.push(stub); + + return securityRules.releaseStorageRuleset({ name: 'foo', createTime: 'time' }) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith( + 'firebase.storage/bucketName.appspot.com', 'foo'); + }); + }); + }); + + describe('releaseStorageRulesetFromSource', () => { + const RULES_FILE = { + name: 'storage.rules', + content: 'test source {}', + }; + const RULES_CONTENT: RulesetContent = { + source: { + files: [ + RULES_FILE, + ], + }, + }; + + INVALID_SOURCES.forEach((invalidSource) => { + it(`should reject when called with source: ${JSON.stringify(invalidSource)}`, () => { + return securityRules.releaseStorageRulesetFromSource(invalidSource) + .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.include(INVALID_BUCKET_ERROR); + }); + }); + + it('should propagate API errors', () => { + const stub = sinon + .stub(SecurityRulesApiClient.prototype, 'createRuleset') + .rejects(EXPECTED_ERROR); + stubs.push(stub); + return securityRules.releaseStorageRulesetFromSource('foo') + .should.eventually.be.rejected.and.deep.include(EXPECTED_ERROR); + }); + + const sources: {[key: string]: string | Buffer} = { + string: RULES_FILE.content, + buffer: Buffer.from(RULES_FILE.content), + }; + Object.keys(sources).forEach((key) => { + it(`should resolve on success when source specified as a ${key} for default bucket`, () => { + const [createRuleset, updateRelease] = stubReleaseFromSource(); + + return securityRules.releaseStorageRulesetFromSource(sources[key]) + .then((ruleset) => { + expect(ruleset.name).to.equal('foo'); + expect(ruleset.createTime).to.equal(CREATE_TIME_UTC); + expect(ruleset.source.length).to.equal(1); + + const file = ruleset.source[0]; + expect(file.name).equals('firestore.rules'); + expect(file.content).equals('service cloud.firestore{\n}\n'); + + expect(createRuleset).to.have.been.called.calledOnce.and.calledWith(RULES_CONTENT); + expect(updateRelease).to.have.been.calledOnce.and.calledWith( + 'firebase.storage/bucketName.appspot.com', ruleset.name); + }); + }); + }); + + Object.keys(sources).forEach((key) => { + it(`should resolve on success when source specified as a ${key} for a custom bucket`, () => { + const [createRuleset, updateRelease] = stubReleaseFromSource(); + + return securityRules.releaseStorageRulesetFromSource(sources[key], 'other.appspot.com') + .then((ruleset) => { + expect(ruleset.name).to.equal('foo'); + expect(ruleset.createTime).to.equal(CREATE_TIME_UTC); + expect(ruleset.source.length).to.equal(1); + + const file = ruleset.source[0]; + expect(file.name).equals('firestore.rules'); + expect(file.content).equals('service cloud.firestore{\n}\n'); + + expect(createRuleset).to.have.been.called.calledOnce.and.calledWith(RULES_CONTENT); + expect(updateRelease).to.have.been.calledOnce.and.calledWith( + 'firebase.storage/other.appspot.com', ruleset.name); + }); + }); + }); + }); + + describe('createRulesFileFromSource', () => { + const INVALID_STRINGS: any[] = [null, undefined, '', 1, true, {}, []]; + + INVALID_STRINGS.forEach((invalidName) => { + it(`should throw if the name is ${JSON.stringify(invalidName)}`, () => { + expect(() => securityRules.createRulesFileFromSource(invalidName, 'test')) + .to.throw('Name must be a non-empty string.'); + }); + }); + + const invalidSources = [...INVALID_STRINGS]; + invalidSources.forEach((invalidSource) => { + it(`should throw if the source is ${JSON.stringify(invalidSource)}`, () => { + expect(() => securityRules.createRulesFileFromSource('test.rules', invalidSource)) + .to.throw('Source must be a non-empty string or a Buffer.'); + }); + }); + + it('should succeed when source specified as a string', () => { + const file = securityRules.createRulesFileFromSource('test.rules', 'test source {}'); + expect(file.name).to.equal('test.rules'); + expect(file.content).to.equal('test source {}'); + }); + + it('should succeed when source specified as a Buffer', () => { + const file = securityRules.createRulesFileFromSource('test.rules', Buffer.from('test source {}')); + expect(file.name).to.equal('test.rules'); + expect(file.content).to.equal('test source {}'); + }); + }); + + describe('createRuleset', () => { + const RULES_FILE = { + name: 'test.rules', + content: 'test source {}', + }; + + it('should propagate API errors', () => { + const stub = sinon + .stub(SecurityRulesApiClient.prototype, 'createRuleset') + .rejects(EXPECTED_ERROR); + stubs.push(stub); + return securityRules.createRuleset(RULES_FILE) + .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 as any); + stubs.push(stub); + return securityRules.createRuleset(RULES_FILE) + .should.eventually.be.rejected.and.have.property( + 'message', 'Invalid Ruleset response: null'); + }); + + it('should reject when API response does not contain a name', () => { + const response = deepCopy(FIRESTORE_RULESET_RESPONSE); + response.name = ''; + const stub = sinon + .stub(SecurityRulesApiClient.prototype, 'createRuleset') + .resolves(response); + stubs.push(stub); + return securityRules.createRuleset(RULES_FILE) + .should.eventually.be.rejected.and.have.property( + 'message', `Invalid Ruleset response: ${JSON.stringify(response)}`); + }); + + it('should reject when API response does not contain a createTime', () => { + const response = deepCopy(FIRESTORE_RULESET_RESPONSE); + response.createTime = ''; + const stub = sinon + .stub(SecurityRulesApiClient.prototype, 'createRuleset') + .resolves(response); + stubs.push(stub); + return securityRules.createRuleset(RULES_FILE) + .should.eventually.be.rejected.and.have.property( + 'message', `Invalid Ruleset response: ${JSON.stringify(response)}`); + }); + + it('should resolve with Ruleset on success', () => { + const stub = sinon + .stub(SecurityRulesApiClient.prototype, 'createRuleset') + .resolves(FIRESTORE_RULESET_RESPONSE); + stubs.push(stub); + + return securityRules.createRuleset(RULES_FILE) + .then((ruleset) => { + expect(ruleset.name).to.equal('foo'); + expect(ruleset.createTime).to.equal(CREATE_TIME_UTC); + expect(ruleset.source.length).to.equal(1); + + const file = ruleset.source[0]; + expect(file.name).equals('firestore.rules'); + expect(file.content).equals('service cloud.firestore{\n}\n'); + + const request: RulesetContent = { + source: { + files: [ + RULES_FILE, + ], + }, + }; + expect(stub).to.have.been.called.calledOnce.and.calledWith(request); + }); + }); + }); + + describe('deleteRuleset', () => { + it('should propagate API errors', () => { + const stub = sinon + .stub(SecurityRulesApiClient.prototype, 'deleteRuleset') + .rejects(EXPECTED_ERROR); + stubs.push(stub); + return securityRules.deleteRuleset('foo') + .should.eventually.be.rejected.and.deep.include(EXPECTED_ERROR); + }); + + it('should resolve on success', () => { + const stub = sinon + .stub(SecurityRulesApiClient.prototype, 'deleteRuleset') + .resolves(); + stubs.push(stub); + + return securityRules.deleteRuleset('foo'); + }); + }); + + describe('listRulesetMetadata', () => { + const LIST_RULESETS_RESPONSE = { + rulesets: [ + { + name: 'projects/test-project/rulesets/rs1', + createTime: '2019-03-08T23:45:23.288047Z', + }, + { + name: 'projects/test-project/rulesets/rs2', + createTime: '2019-03-08T23:45:23.288047Z', + }, + ], + nextPageToken: 'next', + }; + + it('should propagate API errors', () => { + const stub = sinon + .stub(SecurityRulesApiClient.prototype, 'listRulesets') + .rejects(EXPECTED_ERROR); + stubs.push(stub); + return securityRules.listRulesetMetadata() + .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 as any); + stubs.push(stub); + return securityRules.listRulesetMetadata() + .should.eventually.be.rejected.and.have.property( + 'message', 'Invalid ListRulesets response: null'); + }); + + it('should reject when API response does not contain rulesets', () => { + const response: any = deepCopy(LIST_RULESETS_RESPONSE); + response.rulesets = ''; + const stub = sinon + .stub(SecurityRulesApiClient.prototype, 'listRulesets') + .resolves(response); + stubs.push(stub); + return securityRules.listRulesetMetadata() + .should.eventually.be.rejected.and.have.property( + 'message', `Invalid ListRulesets response: ${JSON.stringify(response)}`); + }); + + it('should resolve with RulesetMetadataList on success', () => { + const stub = sinon + .stub(SecurityRulesApiClient.prototype, 'listRulesets') + .resolves(LIST_RULESETS_RESPONSE); + stubs.push(stub); + + return securityRules.listRulesetMetadata() + .then((result) => { + expect(result.rulesets.length).equals(2); + expect(result.rulesets[0].name).equals('rs1'); + expect(result.rulesets[0].createTime).equals(CREATE_TIME_UTC); + expect(result.rulesets[1].name).equals('rs2'); + expect(result.rulesets[1].createTime).equals(CREATE_TIME_UTC); + + expect(result.nextPageToken).equals('next'); + + expect(stub).to.have.been.calledOnce.and.calledWith(100); + }); + }); + + it('should resolve with RulesetMetadataList on success when called with page size', () => { + const stub = sinon + .stub(SecurityRulesApiClient.prototype, 'listRulesets') + .resolves(LIST_RULESETS_RESPONSE); + stubs.push(stub); + + return securityRules.listRulesetMetadata(10) + .then((result) => { + expect(result.rulesets.length).equals(2); + expect(result.rulesets[0].name).equals('rs1'); + expect(result.rulesets[0].createTime).equals(CREATE_TIME_UTC); + expect(result.rulesets[1].name).equals('rs2'); + expect(result.rulesets[1].createTime).equals(CREATE_TIME_UTC); + + expect(result.nextPageToken).equals('next'); + + expect(stub).to.have.been.calledOnce.and.calledWith(10); + }); + }); + + it('should resolve with RulesetMetadataList on success when called with page token', () => { + const stub = sinon + .stub(SecurityRulesApiClient.prototype, 'listRulesets') + .resolves(LIST_RULESETS_RESPONSE); + stubs.push(stub); + + return securityRules.listRulesetMetadata(10, 'next') + .then((result) => { + expect(result.rulesets.length).equals(2); + expect(result.rulesets[0].name).equals('rs1'); + expect(result.rulesets[0].createTime).equals(CREATE_TIME_UTC); + expect(result.rulesets[1].name).equals('rs2'); + expect(result.rulesets[1].createTime).equals(CREATE_TIME_UTC); + + expect(result.nextPageToken).equals('next'); + + expect(stub).to.have.been.calledOnce.and.calledWith(10, 'next'); + }); + }); + + it('should resolve with RulesetMetadataList when the response contains no page token', () => { + const response = deepCopy(LIST_RULESETS_RESPONSE); + delete (response as any).nextPageToken; + const stub = sinon + .stub(SecurityRulesApiClient.prototype, 'listRulesets') + .resolves(response); + stubs.push(stub); + + return securityRules.listRulesetMetadata(10, 'next') + .then((result) => { + expect(result.rulesets.length).equals(2); + expect(result.rulesets[0].name).equals('rs1'); + expect(result.rulesets[0].createTime).equals(CREATE_TIME_UTC); + expect(result.rulesets[1].name).equals('rs2'); + expect(result.rulesets[1].createTime).equals(CREATE_TIME_UTC); + + expect(result.nextPageToken).to.be.undefined; + + expect(stub).to.have.been.calledOnce.and.calledWith(10, 'next'); + }); + }); + }); +}); diff --git a/test/unit/storage/index.spec.ts b/test/unit/storage/index.spec.ts new file mode 100644 index 0000000000..7924f8a61c --- /dev/null +++ b/test/unit/storage/index.spec.ts @@ -0,0 +1,148 @@ +/*! + * @license + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +import * as chai from 'chai'; +import * as sinonChai from 'sinon-chai'; +import { createSandbox, SinonSandbox } from 'sinon'; +import * as chaiAsPromised from 'chai-as-promised'; + +import * as mocks from '../../resources/mocks'; +import { App } from '../../../src/app/index'; +import * as StorageUtils from '../../../src/storage/utils'; +import { getStorage, Storage, getDownloadURL } from '../../../src/storage/index'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +describe('Storage', () => { + let mockApp: App; + let mockCredentialApp: App; + + const noProjectIdError = + 'Failed to initialize Google Cloud Storage client with the ' + + 'available credential. Must initialize the SDK with a certificate credential or ' + + 'application default credentials to use Cloud Storage API.'; + + let sandbox: SinonSandbox; + beforeEach(() => { + mockApp = mocks.app(); + mockCredentialApp = mocks.mockCredentialApp(); + sandbox = createSandbox(); + }); + afterEach(() => { + sandbox.restore(); + }); + + describe('getStorage()', () => { + it('should throw when default app is not available', () => { + expect(() => { + return getStorage(); + }).to.throw('The default Firebase app does not exist.'); + }); + + it('should reject given an invalid credential without project ID', () => { + // Project ID not set in the environment. + delete process.env.GOOGLE_CLOUD_PROJECT; + delete process.env.GCLOUD_PROJECT; + expect(() => getStorage(mockCredentialApp)).to.throw(noProjectIdError); + }); + + it('should not throw given a valid app', () => { + expect(() => { + return getStorage(mockApp); + }).not.to.throw(); + }); + + it('should return the same instance for a given app instance', () => { + const storage1: Storage = getStorage(mockApp); + const storage2: Storage = getStorage(mockApp); + expect(storage1).to.equal(storage2); + }); + + it('should return an error when no metadata is available', async () => { + sandbox + .stub(StorageUtils, 'getFirebaseMetadata') + .returns(Promise.resolve({} as StorageUtils.FirebaseMetadata)); + const storage1 = getStorage(mockApp); + const fileRef = storage1.bucket('gs://mock').file('abc'); + await expect(getDownloadURL(fileRef)).to.be.rejectedWith( + 'No download token available. Please create one in the Firebase Console.' + ); + }); + + it('should return an error when unable to fetch metadata', async () => { + const error = new Error('Could not get metadata'); + sandbox + .stub(StorageUtils, 'getFirebaseMetadata') + .returns(Promise.reject(error)); + const storage1 = getStorage(mockApp); + const fileRef = storage1.bucket('gs://mock').file('abc'); + await expect(getDownloadURL(fileRef)).to.be.rejectedWith( + error + ); + }); + it('should return the proper download url when metadata is available', async () => { + const downloadTokens = ['abc', 'def']; + sandbox + .stub(StorageUtils, 'getFirebaseMetadata') + .returns( + Promise.resolve({ + downloadTokens: downloadTokens.join(','), + } as StorageUtils.FirebaseMetadata) + ); + const storage1 = getStorage(mockApp); + const fileRef = storage1.bucket('gs://mock').file('abc'); + await expect(getDownloadURL(fileRef)).to.eventually.eq( + `https://firebasestorage.googleapis.com/v0/b/${fileRef.bucket.name}/o/${encodeURIComponent(fileRef.name)}?alt=media&token=${downloadTokens[0]}` + ); + }); + it('should use the emulator host name when either envs are set', async () => { + const HOST = 'localhost:9091'; + const envsToCheck = [ + { envName: 'FIREBASE_STORAGE_EMULATOR_HOST', value: HOST }, + { envName: 'STORAGE_EMULATOR_HOST', value: `http://${HOST}` }, + ]; + const downloadTokens = ['abc', 'def']; + sandbox.stub(StorageUtils, 'getFirebaseMetadata').returns( + Promise.resolve({ + downloadTokens: downloadTokens.join(','), + } as StorageUtils.FirebaseMetadata) + ); + for (const { envName, value } of envsToCheck) { + + delete process.env.STORAGE_EMULATOR_HOST; + delete process.env[envName]; + process.env[envName] = value; + + // Need to create a new mock app to force `getStorage`'s checking of env vars. + const storage1 = getStorage(mocks.app(envName)); + const fileRef = storage1.bucket('gs://mock').file('abc'); + await expect(getDownloadURL(fileRef)).to.eventually.eq( + `http://${HOST}/v0/b/${fileRef.bucket.name}/o/${encodeURIComponent( + fileRef.name + )}?alt=media&token=${downloadTokens[0]}` + ); + delete process.env[envName]; + } + }); + }); +}); diff --git a/test/unit/storage/storage.spec.ts b/test/unit/storage/storage.spec.ts index 4b95529ef3..ea656a1e92 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"); @@ -17,11 +18,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/app/firebase-app'; +import { Storage } from '../../../src/storage/index'; describe('Storage', () => { let mockApp: FirebaseApp; @@ -90,14 +91,14 @@ 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(() => { - const bucketAny: any = storage.bucket; - bucketAny(invalidName); - }).to.throw(expectedError); - }); + it(`should throw given invalid bucket name: ${ JSON.stringify(invalidName) }`, () => { + expect(() => { + const bucketAny: any = storage.bucket; + bucketAny(invalidName); + }).to.throw(expectedError); + }); }); }); @@ -112,4 +113,33 @@ describe('Storage', () => { expect(storage.bucket('foo').name).to.equal('foo'); }); }); + + describe('Emulator mode', () => { + const VALID_EMULATOR_HOST = 'localhost:9199'; + const INVALID_EMULATOR_HOST = 'https://localhost:9199'; + + beforeEach(() => { + delete process.env.STORAGE_EMULATOR_HOST; + delete process.env.FIREBASE_STORAGE_EMULATOR_HOST; + }); + + it('sets STORAGE_EMULATOR_HOST if FIREBASE_STORAGE_EMULATOR_HOST is set', () => { + 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(() => { + delete process.env.STORAGE_EMULATOR_HOST; + delete process.env.FIREBASE_STORAGE_EMULATOR_HOST; + }); + }) }); diff --git a/test/unit/utils.ts b/test/unit/utils.ts index b9301d91ba..2eb608397e 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"); @@ -15,54 +16,82 @@ */ import * as _ from 'lodash'; -import * as nock from 'nock'; - +import * as sinon from 'sinon'; import * as mocks from '../resources/mocks'; - -import {FirebaseNamespace} from '../../src/firebase-namespace'; -import {FirebaseApp, FirebaseAppOptions} from '../../src/firebase-app'; +import { AppOptions } from '../../src/firebase-namespace-api'; +import { FirebaseApp, FirebaseAppInternals, FirebaseAccessToken } from '../../src/app/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) { - const mockFirebaseNamespaceInternals = new FirebaseNamespace().INTERNAL; - return new FirebaseApp(options as FirebaseAppOptions, mocks.appName, mockFirebaseNamespaceInternals); +export function createAppWithOptions(options: object): FirebaseApp { + return new FirebaseApp(options as AppOptions, mocks.appName); } +/** @return {string} A randomly generated access token string. */ +export function generateRandomAccessToken(): string { + return 'access_token_' + _.random(999999999); +} + /** - * Returns a mocked out success response from the URL generating Google access tokens given a JWT - * signed with a service account private key. - * - * Calling this once will mock ALL future requests to this endpoint. Use nock.cleanAll() to unmock. + * Creates a stub for retrieving an access token from a FirebaseApp. All services should use this + * method for stubbing the OAuth2 flow during unit tests. * - * @param {string} [token] The optional access token to return. If not specified, a random one - * is created. - * @param {number} [expiresIn] The optional expires in value to use for the access token. - * @return {Object} A nock response object. + * @param {string} accessToken The access token string to return. + * @param {FirebaseApp} app The app instance to stub. If not specified, the stub will affect all apps. + * @return {sinon.SinonStub} A Sinon stub. */ -export function mockFetchAccessTokenRequests( - token: string = generateRandomAccessToken(), - expiresIn: number = 60 * 60, -): nock.Scope { - return nock('https://accounts.google.com:443') - .persist() - .post('/o/oauth2/token') - .reply(200, { - access_token: token, - token_type: 'Bearer', - expires_in: expiresIn, - }, { - 'cache-control': 'no-cache, no-store, max-age=0, must-revalidate', - }); +export function stubGetAccessToken(accessToken?: string, app?: FirebaseApp): sinon.SinonStub { + if (typeof accessToken === 'undefined') { + accessToken = generateRandomAccessToken(); + } + const result: FirebaseAccessToken = { + accessToken, + expirationTime: Date.now() + 3600, + }; + if (app) { + return sinon.stub(app.INTERNAL, 'getToken').resolves(result); + } else { + return sinon.stub(FirebaseAppInternals.prototype, 'getToken').resolves(result); + } } +/** + * Creates a mock HTTP response from the given data and parameters. + * + * @param {object | string} data Data to be included in the response body. + * @param {number=} status HTTP status code (defaults to 200). + * @param {*=} headers HTTP headers to be included in the ersponse. + * @return {HttpResponse} An HTTP response object. + */ +export function responseFrom(data: object | string, status = 200, headers: any = {}): HttpResponse { + let responseData: any; + let responseText: string; + if (typeof data === 'object') { + responseData = data; + responseText = JSON.stringify(data); + } else { + try { + responseData = JSON.parse(data); + } catch (error) { + responseData = null; + } + responseText = data as string; + } + return { + status, + headers, + data: responseData, + text: responseText, + isJson: () => responseData != null, + }; +} -/** @return {string} A randomly generated access token string. */ -export function generateRandomAccessToken(): string { - return 'access_token_' + _.random(999999999); +export function errorFrom(data: any, status = 500): HttpError { + return new HttpError(responseFrom(data, status)); } diff --git a/test/unit/utils/api-request.spec.ts b/test/unit/utils/api-request.spec.ts index 5f8c6a5664..fd92a9c191 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"); @@ -16,11 +17,6 @@ 'use strict'; -// Use untyped import syntax for Node built-ins. -import https = require('https'); -import stream = require('stream'); - -import * as _ from 'lodash'; import * as chai from 'chai'; import * as nock from 'nock'; import * as sinon from 'sinon'; @@ -30,10 +26,14 @@ 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/app/firebase-app'; import { - SignedApiRequestHandler, HttpRequestHandler, ApiSettings, + 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 * as zlib from 'zlib'; chai.should(); chai.use(sinonChai); @@ -41,15 +41,9 @@ chai.use(chaiAsPromised); const expect = chai.expect; -const mockPort = 443; const mockHost = 'www.example.com'; const mockPath = '/foo/bar'; - -const mockSuccessResponse = { - foo: 'one', - bar: 2, - baz: true, -}; +const mockUrl = `https://${mockHost}${mockPath}`; const mockErrorResponse = { error: { @@ -59,45 +53,6 @@ const mockErrorResponse = { }; const mockTextErrorResponse = 'Text error response'; -const mockTextSuccessResponse = 'Text success response'; - -const mockRequestData = { - foo: 'one', - bar: 2, - baz: true, -}; - -const mockRequestHeaders = { - 'content-type': 'application/json', -}; - -/** - * Returns a mocked out successful response for a dummy URL. - * - * @param {string} [responseContentType] Optional response content type. - * @param {string} [method] Optional request method. - * @param {any} [response] Optional response. - * - * @return {Object} A nock response object. - */ -function mockRequest( - responseContentType = 'application/json', - method = 'GET', - response?: any, -) { - if (typeof response === 'undefined') { - response = mockSuccessResponse; - if (responseContentType === 'text/html') { - response = mockTextSuccessResponse; - } - } - - return nock('https://' + mockHost) - .intercept(mockPath, method) - .reply(200, response, { - 'content-type': responseContentType, - }); -} /** * Returns a mocked out HTTP error response for a dummy URL. @@ -112,7 +67,7 @@ function mockRequestWithHttpError( statusCode = 400, responseContentType = 'application/json', response: any = mockErrorResponse, -) { +): nock.Scope { if (responseContentType === 'text/html') { response = mockTextErrorResponse; } @@ -132,305 +87,1290 @@ function mockRequestWithHttpError( * * @return {Object} A nock response object. */ -function mockRequestWithError(err: Error) { +function mockRequestWithError(err: any): nock.Scope { return nock('https://' + mockHost) .get(mockPath) .replyWithError(err); } -describe('HttpRequestHandler', () => { - let mockedRequests: nock.Scope[] = []; - let requestWriteSpy: sinon.SinonSpy; - let httpsRequestStub: sinon.SinonStub; - let mockRequestStream: mocks.MockStream; - const httpRequestHandler = new HttpRequestHandler(); +/** + * Returns a new RetryConfig instance for testing. This is same as the default + * RetryConfig, with the backOffFactor set to 0 to avoid delays. + * + * @return {RetryConfig} A new RetryConfig instance. + */ +function testRetryConfig(): RetryConfig { + const config = defaultRetryConfig(); + config.backOffFactor = 0; + return config; +} - beforeEach(() => { - mockRequestStream = new mocks.MockStream(); - }); +describe('HttpClient', () => { + let mockedRequests: nock.Scope[] = []; + let transportSpy: sinon.SinonSpy | null = null; + let delayStub: sinon.SinonStub | null = null; + let clock: sinon.SinonFakeTimers | null = null; + + const sampleMultipartData = '--boundary\r\n' + + 'Content-type: application/json\r\n\r\n' + + '{"foo": 1}\r\n' + + '--boundary\r\n' + + 'Content-type: text/plain\r\n\r\n' + + 'foo bar\r\n' + + '--boundary--\r\n'; afterEach(() => { - _.forEach(mockedRequests, (mockedRequest) => mockedRequest.done()); + mockedRequests.forEach((mockedRequest) => mockedRequest.done()); mockedRequests = []; - - if (requestWriteSpy && requestWriteSpy.restore) { - requestWriteSpy.restore(); + if (transportSpy) { + transportSpy.restore(); + transportSpy = null; + } + if (delayStub) { + delayStub.restore(); + delayStub = null; + } + if (clock) { + clock.restore(); + clock = null; } + }); + + const invalidNumbers: any[] = ['string', null, undefined, {}, [], true, false, NaN, -1]; + const invalidArrays: any[] = ['string', null, {}, true, false, NaN, 0, 1]; - if (httpsRequestStub && httpsRequestStub.restore) { - httpsRequestStub.restore(); + invalidNumbers.forEach((maxRetries: any) => { + it(`should throw when maxRetries is: ${maxRetries}`, () => { + expect(() => { + new HttpClient({ maxRetries } as any); + }).to.throw('maxRetries must be a non-negative integer'); + }); + }); + + invalidNumbers.forEach((backOffFactor: any) => { + if (typeof backOffFactor !== 'undefined') { + it(`should throw when backOffFactor is: ${backOffFactor}`, () => { + expect(() => { + new HttpClient({ maxRetries: 1, backOffFactor } as any); + }).to.throw('backOffFactor must be a non-negative number'); + }); } }); + invalidNumbers.forEach((maxDelayInMillis: any) => { + it(`should throw when maxDelayInMillis is: ${maxDelayInMillis}`, () => { + expect(() => { + new HttpClient({ maxRetries: 1, maxDelayInMillis } as any); + }).to.throw('maxDelayInMillis must be a non-negative integer'); + }); + }); - describe('sendRequest', () => { - it('should be rejected, after 1 retry, on multiple network errors', () => { - mockedRequests.push(mockRequestWithError(new Error('first error'))); - mockedRequests.push(mockRequestWithError(new Error('second error'))); + invalidArrays.forEach((ioErrorCodes: any) => { + it(`should throw when ioErrorCodes is: ${ioErrorCodes}`, () => { + expect(() => { + new HttpClient({ maxRetries: 1, maxDelayInMillis: 10000, ioErrorCodes } as any); + }).to.throw('ioErrorCodes must be an array'); + }); + }); - const sendRequestPromise = httpRequestHandler.sendRequest(mockHost, mockPort, mockPath, 'GET'); + invalidArrays.forEach((statusCodes: any) => { + it(`should throw when statusCodes is: ${statusCodes}`, () => { + expect(() => { + new HttpClient({ maxRetries: 1, maxDelayInMillis: 10000, statusCodes } as any); + }).to.throw('statusCodes must be an array'); + }); + }); - return sendRequestPromise - .then(() => { - throw new Error('Unexpected success.'); - }) - .catch((response) => { - expect(response).to.have.keys(['error', 'statusCode']); - expect(response.error).to.have.property('code', 'app/network-error'); - expect(response.statusCode).to.equal(502); - }); + it('should be fulfilled for a 2xx response with a json payload', () => { + const respData = { foo: 'bar' }; + const scope = nock('https://' + mockHost) + .get(mockPath) + .reply(200, respData, { + 'content-type': 'application/json', + }); + mockedRequests.push(scope); + const client = new HttpClient(); + return client.send({ + method: 'GET', + url: mockUrl, + }).then((resp) => { + expect(resp.status).to.equal(200); + expect(resp.headers['content-type']).to.equal('application/json'); + expect(resp.text).to.equal(JSON.stringify(respData)); + expect(resp.data).to.deep.equal(respData); + expect(resp.multipart).to.be.undefined; + expect(resp.isJson()).to.be.true; }); + }); - it('should succeed, after 1 retry, on a single network error', () => { - mockedRequests.push(mockRequestWithError(new Error('first error'))); - mockedRequests.push(mockRequest()); + it('should be fulfilled for a 2xx response with a text payload', () => { + const respData = 'foo bar'; + const scope = nock('https://' + mockHost) + .get(mockPath) + .reply(200, respData, { + 'content-type': 'text/plain', + }); + mockedRequests.push(scope); + const client = new HttpClient(); + return client.send({ + method: 'GET', + url: mockUrl, + }).then((resp) => { + expect(resp.status).to.equal(200); + expect(resp.headers['content-type']).to.equal('text/plain'); + expect(resp.text).to.equal(respData); + expect(() => { resp.data; }).to.throw('Error while parsing response data'); + expect(resp.multipart).to.be.undefined; + expect(resp.isJson()).to.be.false; + }); + }); - return httpRequestHandler.sendRequest(mockHost, mockPort, mockPath, 'GET') - .should.eventually.be.fulfilled.and.deep.equal(mockSuccessResponse); + it('should be fulfilled for a 2xx response with an empty multipart payload', () => { + const scope = nock('https://' + mockHost) + .get(mockPath) + .reply(200, '--boundary--\r\n', { + 'content-type': 'multipart/mixed; boundary=boundary', + }); + mockedRequests.push(scope); + const client = new HttpClient(); + return client.send({ + method: 'GET', + url: mockUrl, + }).then((resp) => { + expect(resp.status).to.equal(200); + expect(resp.headers['content-type']).to.equal('multipart/mixed; boundary=boundary'); + expect(resp.multipart).to.not.be.undefined; + expect(resp.multipart!.length).to.equal(0); + expect(() => { resp.text; }).to.throw('Unable to parse multipart payload as text'); + expect(() => { resp.data; }).to.throw('Unable to parse multipart payload as JSON'); + expect(resp.isJson()).to.be.false; }); + }); - it('should be rejected on a network timeout', () => { - httpsRequestStub = sinon.stub(https, 'request'); - httpsRequestStub.returns(mockRequestStream); + it('should be fulfilled for a 2xx response with a multipart payload', () => { + const scope = nock('https://' + mockHost) + .get(mockPath) + .reply(200, sampleMultipartData, { + 'content-type': 'multipart/mixed; boundary=boundary', + }); + mockedRequests.push(scope); + const client = new HttpClient(); + return client.send({ + method: 'GET', + url: mockUrl, + }).then((resp) => { + expect(resp.status).to.equal(200); + expect(resp.headers['content-type']).to.equal('multipart/mixed; boundary=boundary'); + expect(resp.multipart).to.exist; + expect(resp.multipart!.map((buffer) => buffer.toString('utf-8'))).to.deep.equal(['{"foo": 1}', 'foo bar']); + expect(() => { resp.text; }).to.throw('Unable to parse multipart payload as text'); + expect(() => { resp.data; }).to.throw('Unable to parse multipart payload as JSON'); + expect(resp.isJson()).to.be.false; + }); + }); - const mockSocket = new mocks.MockSocketEmitter(); + it('should be fulfilled for a 2xx response with any multipart payload', () => { + const scope = nock('https://' + mockHost) + .get(mockPath) + .reply(200, sampleMultipartData, { + 'content-type': 'multipart/something; boundary=boundary', + }); + mockedRequests.push(scope); + const client = new HttpClient(); + return client.send({ + method: 'GET', + url: mockUrl, + }).then((resp) => { + expect(resp.status).to.equal(200); + expect(resp.headers['content-type']).to.equal('multipart/something; boundary=boundary'); + expect(resp.multipart).to.exist; + expect(resp.multipart!.map((buffer) => buffer.toString('utf-8'))).to.deep.equal(['{"foo": 1}', 'foo bar']); + expect(() => { resp.text; }).to.throw('Unable to parse multipart payload as text'); + expect(() => { resp.data; }).to.throw('Unable to parse multipart payload as JSON'); + expect(resp.isJson()).to.be.false; + }); + }); - const sendRequestPromise = httpRequestHandler.sendRequest( - mockHost, mockPort, mockPath, 'GET', undefined, undefined, 5000, - ); + it('should handle as a text response when boundary not present', () => { + const respData = 'foo bar'; + const scope = nock('https://' + mockHost) + .get(mockPath) + .reply(200, respData, { + 'content-type': 'multipart/mixed', + }); + mockedRequests.push(scope); + const client = new HttpClient(); + return client.send({ + method: 'GET', + url: mockUrl, + }).then((resp) => { + expect(resp.status).to.equal(200); + expect(resp.headers['content-type']).to.equal('multipart/mixed'); + expect(resp.multipart).to.be.undefined; + expect(resp.text).to.equal(respData); + expect(() => { resp.data; }).to.throw('Error while parsing response data'); + expect(resp.isJson()).to.be.false; + }); + }); - mockRequestStream.emit('socket', mockSocket); - mockSocket.emit('timeout'); + it('should be fulfilled for a 2xx response with a compressed payload', () => { + const deflated: Buffer = zlib.deflateSync('foo bar'); + const scope = nock('https://' + mockHost) + .get(mockPath) + .reply(200, deflated, { + 'content-type': 'text/plain', + 'content-encoding': 'deflate', + }); + mockedRequests.push(scope); + const client = new HttpClient(); + return client.send({ + method: 'GET', + url: mockUrl, + }).then((resp) => { + expect(resp.status).to.equal(200); + expect(resp.headers['content-type']).to.equal('text/plain'); + expect(resp.headers['content-encoding']).to.be.undefined; + expect(resp.multipart).to.be.undefined; + expect(resp.text).to.equal('foo bar'); + expect(() => { resp.data; }).to.throw('Error while parsing response data'); + expect(resp.isJson()).to.be.false; + }); + }); - return sendRequestPromise - .then(() => { - throw new Error('Unexpected success.'); - }) - .catch((response) => { - expect(response).to.have.keys(['error', 'statusCode']); - expect(response.error).to.have.property('code', 'app/network-timeout'); - expect(response.statusCode).to.equal(408); - }); + it('should use the specified HTTP agent', () => { + const respData = { success: true }; + const scope = nock('https://' + mockHost) + .get(mockPath) + .reply(200, respData, { + 'content-type': 'application/json', + }); + mockedRequests.push(scope); + const client = new HttpClient(); + const httpAgent = new Agent(); + + // eslint-disable-next-line @typescript-eslint/no-var-requires + const https = require('https'); + transportSpy = sinon.spy(https, 'request'); + return client.send({ + method: 'GET', + url: mockUrl, + httpAgent, + }).then((resp) => { + expect(resp.status).to.equal(200); + expect(transportSpy!.callCount).to.equal(1); + const options = transportSpy!.args[0][0]; + expect(options.agent).to.equal(httpAgent); }); + }); - it('should forward the provided options on to the underlying https.request() method', () => { - requestWriteSpy = sinon.spy(mockRequestStream, 'write'); - - const mockResponse = new stream.PassThrough(); - mockResponse.write(JSON.stringify(mockSuccessResponse)); - mockResponse.end(); - - httpsRequestStub = sinon.stub(https, 'request'); - httpsRequestStub.callsArgWith(1, mockResponse) - .returns(mockRequestStream); - - return httpRequestHandler.sendRequest( - mockHost, mockPort, mockPath, 'POST', mockRequestData, mockRequestHeaders, - ) - .then((response) => { - expect(response).to.deep.equal(mockSuccessResponse); - expect(httpsRequestStub).to.have.been.calledOnce; - expect(httpsRequestStub.args[0][0]).to.deep.equal({ - method: 'POST', - host: mockHost, - port: mockPort, - path: mockPath, - headers: mockRequestHeaders, - }); - expect(requestWriteSpy).to.have.been.calledOnce.and.calledWith(JSON.stringify(mockRequestData)); - }); + it('should use the default RetryConfig', () => { + const client = new HttpClient(); + const config = (client as any).retry as RetryConfig; + expect(defaultRetryConfig()).to.deep.equal(config); + }); + + it('should make a POST request with the provided headers and data', () => { + const reqData = { request: 'data' }; + const respData = { success: true }; + const scope = nock('https://' + mockHost, { + reqheaders: { + 'Authorization': 'Bearer token', + 'Content-Type': (header) => { + return header.startsWith('application/json'); // auto-inserted + }, + 'My-Custom-Header': 'CustomValue', + }, + }).post(mockPath, reqData) + .reply(200, respData, { + 'content-type': 'application/json', + }); + mockedRequests.push(scope); + const client = new HttpClient(); + return client.send({ + method: 'POST', + url: mockUrl, + headers: { + 'authorization': 'Bearer token', + 'My-Custom-Header': 'CustomValue', + }, + data: reqData, + }).then((resp) => { + expect(resp.status).to.equal(200); + expect(resp.headers['content-type']).to.equal('application/json'); + expect(resp.data).to.deep.equal(respData); + expect(resp.isJson()).to.be.true; + }); + }); + + it('should use the specified content-type header for the body', () => { + const reqData = { request: 'data' }; + const respData = { success: true }; + const scope = nock('https://' + mockHost, { + reqheaders: { + 'Content-Type': (header) => { + return header.startsWith('custom/type'); + }, + }, + }).post(mockPath, reqData) + .reply(200, respData, { + 'content-type': 'application/json', + }); + mockedRequests.push(scope); + const client = new HttpClient(); + return client.send({ + method: 'POST', + url: mockUrl, + headers: { + 'content-type': 'custom/type', + }, + data: reqData, + }).then((resp) => { + expect(resp.status).to.equal(200); + expect(resp.headers['content-type']).to.equal('application/json'); + expect(resp.data).to.deep.equal(respData); + expect(resp.isJson()).to.be.true; + }); + }); + + it('should not mutate the arguments', () => { + const reqData = { request: 'data' }; + const scope = nock('https://' + mockHost, { + reqheaders: { + 'Authorization': 'Bearer token', + 'Content-Type': (header) => { + return header.startsWith('application/json'); // auto-inserted + }, + 'My-Custom-Header': 'CustomValue', + }, + }).post(mockPath, reqData) + .reply(200, { success: true }, { + 'content-type': 'application/json', + }); + mockedRequests.push(scope); + const client = new HttpClient(); + const request: HttpRequestConfig = { + method: 'POST', + url: mockUrl, + headers: { + 'authorization': 'Bearer token', + 'My-Custom-Header': 'CustomValue', + }, + data: reqData, + }; + const requestCopy = deepCopy(request); + return client.send(request).then((resp) => { + expect(resp.status).to.equal(200); + expect(request).to.deep.equal(requestCopy); + }); + }); + + it('should make a GET request with the provided headers and data', () => { + const reqData = { key1: 'value1', key2: 'value2' }; + const respData = { success: true }; + const scope = nock('https://' + mockHost, { + reqheaders: { + 'Authorization': 'Bearer token', + 'My-Custom-Header': 'CustomValue', + }, + }).get(mockPath) + .query(reqData) + .reply(200, respData, { + 'content-type': 'application/json', + }); + mockedRequests.push(scope); + const client = new HttpClient(); + return client.send({ + method: 'GET', + url: mockUrl, + headers: { + 'authorization': 'Bearer token', + 'My-Custom-Header': 'CustomValue', + }, + data: reqData, + }).then((resp) => { + expect(resp.status).to.equal(200); + expect(resp.headers['content-type']).to.equal('application/json'); + expect(resp.data).to.deep.equal(respData); + expect(resp.isJson()).to.be.true; + }); + }); + + 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 scope = nock('https://' + mockHost) + .get(mockPath) + .query(mergedData) + .reply(200, respData, { + 'content-type': 'application/json', + }); + mockedRequests.push(scope); + const client = new HttpClient(); + return client.send({ + method: 'GET', + url: mockUrl + '?key3=value3', + data: reqData, + }).then((resp) => { + expect(resp.status).to.equal(200); + expect(resp.headers['content-type']).to.equal('application/json'); + expect(resp.data).to.deep.equal(respData); + expect(resp.isJson()).to.be.true; + }); + }); + + 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 scope = nock('https://' + mockHost) + .get(mockPath) + .query(mergedData) + .reply(200, respData, { + 'content-type': 'application/json', + }); + mockedRequests.push(scope); + const client = new HttpClient(); + return client.send({ + method: 'GET', + url: mockUrl + '?key3=value+3%21', + data: reqData, + }).then((resp) => { + expect(resp.status).to.equal(200); + expect(resp.headers['content-type']).to.equal('application/json'); + expect(resp.data).to.deep.equal(respData); + expect(resp.isJson()).to.be.true; + }); + }); + + it('should default to https when protocol not specified', () => { + const respData = { foo: 'bar' }; + const scope = nock('https://' + mockHost) + .get(mockPath) + .reply(200, respData, { + 'content-type': 'application/json', + }); + mockedRequests.push(scope); + const client = new HttpClient(); + return client.send({ + method: 'GET', + url: mockUrl.substring('https://'.length), + }).then((resp) => { + expect(resp.status).to.equal(200); + expect(resp.headers['content-type']).to.equal('application/json'); + expect(resp.text).to.equal(JSON.stringify(respData)); + expect(resp.data).to.deep.equal(respData); + expect(resp.multipart).to.be.undefined; + expect(resp.isJson()).to.be.true; + }); + }); + + it('should fail with a GET request containing non-object data', () => { + const err = 'GET requests cannot have a body.'; + const client = new HttpClient(); + return client.send({ + method: 'GET', + url: mockUrl, + timeout: 50, + data: 'non-object-data', + }).should.eventually.be.rejectedWith(err).and.have.property('code', 'app/network-error'); + }); + + it('should make a HEAD request with the provided headers and data', () => { + const reqData = { key1: 'value1', key2: 'value2' }; + const respData = { success: true }; + const scope = nock('https://' + mockHost, { + reqheaders: { + 'Authorization': 'Bearer token', + 'My-Custom-Header': 'CustomValue', + }, + }).head(mockPath) + .query(reqData) + .reply(200, respData, { + 'content-type': 'application/json', + }); + mockedRequests.push(scope); + const client = new HttpClient(); + return client.send({ + method: 'HEAD', + url: mockUrl, + headers: { + 'authorization': 'Bearer token', + 'My-Custom-Header': 'CustomValue', + }, + data: reqData, + }).then((resp) => { + expect(resp.status).to.equal(200); + expect(resp.headers['content-type']).to.equal('application/json'); + expect(resp.data).to.deep.equal(respData); + expect(resp.isJson()).to.be.true; + }); + }); + + it('should fail with a HEAD request containing non-object data', () => { + const err = 'HEAD requests cannot have a body.'; + const client = new HttpClient(); + return client.send({ + method: 'HEAD', + url: mockUrl, + timeout: 50, + data: 'non-object-data', + }).should.eventually.be.rejectedWith(err).and.have.property('code', 'app/network-error'); + }); + + it('should fail with an HttpError for a 4xx response', () => { + const data = { error: 'data' }; + mockedRequests.push(mockRequestWithHttpError(400, 'application/json', data)); + const client = new HttpClient(); + return client.send({ + method: 'GET', + url: mockUrl, + }).catch((err: HttpError) => { + expect(err.message).to.equal('Server responded with status 400.'); + const resp = err.response; + expect(resp.status).to.equal(400); + expect(resp.headers['content-type']).to.equal('application/json'); + expect(resp.data).to.deep.equal(data); + expect(resp.isJson()).to.be.true; + }); + }); + + it('should fail with an HttpError for a 5xx response', () => { + const data = { error: 'data' }; + mockedRequests.push(mockRequestWithHttpError(500, 'application/json', data)); + const client = new HttpClient(); + return client.send({ + method: 'GET', + url: mockUrl, + }).catch((err: HttpError) => { + expect(err.message).to.equal('Server responded with status 500.'); + const resp = err.response; + expect(resp.status).to.equal(500); + expect(resp.headers['content-type']).to.equal('application/json'); + expect(resp.data).to.deep.equal(data); + expect(resp.isJson()).to.be.true; + }); + }); + + it('should fail for an error response with a multipart payload', () => { + const scope = nock('https://' + mockHost) + .get(mockPath) + .reply(500, sampleMultipartData, { + 'content-type': 'multipart/mixed; boundary=boundary', + }); + mockedRequests.push(scope); + const client = new HttpClient(); + return client.send({ + method: 'GET', + url: mockUrl, + }).catch((err: HttpError) => { + expect(err.message).to.equal('Server responded with status 500.'); + const resp = err.response; + expect(resp.status).to.equal(500); + expect(resp.headers['content-type']).to.equal('multipart/mixed; boundary=boundary'); + expect(resp.multipart).to.exist; + expect(resp.multipart!.map((buffer) => buffer.toString('utf-8'))).to.deep.equal(['{"foo": 1}', 'foo bar']); + expect(() => { resp.text; }).to.throw('Unable to parse multipart payload as text'); + expect(() => { resp.data; }).to.throw('Unable to parse multipart payload as JSON'); + expect(resp.isJson()).to.be.false; }); + }); - describe('with JSON response', () => { - it('should be rejected given a 4xx response', () => { - mockedRequests.push(mockRequestWithHttpError(400)); + it('should fail with a FirebaseAppError for a network 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({ + method: 'GET', + url: mockUrl, + }).should.eventually.be.rejectedWith(err).and.have.property('code', 'app/network-error'); + }); - return httpRequestHandler.sendRequest(mockHost, mockPort, mockPath, 'GET') - .should.eventually.be.rejected.and.deep.equal({ - error: mockErrorResponse, - statusCode: 400, - }); + it('should timeout when the response is repeatedly delayed', () => { + const respData = { foo: 'bar' }; + const scope = nock('https://' + mockHost) + .get(mockPath) + .times(5) + .delay(1000) + .reply(200, respData, { + 'content-type': 'application/json', }); + mockedRequests.push(scope); - it('should be rejected given a 5xx response', () => { - mockedRequests.push(mockRequestWithHttpError(500)); + const err = 'Error while making request: timeout of 50ms exceeded.'; + const client = new HttpClient(testRetryConfig()); - return httpRequestHandler.sendRequest(mockHost, mockPort, mockPath, 'GET') - .should.eventually.be.rejected.and.deep.equal({ - error: mockErrorResponse, - statusCode: 500, - }); + return client.send({ + method: 'GET', + url: mockUrl, + timeout: 50, + }).should.eventually.be.rejectedWith(err).and.have.property('code', 'app/network-timeout'); + }); + + it('should timeout when multiple socket timeouts encountered', () => { + const respData = { foo: 'bar timeout' }; + const scope = nock('https://' + mockHost) + .get(mockPath) + .times(5) + .delayConnection(2000) + .reply(200, respData, { + 'content-type': 'application/json', }); + mockedRequests.push(scope); - it('should be rejected given an error when parsing the JSON response', () => { - mockedRequests.push(mockRequestWithHttpError(400, undefined, mockTextErrorResponse)); + const err = 'Error while making request: timeout of 50ms exceeded.'; + const client = new HttpClient(testRetryConfig()); + + return client.send({ + method: 'GET', + url: mockUrl, + timeout: 50, + }).should.eventually.be.rejectedWith(err).and.have.property('code', 'app/network-timeout'); + }); + + 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' })); + } + + const client = new HttpClient(testRetryConfig()); + const err = 'Error while making request: connection reset 5'; + + return client.send({ + method: 'GET', + url: mockUrl, + timeout: 50, + }).should.eventually.be.rejectedWith(err).and.have.property('code', 'app/network-error'); + }); + + it('should be rejected, after 4 retries, on multiple 503 errors', () => { + const scope = nock('https://' + mockHost) + .get(mockPath) + .times(5) + .reply(503, {}, { + 'content-type': 'application/json', + }); + mockedRequests.push(scope); + + const client = new HttpClient(testRetryConfig()); + + return client.send({ + method: 'GET', + url: mockUrl, + }).catch((err: HttpError) => { + expect(err.message).to.equal('Server responded with status 503.'); + const resp = err.response; + expect(resp.status).to.equal(503); + expect(resp.headers['content-type']).to.equal('application/json'); + expect(resp.data).to.deep.equal({}); + expect(resp.isJson()).to.be.true; + }); + }); - return httpRequestHandler.sendRequest(mockHost, mockPort, mockPath, 'GET') - .then(() => { - throw new Error('Unexpected success.'); - }) - .catch((response) => { - expect(response).to.have.keys(['error', 'statusCode']); - expect(response.error).to.have.property('code', 'app/unable-to-parse-response'); - expect(response.statusCode).to.equal(400); - }); + it('should succeed, after 1 retry, on a single network error', () => { + mockedRequests.push(mockRequestWithError({ message: 'connection reset 1', code: 'ECONNRESET' })); + const respData = { foo: 'bar' }; + const scope = nock('https://' + mockHost) + .get(mockPath) + .reply(200, respData, { + 'content-type': 'application/json', }); + mockedRequests.push(scope); + const client = new HttpClient(defaultRetryConfig()); + return client.send({ + method: 'GET', + url: mockUrl, + }).then((resp) => { + expect(resp.status).to.equal(200); + expect(resp.data).to.deep.equal(respData); + }); + }); - it('should be fulfilled given a 2xx response', () => { - mockedRequests.push(mockRequest()); + it('should not retry when RetryConfig is explicitly null', () => { + 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({ + method: 'GET', + url: mockUrl, + }).should.eventually.be.rejectedWith(err).and.have.property('code', 'app/network-error'); + }); + + it('should not retry when maxRetries is set to 0', () => { + mockedRequests.push(mockRequestWithError({ message: 'connection reset 1', code: 'ECONNRESET' })); + const client = new HttpClient({ + maxRetries: 0, + ioErrorCodes: ['ECONNRESET'], + maxDelayInMillis: 10000, + }); + const err = 'Error while making request: connection reset 1'; + return client.send({ + method: 'GET', + url: mockUrl, + }).should.eventually.be.rejectedWith(err).and.have.property('code', 'app/network-error'); + }); + + it('should not retry when error codes are not configured', () => { + mockedRequests.push(mockRequestWithError({ message: 'connection reset 1', code: 'ECONNRESET' })); + const client = new HttpClient({ + maxRetries: 1, + maxDelayInMillis: 10000, + }); + const err = 'Error while making request: connection reset 1'; + return client.send({ + method: 'GET', + url: mockUrl, + }).should.eventually.be.rejectedWith(err).and.have.property('code', 'app/network-error'); + }); + + 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' }; + const scope = nock('https://' + mockHost) + .get(mockPath) + .reply(200, respData, { + 'content-type': 'application/json', + }); + mockedRequests.push(scope); + const client = new HttpClient({ + maxRetries: 1, + maxDelayInMillis: 1000, + ioErrorCodes: ['ETESTCODE'], + }); + return client.send({ + method: 'GET', + url: mockUrl, + }).then((resp) => { + expect(resp.status).to.equal(200); + expect(resp.data).to.deep.equal(respData); + }); + }); - return httpRequestHandler.sendRequest(mockHost, mockPort, mockPath, 'GET') - .should.eventually.be.fulfilled.and.deep.equal(mockSuccessResponse); + it('should succeed after a retry on a configured HTTP error', () => { + const scope1 = nock('https://' + mockHost) + .get(mockPath) + .reply(503, {}, { + 'content-type': 'application/json', + }); + mockedRequests.push(scope1); + const respData = { foo: 'bar' }; + const scope2 = nock('https://' + mockHost) + .get(mockPath) + .reply(200, respData, { + 'content-type': 'application/json', }); + mockedRequests.push(scope2); + const client = new HttpClient(testRetryConfig()); + return client.send({ + method: 'GET', + url: mockUrl, + }).then((resp) => { + expect(resp.status).to.equal(200); + expect(resp.data).to.deep.equal(respData); + }); + }); - it('should accept additional parameters', () => { - mockedRequests.push(mockRequest(undefined, 'POST')); + 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' })); + + // followed by 3 HTTP errors + const scope = nock('https://' + mockHost) + .get(mockPath) + .times(3) + .reply(503, {}, { + 'content-type': 'application/json', + }); + mockedRequests.push(scope); + + const client = new HttpClient(testRetryConfig()); + + return client.send({ + method: 'GET', + url: mockUrl, + }).catch((err: HttpError) => { + expect(err.message).to.equal('Server responded with status 503.'); + const resp = err.response; + expect(resp.status).to.equal(503); + expect(resp.headers['content-type']).to.equal('application/json'); + expect(resp.data).to.deep.equal({}); + expect(resp.isJson()).to.be.true; + }); + }); - return httpRequestHandler.sendRequest( - mockHost, mockPort, mockPath, 'POST', mockRequestData, mockRequestHeaders, 10000, - ).should.eventually.be.fulfilled.and.deep.equal(mockSuccessResponse); + it('should not retry when retry-after exceeds maxDelayInMillis', () => { + const scope = nock('https://' + mockHost) + .get(mockPath) + .reply(503, {}, { + 'content-type': 'application/json', + 'retry-after': '61', }); + mockedRequests.push(scope); + const client = new HttpClient({ + maxRetries: 1, + maxDelayInMillis: 60 * 1000, + statusCodes: [503], + }); + return client.send({ + method: 'GET', + url: mockUrl, + }).catch((err: HttpError) => { + expect(err.message).to.equal('Server responded with status 503.'); + const resp = err.response; + expect(resp.status).to.equal(503); + expect(resp.headers['content-type']).to.equal('application/json'); + expect(resp.data).to.deep.equal({}); + expect(resp.isJson()).to.be.true; }); + }); - describe('with text response', () => { - it('should be rejected given a 4xx response', () => { - mockedRequests.push(mockRequestWithHttpError(400, 'text/html')); + it('should retry with exponential back off', () => { + const scope = nock('https://' + mockHost) + .get(mockPath) + .times(5) + .reply(503, {}, { + 'content-type': 'application/json', + }); + mockedRequests.push(scope); + const client = new HttpClient(defaultRetryConfig()); + delayStub = sinon.stub(client as any, 'waitForRetry').resolves(); + + return client.send({ + method: 'GET', + url: mockUrl, + }).catch((err: HttpError) => { + expect(err.message).to.equal('Server responded with status 503.'); + const resp = err.response; + expect(resp.status).to.equal(503); + expect(resp.headers['content-type']).to.equal('application/json'); + expect(resp.data).to.deep.equal({}); + expect(resp.isJson()).to.be.true; + expect(delayStub!.callCount).to.equal(4); + const delays = delayStub!.args.map((args) => args[0]); + expect(delays).to.deep.equal([0, 1000, 2000, 4000]); + }); + }); - return httpRequestHandler.sendRequest(mockHost, mockPort, mockPath, 'GET') - .should.eventually.be.rejected.and.deep.equal({ - error: mockTextErrorResponse, - statusCode: 400, - }); + it('delay should not exceed maxDelayInMillis', () => { + const scope = nock('https://' + mockHost) + .get(mockPath) + .times(5) + .reply(503, {}, { + 'content-type': 'application/json', }); + mockedRequests.push(scope); + const client = new HttpClient({ + maxRetries: 4, + backOffFactor: 1, + maxDelayInMillis: 4 * 1000, + statusCodes: [503], + }); + delayStub = sinon.stub(client as any, 'waitForRetry').resolves(); + return client.send({ + method: 'GET', + url: mockUrl, + }).catch((err: HttpError) => { + expect(err.message).to.equal('Server responded with status 503.'); + const resp = err.response; + expect(resp.status).to.equal(503); + expect(resp.headers['content-type']).to.equal('application/json'); + expect(resp.data).to.deep.equal({}); + expect(resp.isJson()).to.be.true; + expect(delayStub!.callCount).to.equal(4); + const delays = delayStub!.args.map((args) => args[0]); + expect(delays).to.deep.equal([0, 2000, 4000, 4000]); + }); + }); - it('should be rejected given a 5xx response', () => { - mockedRequests.push(mockRequestWithHttpError(500, 'text/html')); + it('should retry without delays when backOffFactor is not set', () => { + const scope = nock('https://' + mockHost) + .get(mockPath) + .times(5) + .reply(503, {}, { + 'content-type': 'application/json', + }); + mockedRequests.push(scope); + const client = new HttpClient({ + maxRetries: 4, + maxDelayInMillis: 60 * 1000, + statusCodes: [503], + }); + delayStub = sinon.stub(client as any, 'waitForRetry').resolves(); + return client.send({ + method: 'GET', + url: mockUrl, + }).catch((err: HttpError) => { + expect(err.message).to.equal('Server responded with status 503.'); + const resp = err.response; + expect(resp.status).to.equal(503); + expect(resp.headers['content-type']).to.equal('application/json'); + expect(resp.data).to.deep.equal({}); + expect(resp.isJson()).to.be.true; + expect(delayStub!.callCount).to.equal(4); + const delays = delayStub!.args.map((args) => args[0]); + expect(delays).to.deep.equal([0, 0, 0, 0]); + }); + }); - return httpRequestHandler.sendRequest(mockHost, mockPort, mockPath, 'GET') - .should.eventually.be.rejected.and.deep.equal({ - error: mockTextErrorResponse, - statusCode: 500, - }); + it('should wait when retry-after expressed as seconds', () => { + const scope1 = nock('https://' + mockHost) + .get(mockPath) + .reply(503, {}, { + 'content-type': 'application/json', + 'retry-after': '30', }); + mockedRequests.push(scope1); + const respData = { foo: 'bar' }; + const scope2 = nock('https://' + mockHost) + .get(mockPath) + .reply(200, respData, { + 'content-type': 'application/json', + }); + mockedRequests.push(scope2); + + const client = new HttpClient(defaultRetryConfig()); + delayStub = sinon.stub(client as any, 'waitForRetry').resolves(); + + return client.send({ + method: 'GET', + url: mockUrl, + }).then((resp: HttpResponse) => { + expect(resp.status).to.equal(200); + expect(resp.headers['content-type']).to.equal('application/json'); + expect(resp.data).to.deep.equal(respData); + expect(resp.isJson()).to.be.true; + expect(delayStub!.callCount).to.equal(1); + expect(delayStub!.args[0][0]).to.equal(30 * 1000); + }); + }); - it('should be fulfilled given a 2xx response', () => { - mockedRequests.push(mockRequest('text/html')); + it('should wait when retry-after expressed as a timestamp', () => { + clock = sinon.useFakeTimers(); + clock.setSystemTime(1000); + const timestamp = new Date(clock.now + 30 * 1000); - return httpRequestHandler.sendRequest(mockHost, mockPort, mockPath, 'GET') - .should.eventually.be.fulfilled.and.deep.equal(mockTextSuccessResponse); + const scope1 = nock('https://' + mockHost) + .get(mockPath) + .reply(503, {}, { + 'content-type': 'application/json', + 'retry-after': timestamp.toUTCString(), + }); + mockedRequests.push(scope1); + const respData = { foo: 'bar' }; + const scope2 = nock('https://' + mockHost) + .get(mockPath) + .reply(200, respData, { + 'content-type': 'application/json', }); + mockedRequests.push(scope2); + + const client = new HttpClient(defaultRetryConfig()); + delayStub = sinon.stub(client as any, 'waitForRetry').resolves(); + + return client.send({ + method: 'GET', + url: mockUrl, + }).then((resp: HttpResponse) => { + expect(resp.status).to.equal(200); + expect(resp.headers['content-type']).to.equal('application/json'); + expect(resp.data).to.deep.equal(respData); + expect(resp.isJson()).to.be.true; + expect(delayStub!.callCount).to.equal(1); + expect(delayStub!.args[0][0]).to.equal(30 * 1000); + }); + }); - it('should accept additional parameters', () => { - mockedRequests.push(mockRequest('text/html', 'POST')); + it('should not wait when retry-after timestamp is expired', () => { + const timestamp = new Date(Date.now() - 30 * 1000); - return httpRequestHandler.sendRequest( - mockHost, mockPort, mockPath, 'POST', mockRequestData, mockRequestHeaders, 10000, - ).should.eventually.be.fulfilled.and.deep.equal(mockTextSuccessResponse); + const scope1 = nock('https://' + mockHost) + .get(mockPath) + .reply(503, {}, { + 'content-type': 'application/json', + 'retry-after': timestamp.toUTCString(), }); + mockedRequests.push(scope1); + const respData = { foo: 'bar' }; + const scope2 = nock('https://' + mockHost) + .get(mockPath) + .reply(200, respData, { + 'content-type': 'application/json', + }); + mockedRequests.push(scope2); + + const client = new HttpClient(defaultRetryConfig()); + delayStub = sinon.stub(client as any, 'waitForRetry').resolves(); + + return client.send({ + method: 'GET', + url: mockUrl, + }).then((resp: HttpResponse) => { + expect(resp.status).to.equal(200); + expect(resp.headers['content-type']).to.equal('application/json'); + expect(resp.data).to.deep.equal(respData); + expect(resp.isJson()).to.be.true; + expect(delayStub!.callCount).to.equal(1); + expect(delayStub!.args[0][0]).to.equal(0); }); }); -}); + it('should not wait when retry-after is malformed', () => { + const scope1 = nock('https://' + mockHost) + .get(mockPath) + .reply(503, {}, { + 'content-type': 'application/json', + 'retry-after': 'invalid', + }); + mockedRequests.push(scope1); + const respData = { foo: 'bar' }; + const scope2 = nock('https://' + mockHost) + .get(mockPath) + .reply(200, respData, { + 'content-type': 'application/json', + }); + mockedRequests.push(scope2); + + const client = new HttpClient(defaultRetryConfig()); + delayStub = sinon.stub(client as any, 'waitForRetry').resolves(); + + return client.send({ + method: 'GET', + url: mockUrl, + }).then((resp: HttpResponse) => { + expect(resp.status).to.equal(200); + expect(resp.headers['content-type']).to.equal('application/json'); + expect(resp.data).to.deep.equal(respData); + expect(resp.isJson()).to.be.true; + expect(delayStub!.callCount).to.equal(1); + expect(delayStub!.args[0][0]).to.equal(0); + }); + }); + + it('should reject if the request payload is invalid', () => { + const client = new HttpClient(defaultRetryConfig()); + const err = 'Error while making request: Request data must be a string, a Buffer ' + + 'or a json serializable object'; + return client.send({ + method: 'POST', + url: mockUrl, + data: 1 as any, + }).should.eventually.be.rejectedWith(err).and.have.property('code', 'app/network-error'); + }); + + it('should use the port 80 for http URLs', () => { + const respData = { foo: 'bar' }; + const scope = nock('http://' + mockHost + ':80') + .get('/') + .reply(200, respData, { + 'content-type': 'application/json', + }); + mockedRequests.push(scope); + const client = new HttpClient(defaultRetryConfig()); + return client.send({ + method: 'GET', + url: 'http://' + mockHost, + }).then((resp) => { + expect(resp.status).to.equal(200); + }); + }); -describe('SignedApiRequestHandler', () => { + it('should use the port specified in the URL', () => { + const respData = { foo: 'bar' }; + const scope = nock('https://' + mockHost + ':8080') + .get('/') + .reply(200, respData, { + 'content-type': 'application/json', + }); + mockedRequests.push(scope); + const client = new HttpClient(defaultRetryConfig()); + return client.send({ + method: 'GET', + url: 'https://' + mockHost + ':8080', + }).then((resp) => { + expect(resp.status).to.equal(200); + }); + }); +}); + +describe('AuthorizedHttpClient', () => { let mockApp: FirebaseApp; - const mockAccessToken: string = utils.generateRandomAccessToken(); + let mockedRequests: nock.Scope[] = []; + let getTokenStub: sinon.SinonStub; - before(() => utils.mockFetchAccessTokenRequests(mockAccessToken)); + const mockAccessToken: string = utils.generateRandomAccessToken(); + const requestHeaders = { + reqheaders: { + Authorization: `Bearer ${mockAccessToken}`, + }, + }; + + before(() => { + getTokenStub = utils.stubGetAccessToken(mockAccessToken); + }); - after(() => nock.cleanAll()); + after(() => { + getTokenStub.restore(); + }); beforeEach(() => { mockApp = mocks.app(); }); afterEach(() => { + mockedRequests.forEach((mockedRequest) => mockedRequest.done()); + mockedRequests = []; return mockApp.delete(); }); - describe('Constructor', () => { - it('should succeed with a FirebaseApp instance', () => { - expect(() => { - const authRequestHandlerAny: any = SignedApiRequestHandler; - return new authRequestHandlerAny(mockApp); - }).not.to.throw(Error); + it('should be fulfilled for a 2xx response with a json payload', () => { + const respData = { foo: 'bar' }; + const scope = nock('https://' + mockHost, requestHeaders) + .get(mockPath) + .reply(200, respData, { + 'content-type': 'application/json', + }); + mockedRequests.push(scope); + const client = new AuthorizedHttpClient(mockApp); + return client.send({ + method: 'GET', + url: mockUrl, + }).then((resp) => { + expect(resp.status).to.equal(200); + expect(resp.headers['content-type']).to.equal('application/json'); + expect(resp.text).to.equal(JSON.stringify(respData)); + expect(resp.data).to.deep.equal(respData); }); }); - describe('sendRequest', () => { - let mockedRequests: nock.Scope[] = []; + describe('HTTP Agent', () => { + let transportSpy: sinon.SinonSpy | null = null; + let mockAppWithAgent: FirebaseApp; + let agentForApp: Agent; + + beforeEach(() => { + const options = mockApp.options; + options.httpAgent = new Agent(); + + // eslint-disable-next-line @typescript-eslint/no-var-requires + const https = require('https'); + transportSpy = sinon.spy(https, 'request'); + mockAppWithAgent = mocks.appWithOptions(options); + agentForApp = options.httpAgent; + }); + afterEach(() => { - _.forEach(mockedRequests, (mockedRequest) => mockedRequest.done()); - mockedRequests = []; + transportSpy!.restore(); + transportSpy = null; + return mockAppWithAgent.delete(); }); - const expectedResult = { - users : [ - {localId: 'uid'}, - ], - }; - let stub: sinon.SinonStub; - beforeEach(() => stub = sinon.stub(HttpRequestHandler.prototype, 'sendRequest') - .returns(Promise.resolve(expectedResult))); - afterEach(() => stub.restore()); - const data = {localId: ['uid']}; - const preHeaders = { - 'Content-Type': 'application/json', - }; - const headers = { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer ' + mockAccessToken, - }; - const httpMethod: any = 'POST'; - const host = 'www.googleapis.com'; - const port = 443; - const path = '/identitytoolkit/v3/relyingparty/getAccountInfo'; - const timeout = 10000; - it('should resolve successfully with a valid request', () => { - const requestHandler = new SignedApiRequestHandler(mockApp); - return requestHandler.sendRequest( - host, port, path, httpMethod, data, preHeaders, timeout) - .then((result) => { - expect(result).to.deep.equal(expectedResult); - expect(stub).to.have.been.calledOnce.and.calledWith( - host, port, path, httpMethod, data, headers, timeout); + it('should use the HTTP agent set in request', () => { + const respData = { success: true }; + const scope = nock('https://' + mockHost, requestHeaders) + .get(mockPath) + .reply(200, respData, { + 'content-type': 'application/json', }); + mockedRequests.push(scope); + const client = new AuthorizedHttpClient(mockAppWithAgent); + const httpAgent = new Agent(); + return client.send({ + method: 'GET', + url: mockUrl, + httpAgent, + }).then((resp) => { + expect(resp.status).to.equal(200); + expect(transportSpy!.callCount).to.equal(1); + const options = transportSpy!.args[0][0]; + expect(options.agent).to.equal(httpAgent); + }); + }); + + it('should use the HTTP agent set in AppOptions', () => { + const respData = { success: true }; + const scope = nock('https://' + mockHost, requestHeaders) + .get(mockPath) + .reply(200, respData, { + 'content-type': 'application/json', + }); + mockedRequests.push(scope); + const client = new AuthorizedHttpClient(mockAppWithAgent); + return client.send({ + method: 'GET', + url: mockUrl, + }).then((resp) => { + expect(resp.status).to.equal(200); + expect(transportSpy!.callCount).to.equal(1); + const options = transportSpy!.args[0][0]; + expect(options.agent).to.equal(agentForApp); + }); }); }); - describe('sendDeleteRequest', () => { - let mockedRequests: nock.Scope[] = []; - afterEach(() => { - _.forEach(mockedRequests, (mockedRequest) => mockedRequest.done()); - mockedRequests = []; + it('should make a POST request with the provided headers and data', () => { + const reqData = { request: 'data' }; + const respData = { success: true }; + const options = { + reqheaders: { + 'Content-Type': (header: string) => { + return header.startsWith('application/json'); // auto-inserted + }, + 'My-Custom-Header': 'CustomValue', + }, + }; + Object.assign(options.reqheaders, requestHeaders.reqheaders); + const scope = nock('https://' + mockHost, options) + .post(mockPath, reqData) + .reply(200, respData, { + 'content-type': 'application/json', + }); + mockedRequests.push(scope); + const client = new AuthorizedHttpClient(mockApp); + return client.send({ + method: 'POST', + url: mockUrl, + headers: { + 'My-Custom-Header': 'CustomValue', + }, + data: reqData, + }).then((resp) => { + expect(resp.status).to.equal(200); + expect(resp.headers['content-type']).to.equal('application/json'); + expect(resp.data).to.deep.equal(respData); }); + }); - const expectedResult = { - users : [ - {localId: 'uid'}, - ], + it('should not mutate the arguments', () => { + const reqData = { request: 'data' }; + const options = { + reqheaders: { + 'Content-Type': (header: string) => { + return header.startsWith('application/json'); // auto-inserted + }, + 'My-Custom-Header': 'CustomValue', + }, }; - let stub: sinon.SinonStub; - beforeEach(() => stub = sinon.stub(HttpRequestHandler.prototype, 'sendRequest') - .returns(Promise.resolve(expectedResult))); - afterEach(() => stub.restore()); - const headers = { - Authorization: 'Bearer ' + mockAccessToken, + Object.assign(options.reqheaders, requestHeaders.reqheaders); + const scope = nock('https://' + mockHost, options) + .post(mockPath, reqData) + .reply(200, { success: true }, { + 'content-type': 'application/json', + }); + mockedRequests.push(scope); + const client = new AuthorizedHttpClient(mockApp); + const request: HttpRequestConfig = { + method: 'POST', + url: mockUrl, + headers: { + 'My-Custom-Header': 'CustomValue', + }, + data: reqData, }; - const httpMethod: any = 'DELETE'; - const host = 'www.googleapis.com'; - const port = 443; - const path = '/identitytoolkit/v3/relyingparty/getAccountInfo'; - const timeout = 10000; - it('should resolve successfully with a valid request', () => { - const requestHandler = new SignedApiRequestHandler(mockApp); - return requestHandler.sendRequest( - host, port, path, httpMethod, undefined, undefined, timeout) - .then((result) => { - expect(result).to.deep.equal(expectedResult); - expect(stub).to.have.been.calledOnce.and.calledWith( - host, port, path, httpMethod, undefined, headers, timeout); - }); + const requestCopy = deepCopy(request); + return client.send(request).then((resp) => { + expect(resp.status).to.equal(200); + expect(request).to.deep.equal(requestCopy); }); }); }); @@ -491,8 +1431,8 @@ describe('ApiSettings', () => { describe('with set properties', () => { const apiSettings: ApiSettings = new ApiSettings('getAccountInfo', 'GET'); // Set all apiSettings properties. - const requestValidator = (request) => undefined; - const responseValidator = (response) => undefined; + const requestValidator: ApiCallbackFunction = () => undefined; + const responseValidator: ApiCallbackFunction = () => undefined; apiSettings.setRequestValidator(requestValidator); apiSettings.setResponseValidator(responseValidator); it('should return the correct requestValidator', () => { @@ -505,4 +1445,119 @@ describe('ApiSettings', () => { }); }); +describe('parseHttpResponse()', () => { + const config: HttpRequestConfig = { + method: 'GET', + url: 'https://example.com', + }; + + it('should parse a successful response with json content', () => { + const text = 'HTTP/1.1 200 OK\r\n' + + 'Content-type: application/json\r\n' + + 'Date: Thu, 07 Feb 2019 19:20:34 GMT\r\n' + + '\r\n' + + '{"foo": 1}'; + + const response = parseHttpResponse(text, config); + + expect(response.status).to.equal(200); + expect(Object.keys(response.headers).length).to.equal(2); + 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.text).to.equal('{"foo": 1}'); + }); + + it('should parse an error response with json content', () => { + const text = 'HTTP/1.1 400 Bad Request\r\n' + + 'Content-type: application/json\r\n' + + 'Date: Thu, 07 Feb 2019 19:20:34 GMT\r\n' + + '\r\n' + + '{"foo": 1}'; + + const response = parseHttpResponse(text, config); + + expect(response.status).to.equal(400); + expect(Object.keys(response.headers).length).to.equal(2); + 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.text).to.equal('{"foo": 1}'); + }); + + it('should parse a response with text content', () => { + const text = 'HTTP/1.1 200 OK\r\n' + + 'Content-type: text/plain\r\n' + + 'Date: Thu, 07 Feb 2019 19:20:34 GMT\r\n' + + '\r\n' + + 'foo bar'; + + const response = parseHttpResponse(text, config); + + expect(response.status).to.equal(200); + expect(Object.keys(response.headers).length).to.equal(2); + expect(response.headers).to.have.property('content-type', 'text/plain'); + expect(response.headers).to.have.property('date', 'Thu, 07 Feb 2019 19:20:34 GMT'); + expect(response.isJson()).to.be.false; + expect(response.text).to.equal('foo bar'); + }); + it('should parse given a buffer', () => { + const text = 'HTTP/1.1 200 OK\r\n' + + 'Content-type: text/plain\r\n' + + 'Date: Thu, 07 Feb 2019 19:20:34 GMT\r\n' + + '\r\n' + + 'foo bar'; + + const response = parseHttpResponse(Buffer.from(text), config); + + expect(response.status).to.equal(200); + expect(Object.keys(response.headers).length).to.equal(2); + expect(response.headers).to.have.property('content-type', 'text/plain'); + expect(response.headers).to.have.property('date', 'Thu, 07 Feb 2019 19:20:34 GMT'); + expect(response.isJson()).to.be.false; + expect(response.text).to.equal('foo bar'); + }); + + it('should remove any trailing white space in the payload', () => { + const text = 'HTTP/1.1 200 OK\r\n' + + 'Content-type: text/plain\r\n' + + 'Date: Thu, 07 Feb 2019 19:20:34 GMT\r\n' + + '\r\n' + + 'foo bar\r\n'; + + const response = parseHttpResponse(text, config); + + expect(response.isJson()).to.be.false; + expect(response.text).to.equal('foo bar'); + }); + + it('should throw when the header is malformed', () => { + const text = 'malformed http header\r\n' + + 'Content-type: application/json\r\n' + + 'Date: Thu, 07 Feb 2019 19:20:34 GMT\r\n' + + '\r\n' + + '{"foo": 1}'; + + expect(() => parseHttpResponse(text, config)).to.throw('Malformed HTTP status line.'); + }); +}); + +describe('defaultRetryConfig()', () => { + it('should return a RetryConfig with default settings', () => { + const config = defaultRetryConfig(); + expect(config.maxRetries).to.equal(4); + expect(config.ioErrorCodes).to.deep.equal(['ECONNRESET', 'ETIMEDOUT']); + expect(config.statusCodes).to.deep.equal([503]); + expect(config.maxDelayInMillis).to.equal(60000); + expect(config.backOffFactor).to.equal(0.5); + }); + + it('should return a new instance on each invocation', () => { + const config1 = defaultRetryConfig(); + const config2 = defaultRetryConfig(); + expect(config1).to.not.equal(config2); + }); +}); diff --git a/test/unit/utils/crypto-signer.spec.ts b/test/unit/utils/crypto-signer.spec.ts new file mode 100644 index 0000000000..efda058c02 --- /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/app/credential-internal'; +import { AuthorizedHttpClient, HttpClient } from '../../../src/utils/api-request'; +import { FirebaseApp } from '../../../src/app/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/error.spec.ts b/test/unit/utils/error.spec.ts index fadc3207f7..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"); @@ -33,7 +34,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 +50,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 }); }); }); @@ -80,7 +81,7 @@ describe('FirebaseAuthError', () => { const error = FirebaseAuthError.fromServerError('USER_NOT_FOUND'); expect(error.code).to.be.equal('auth/user-not-found'); expect(error.message).to.be.equal( - 'There is no user record corresponding to the provided identifier.'); + 'There is no user record corresponding to the provided identifier.'); }); it('should initialize an error from an unexpected server code', () => { @@ -88,22 +89,37 @@ describe('FirebaseAuthError', () => { expect(error.code).to.be.equal('auth/internal-error'); expect(error.message).to.be.equal('An internal error has occurred.'); }); + + it('should initialize an error from an expected server with server detailed message', () => { + // Error code should be separated from detailed message at first colon. + const error = FirebaseAuthError.fromServerError('CONFIGURATION_NOT_FOUND : more details key: value'); + expect(error.code).to.be.equal('auth/configuration-not-found'); + expect(error.message).to.be.equal('more details key: value'); + }); }); describe('with message specified', () => { it('should initialize an error from an expected server code', () => { const error = FirebaseAuthError.fromServerError( - 'USER_NOT_FOUND', 'Invalid uid'); + 'USER_NOT_FOUND', 'Invalid uid'); expect(error.code).to.be.equal('auth/user-not-found'); expect(error.message).to.be.equal('Invalid uid'); }); it('should initialize an error from an unexpected server code', () => { const error = FirebaseAuthError.fromServerError( - 'UNEXPECTED_ERROR', 'An unexpected error occurred.'); + 'UNEXPECTED_ERROR', 'An unexpected error occurred.'); expect(error.code).to.be.equal('auth/internal-error'); expect(error.message).to.be.equal('An unexpected error occurred.'); }); + + it('should initialize an error from an expected server with server detailed message', () => { + const error = FirebaseAuthError.fromServerError( + 'CONFIGURATION_NOT_FOUND : more details', + 'Ignored message'); + expect(error.code).to.be.equal('auth/configuration-not-found'); + expect(error.message).to.be.equal('more details'); + }); }); describe('with raw server response specified', () => { @@ -116,14 +132,14 @@ describe('FirebaseAuthError', () => { it('should not include raw server response from an expected server code', () => { const error = FirebaseAuthError.fromServerError( - 'USER_NOT_FOUND', 'Invalid uid', mockRawServerResponse); + 'USER_NOT_FOUND', 'Invalid uid', mockRawServerResponse); expect(error.code).to.be.equal('auth/user-not-found'); expect(error.message).to.be.equal('Invalid uid'); }); it('should include raw server response from an unexpected server code', () => { const error = FirebaseAuthError.fromServerError( - 'UNEXPECTED_ERROR', 'An unexpected error occurred.', mockRawServerResponse); + 'UNEXPECTED_ERROR', 'An unexpected error occurred.', mockRawServerResponse); expect(error.code).to.be.equal('auth/internal-error'); expect(error.message).to.be.equal( 'An unexpected error occurred. Raw server response: "' + diff --git a/test/unit/utils/index.spec.ts b/test/unit/utils/index.spec.ts index 678e667227..7105726e67 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"); @@ -14,17 +15,34 @@ * limitations under the License. */ -import {expect} from 'chai'; +import * as _ from 'lodash'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; import * as mocks from '../../resources/mocks'; -import {addReadonlyGetter, getProjectId} from '../../../src/utils/index'; -import {isNonEmptyString} from '../../../src/utils/validator'; -import {FirebaseApp, FirebaseAppOptions} from '../../../src/firebase-app'; +import { + addReadonlyGetter, getExplicitProjectId, findProjectId, + toWebSafeBase64, formatString, generateUpdateMask, transformMillisecondsToSecondsString, parseResourceName, +} from '../../../src/utils/index'; +import { isNonEmptyString } from '../../../src/utils/validator'; +import { FirebaseApp } from '../../../src/app/firebase-app'; +import { ComputeEngineCredential } from '../../../src/app/credential-internal'; +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 = {}; @@ -39,7 +57,7 @@ describe('addReadonlyGetter()', () => { expect(() => { obj.foo = false; - }).to.throw(/Cannot assign to read only property \'foo\' of/); + }).to.throw(/Cannot assign to read only property 'foo' of/); }); it('should make the new property enumerable', () => { @@ -50,14 +68,35 @@ describe('addReadonlyGetter()', () => { }); }); -describe('getProjectId()', () => { - let gcloudProject: string; +describe('toWebSafeBase64()', () => { + it('should convert a byte buffer to a web-safe base64 encoded string', () => { + const inputBuffer = Buffer.from('hello'); + expect(toWebSafeBase64(inputBuffer)).to.equal(inputBuffer.toString('base64')); + }); + + it('should convert to web safe base64 encoded with plus signs and slashes replaced', () => { + // This converts to base64 encoded string: b++/vQ== + const inputBuffer = Buffer.from('o�'); + expect(toWebSafeBase64(inputBuffer)).to.equal('b--_vQ=='); + }); +}); + +describe('getExplicitProjectId()', () => { + let googleCloudProject: string | undefined; + let gcloudProject: string | undefined; before(() => { + googleCloudProject = process.env.GOOGLE_CLOUD_PROJECT; gcloudProject = process.env.GCLOUD_PROJECT; }); after(() => { + if (isNonEmptyString(googleCloudProject)) { + process.env.GOOGLE_CLOUD_PROJECT = googleCloudProject; + } else { + delete process.env.GOOGLE_CLOUD_PROJECT; + } + if (isNonEmptyString(gcloudProject)) { process.env.GCLOUD_PROJECT = gcloudProject; } else { @@ -66,28 +105,318 @@ describe('getProjectId()', () => { }); it('should return the explicitly specified project ID from app options', () => { - const options: FirebaseAppOptions = { + const options = { credential: new mocks.MockCredential(), projectId: 'explicit-project-id', }; const app: FirebaseApp = mocks.appWithOptions(options); - expect(getProjectId(app)).to.equal(options.projectId); + expect(getExplicitProjectId(app)).to.equal(options.projectId); }); it('should return the project ID from service account', () => { const app: FirebaseApp = mocks.app(); - expect(getProjectId(app)).to.equal('project_id'); + expect(getExplicitProjectId(app)).to.equal('project_id'); + }); + + it('should return the project ID set in GOOGLE_CLOUD_PROJECT environment variable', () => { + process.env.GOOGLE_CLOUD_PROJECT = 'env-var-project-id'; + const app: FirebaseApp = mocks.mockCredentialApp(); + expect(getExplicitProjectId(app)).to.equal('env-var-project-id'); }); - it('should return the project ID set in environment', () => { + it('should return the project ID set in GCLOUD_PROJECT environment variable', () => { process.env.GCLOUD_PROJECT = 'env-var-project-id'; const app: FirebaseApp = mocks.mockCredentialApp(); - expect(getProjectId(app)).to.equal('env-var-project-id'); + expect(getExplicitProjectId(app)).to.equal('env-var-project-id'); }); it('should return null when project ID is not set', () => { + delete process.env.GOOGLE_CLOUD_PROJECT; delete process.env.GCLOUD_PROJECT; const app: FirebaseApp = mocks.mockCredentialApp(); - expect(getProjectId(app)).to.be.null; + expect(getExplicitProjectId(app)).to.be.null; + }); +}); + +describe('findProjectId()', () => { + let googleCloudProject: string | undefined; + let gcloudProject: string | undefined; + let httpStub: sinon.SinonStub; + + before(() => { + googleCloudProject = process.env.GOOGLE_CLOUD_PROJECT; + gcloudProject = process.env.GCLOUD_PROJECT; + }); + + after(() => { + if (isNonEmptyString(googleCloudProject)) { + process.env.GOOGLE_CLOUD_PROJECT = googleCloudProject; + } else { + delete process.env.GOOGLE_CLOUD_PROJECT; + } + + if (isNonEmptyString(gcloudProject)) { + process.env.GCLOUD_PROJECT = gcloudProject; + } else { + delete process.env.GCLOUD_PROJECT; + } + }); + + beforeEach(() => { + delete process.env.GOOGLE_CLOUD_PROJECT; + delete process.env.GCLOUD_PROJECT; + httpStub = sinon.stub(HttpClient.prototype, 'send'); + }); + + afterEach(() => { + httpStub.restore(); + }); + + it('should return the explicitly specified project ID from app options', () => { + const options = { + credential: new mocks.MockCredential(), + projectId: 'explicit-project-id', + }; + const app: FirebaseApp = mocks.appWithOptions(options); + return findProjectId(app).should.eventually.equal(options.projectId); + }); + + it('should return the project ID from service account', () => { + const app: FirebaseApp = mocks.app(); + return findProjectId(app).should.eventually.equal('project_id'); + }); + + it('should return the project ID set in GOOGLE_CLOUD_PROJECT environment variable', () => { + process.env.GOOGLE_CLOUD_PROJECT = 'env-var-project-id'; + const app: FirebaseApp = mocks.mockCredentialApp(); + return findProjectId(app).should.eventually.equal('env-var-project-id'); + }); + + it('should return the project ID set in GCLOUD_PROJECT environment variable', () => { + process.env.GCLOUD_PROJECT = 'env-var-project-id'; + const app: FirebaseApp = mocks.mockCredentialApp(); + return findProjectId(app).should.eventually.equal('env-var-project-id'); + }); + + it('should return the project ID discovered from the metadata service', () => { + const expectedProjectId = 'test-project-id'; + const response = utils.responseFrom(expectedProjectId); + httpStub.resolves(response); + const app: FirebaseApp = mocks.appWithOptions({ + credential: new ComputeEngineCredential(), + }); + return findProjectId(app).should.eventually.equal(expectedProjectId); + }); + + it('should reject when the metadata service is not available', () => { + httpStub.rejects(new FirebaseAppError('network-error', 'Failed to connect')); + const app: FirebaseApp = mocks.appWithOptions({ + credential: new ComputeEngineCredential(), + }); + return findProjectId(app).should.eventually + .rejectedWith('Failed to determine project ID: Failed to connect'); + }); + + it('should return null when project ID is not set and discoverable', () => { + const app: FirebaseApp = mocks.mockCredentialApp(); + return findProjectId(app).should.eventually.be.null; + }); +}); + +describe('findProjectId()', () => { + let googleCloudProject: string | undefined; + let gcloudProject: string | undefined; + + before(() => { + googleCloudProject = process.env.GOOGLE_CLOUD_PROJECT; + gcloudProject = process.env.GCLOUD_PROJECT; + }); + + after(() => { + if (isNonEmptyString(googleCloudProject)) { + process.env.GOOGLE_CLOUD_PROJECT = googleCloudProject; + } else { + delete process.env.GOOGLE_CLOUD_PROJECT; + } + + if (isNonEmptyString(gcloudProject)) { + process.env.GCLOUD_PROJECT = gcloudProject; + } else { + delete process.env.GCLOUD_PROJECT; + } + }); + + it('should return the explicitly specified project ID from app options', () => { + const options = { + credential: new mocks.MockCredential(), + projectId: 'explicit-project-id', + }; + const app: FirebaseApp = mocks.appWithOptions(options); + return findProjectId(app).should.eventually.equal(options.projectId); + }); + + it('should return the project ID from service account', () => { + const app: FirebaseApp = mocks.app(); + return findProjectId(app).should.eventually.equal('project_id'); + }); + + it('should return the project ID set in GOOGLE_CLOUD_PROJECT environment variable', () => { + process.env.GOOGLE_CLOUD_PROJECT = 'env-var-project-id'; + const app: FirebaseApp = mocks.mockCredentialApp(); + return findProjectId(app).should.eventually.equal('env-var-project-id'); + }); + + it('should return the project ID set in GCLOUD_PROJECT environment variable', () => { + process.env.GCLOUD_PROJECT = 'env-var-project-id'; + const app: FirebaseApp = mocks.mockCredentialApp(); + return findProjectId(app).should.eventually.equal('env-var-project-id'); + }); + + it('should return null when project ID is not set', () => { + delete process.env.GOOGLE_CLOUD_PROJECT; + delete process.env.GCLOUD_PROJECT; + const app: FirebaseApp = mocks.mockCredentialApp(); + return findProjectId(app).should.eventually.be.null; + }); +}); + +describe('formatString()', () => { + it('should keep string as is if not parameters are provided', () => { + const str = 'projects/{projectId}/{api}/path/api/projectId'; + expect(formatString(str)).to.equal(str); + }); + + it('should substitute parameters in string', () => { + const str = 'projects/{projectId}/{api}/path/api/projectId'; + const expectedOutput = 'projects/PROJECT_ID/API/path/api/projectId'; + const params = { + projectId: 'PROJECT_ID', + api: 'API', + notFound: 'NOT_FOUND', + }; + expect(formatString(str, params)).to.equal(expectedOutput); + }); + + it('should keep string as is if braces are not matching', () => { + const str = 'projects/projectId}/{api/path/api/projectId'; + const params = { + projectId: 'PROJECT_ID', + api: 'API', + }; + expect(formatString(str, params)).to.equal(str); + }); + + it('should handle multiple successive braces', () => { + const str = 'projects/{{projectId}}/path/{{api}}/projectId'; + const expectedOutput = 'projects/{PROJECT_ID}/path/{API}/projectId'; + const params = { + projectId: 'PROJECT_ID', + api: 'API', + }; + expect(formatString(str, params)).to.equal(expectedOutput); + }); + + it('should substitute multiple occurrences of the same parameter', () => { + const str = 'projects/{projectId}/{api}/path/api/{projectId}'; + const expectedOutput = 'projects/PROJECT_ID/API/path/api/PROJECT_ID'; + const params = { + projectId: 'PROJECT_ID', + api: 'API', + }; + expect(formatString(str, params)).to.equal(expectedOutput); + }); + + it('should keep string as is if parameters are not found', () => { + const str = 'projects/{projectId}/{api}/path/api/projectId'; + const params = { + notFound: 'value', + }; + expect(formatString(str, params)).to.equal(str); + }); +}); + +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)).to.deep.equal([]); + }); + }); + + it('should return empty array for empty object', () => { + expect(generateUpdateMask({})).to.deep.equal([]); + }); + + it('should return expected update mask array for nested object', () => { + const expectedMaskArray = [ + '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']); + }); +}); + + +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]); + }); + }); +}); + +describe('parseResourceName()', () => { + + const FULL_RESOURCE_NAME = 'projects/abc/locations/us/functions/f1'; + const PARTIAL_RESOURCE_NAME = 'locations/us/functions/f1'; + const projectId = 'abc'; + const locationId = 'us'; + const resourceId = 'f1'; + + it('should return projectId, location, and resource when given a full resource name', () => { + expect(parseResourceName(FULL_RESOURCE_NAME, 'functions')) + .to.deep.equal({ projectId, locationId, resourceId }); + }); + + it('should return location and resource when given a partial resource name', () => { + expect(parseResourceName(PARTIAL_RESOURCE_NAME, 'functions')) + .to.deep.equal({ projectId: undefined, locationId, resourceId }); + }); + + it('should return the resource when given only the resource name', () => { + expect(parseResourceName('f1', 'functions')) + .to.deep.equal({ resourceId }); }); }); diff --git a/test/unit/utils/jwt.spec.ts b/test/unit/utils/jwt.spec.ts new file mode 100644 index 0000000000..18cff06ff7 --- /dev/null +++ b/test/unit/utils/jwt.spec.ts @@ -0,0 +1,748 @@ +/*! + * 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 mocks from '../../resources/mocks'; +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 SIX_HOURS_IN_SECONDS = ONE_HOUR_IN_SECONDS * 6; +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. + * + * @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', + }); +} + +/** + * 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'); +} + +/** + * Returns a mocked out success JWKS response. + * + * @returns 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. + * + * @returns A nock response object. + */ +function mockFetchJsonWebKeysWithErrorResponse(): nock.Scope { + return nock('https://firebaseappcheck.googleapis.com') + .get(jwksPath) + .reply(200, { + error: 'message', + error_description: 'description', + }); +} + +/** + * Returns a mocked out failed JSON Web Keys response. + * The status code is non-200 and the response itself will fail. + * + * @returns A nock response object. + */ +function mockFailedFetchJsonWebKeys(): nock.Scope { + return nock('https://firebaseappcheck.googleapis.com') + .get(jwksPath) + .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: DecodedToken = { + header: { + alg: 'RS256', + kid: 'aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd', + typ: 'JWT', + }, + payload: TOKEN_PAYLOAD +}; + +const DECODED_UNSIGNED_TOKEN: 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 (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 decodeJwt(invalidIdToken as any) + .should.eventually.be.rejectedWith('The provided token must be a string.'); + }); + }); + + it('should reject given an empty string token', () => { + return decodeJwt('') + .should.eventually.be.rejectedWith('Decoding token failed.'); + }); + + it('should reject given an invalid token', () => { + return 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 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: {} + }, undefined, 'secret'); + + return 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 (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 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 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 verifyJwtSignature(mockIdToken, mocks.keyPairs[0].public, + { algorithms: [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: {} + }, undefined, 'secret'); + + return verifyJwtSignature(mockIdToken, undefined as any, { algorithms: ['none'] }) + .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 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 verifyJwtSignature(mockIdToken, mocks.keyPairs[0].public, + { algorithms: ['RS384'] }) + .should.eventually.be.rejectedWith('invalid algorithm') + .with.property('code', 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 verifyJwtSignature(mockIdToken, mocks.keyPairs[0].public, + { algorithms: [ALGORITHM_RS256] }) + .then(() => { + clock!.tick(1); + + // token should now be invalid + 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', 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 verifyJwtSignature(mockIdToken, getKeyCallback, + { algorithms: [ALGORITHM_RS256] }) + .should.eventually.be.rejectedWith('key fetch failed.') + .with.property('code', 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 verifyJwtSignature(mockIdToken, getKeyCallback, + { algorithms: [ALGORITHM_RS256] }) + .should.eventually.be.rejectedWith('no-matching-kid-error') + .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 verifyJwtSignature(mockIdToken, mocks.keyPairs[1].public, + { algorithms: [ALGORITHM_RS256] }) + .should.eventually.be.rejectedWith('invalid signature') + .with.property('code', JwtErrorCode.INVALID_SIGNATURE); + }); + + it('should be rejected given an invalid JWT.', () => { + return verifyJwtSignature('invalid-token', mocks.keyPairs[0].public) + .should.eventually.be.rejectedWith('jwt malformed') + .with.property('code', JwtErrorCode.INVALID_SIGNATURE); + }); +}); + +describe('PublicKeySignatureVerifier', () => { + let stubs: sinon.SinonStub[] = []; + 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 PublicKeySignatureVerifier( + new 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 PublicKeySignatureVerifier(invalidKeyFetchers as any); + }).to.throw('The provided key fetcher is not an object or null.'); + }); + }); + }); + + describe('withCertificateUrl', () => { + 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); + }); + }); + + 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 fulfilled given a valid token', () => { + const keyFetcherStub = sinon.stub(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 fulfilled 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(UrlKeyFetcher.prototype, 'fetchPublicKeys') + .resolves(VALID_PUBLIC_KEYS_RESPONSE); + stubs.push(keyFetcherStub); + const mockIdToken = mocks.generateIdToken({ + algorithm: 'RS384', + }); + + return verifier.verify(mockIdToken).should.eventually.be + .rejectedWith('invalid algorithm') + .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(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', JwtErrorCode.NO_MATCHING_KID); + }); + + it('should reject when an error occurs while fetching the keys', () => { + 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', JwtErrorCode.KEY_FETCH_ERROR); + }); + }); +}); + +describe('EmulatorSignatureVerifier', () => { + const emulatorVerifier = new EmulatorSignatureVerifier(); + + describe('verify', () => { + it('should be fulfilled given a valid unsigned (emulator) token', () => { + const mockIdToken = mocks.generateIdToken({ + algorithm: 'none', + header: {} + }, undefined, 'secret'); + + 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: UrlKeyFetcher; + let clock: sinon.SinonFakeTimers | undefined; + let httpsSpy: sinon.SinonSpy; + + beforeEach(() => { + keyFetcher = new 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 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 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 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 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)'); + }); + }); +}); + +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((SIX_HOURS_IN_SECONDS - 1) * 1000); + return keyFetcher.fetchPublicKeys(); + }).then(() => { + expect(https.request).to.have.been.calledOnce; + clock!.tick(SIX_HOURS_IN_SECONDS * 1000); // 6 hours in milliseconds + return keyFetcher.fetchPublicKeys(); + }).then(() => { + // App check keys do not contain cache headers so we cache the keys for 6 hours. + // 6 hours has passed + expect(https.request).to.have.been.calledTwice; + clock!.tick((SIX_HOURS_IN_SECONDS - 1) * 1000); + return keyFetcher.fetchPublicKeys(); + }).then(() => { + expect(https.request).to.have.been.calledTwice; + clock!.tick(SIX_HOURS_IN_SECONDS * 1000); + return keyFetcher.fetchPublicKeys(); + }).then(() => { + // 12 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'); + }); + }); +}); diff --git a/test/unit/utils/validator.spec.ts b/test/unit/utils/validator.spec.ts index 37a09cf39a..a2f86c83c2 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"); @@ -21,7 +22,8 @@ import * as chaiAsPromised from 'chai-as-promised'; import { isArray, isNonEmptyArray, isBoolean, isNumber, isString, isNonEmptyString, isNonNullObject, - isEmail, isPassword, isURL, isUid, isPhoneNumber, isObject, + isEmail, isPassword, isURL, isUid, isPhoneNumber, isObject, isBuffer, + isUTCDateString, isISODateString, } from '../../../src/utils/validator'; @@ -51,10 +53,10 @@ describe('isArray()', () => { expect(isArray(undefined as any)).to.be.false; }); - const nonBooleans = [null, NaN, 0, 1, '', 'a', true, false, {}, { a: 1 }, _.noop]; - nonBooleans.forEach((nonBoolean) => { - it('should return false given a non-array argument: ' + JSON.stringify(nonBoolean), () => { - expect(isArray(nonBoolean as any)).to.be.false; + const nonArrays = [null, NaN, 0, 1, '', 'a', true, false, {}, { a: 1 }, _.noop]; + nonArrays.forEach((nonArray) => { + it('should return false given a non-array argument: ' + JSON.stringify(nonArray), () => { + expect(isArray(nonArray as any)).to.be.false; }); }); @@ -67,10 +69,12 @@ describe('isArray()', () => { }); it('should return true given an empty array created from Array constructor', () => { + // eslint-disable-next-line @typescript-eslint/no-array-constructor expect(isArray(new Array())).to.be.true; }); it('should return true given a non-empty array created from Array constructor', () => { + // eslint-disable-next-line @typescript-eslint/no-array-constructor expect(isArray(new Array(1, 2, 3))).to.be.true; }); }); @@ -80,10 +84,10 @@ describe('isNonEmptyArray()', () => { expect(isNonEmptyArray(undefined as any)).to.be.false; }); - const nonBooleans = [null, NaN, 0, 1, '', 'a', true, false, {}, { a: 1 }, _.noop]; - nonBooleans.forEach((nonBoolean) => { - it('should return false given a non-array argument: ' + JSON.stringify(nonBoolean), () => { - expect(isNonEmptyArray(nonBoolean as any)).to.be.false; + const nonArrays = [null, NaN, 0, 1, '', 'a', true, false, {}, { a: 1 }, _.noop]; + nonArrays.forEach((nonArray) => { + it('should return false given a non-array argument: ' + JSON.stringify(nonArray), () => { + expect(isNonEmptyArray(nonArray as any)).to.be.false; }); }); @@ -96,10 +100,12 @@ describe('isNonEmptyArray()', () => { }); it('should return false given an empty array created from Array constructor', () => { + // eslint-disable-next-line @typescript-eslint/no-array-constructor expect(isNonEmptyArray(new Array())).to.be.false; }); it('should return true given a non-empty array created from Array constructor', () => { + // eslint-disable-next-line @typescript-eslint/no-array-constructor expect(isNonEmptyArray(new Array(1, 2, 3))).to.be.true; }); }); @@ -225,10 +231,10 @@ describe('isObject()', () => { expect(isObject(undefined as any)).to.be.false; }); - const nonStrings = [NaN, 0, 1, true, false, '', 'a', _.noop]; - nonStrings.forEach((nonString) => { - it('should return false given a non-object argument: ' + JSON.stringify(nonString), () => { - expect(isObject(nonString as any)).to.be.false; + const nonObjects = [NaN, 0, 1, true, false, '', 'a', _.noop]; + nonObjects.forEach((nonObject) => { + it('should return false given a non-object argument: ' + JSON.stringify(nonObject), () => { + expect(isObject(nonObject as any)).to.be.false; }); }); @@ -258,10 +264,10 @@ describe('isNonNullObject()', () => { expect(isNonNullObject(undefined as any)).to.be.false; }); - const nonStrings = [NaN, 0, 1, true, false, '', 'a', _.noop]; - nonStrings.forEach((nonString) => { - it('should return false given a non-object argument: ' + JSON.stringify(nonString), () => { - expect(isNonNullObject(nonString as any)).to.be.false; + const nonObjects = [NaN, 0, 1, true, false, '', 'a', _.noop]; + nonObjects.forEach((nonObject) => { + it('should return false given a non-object argument: ' + JSON.stringify(nonObject), () => { + expect(isNonNullObject(nonObject as any)).to.be.false; }); }); @@ -302,7 +308,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', () => { @@ -350,7 +356,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', () => { @@ -382,6 +388,7 @@ describe('isURL()', () => { it('show return true with a valid web URL string', () => { expect(isURL('https://www.example.com:8080')).to.be.true; expect(isURL('https://www.example.com')).to.be.true; + expect(isURL('http://localhost/path/name/')).to.be.true; expect(isURL('https://www.example.com:8080/path/name/index.php?a=1&b=2&c=3#abcd')) .to.be.true; expect(isURL('http://www.example.com:8080/path/name/index.php?a=1&b=2&c=3#abcd')) @@ -452,3 +459,74 @@ describe('isPhoneNumber()', () => { expect(isPhoneNumber('+1 800 FLOwerS')).to.be.true; }); }); + +describe('isBuffer()', () => { + it('should return false given no argument', () => { + expect(isBuffer(undefined as any)).to.be.false; + }); + + const nonBuffers = [null, NaN, 0, 1, '', 'a', [], ['a'], {}, { a: 1 }, _.noop, false]; + nonBuffers.forEach((nonBuffer) => { + it('should return false given a non-buffer argument: ' + JSON.stringify(nonBuffer), () => { + expect(isBuffer(nonBuffer as any)).to.be.false; + }); + }); + + it('should return true given a buffer', () => { + expect(isBuffer(Buffer.from('I am a buffer'))).to.be.true; + }); +}); + +describe('isUTCDateString()', () => { + const validUTCDateString = 'Fri, 25 Oct 2019 04:01:21 GMT'; + it('should return false given no argument', () => { + expect(isUTCDateString(undefined as any)).to.be.false; + }); + + const nonUTCDateStrings = [ + null, NaN, 0, 1, true, false, [], ['a'], {}, { a: 1 }, _.noop, + new Date().getTime(), + new Date().getTime().toString(), + new Date().toISOString(), + 'Fri, 25 Oct 2019 04:01:21', + '25 Oct 2019', 'Fri, 25 Oct 2019', + '2019-10-25', '2019-10-25T04:07:34.036', + new Date().toDateString(), + ]; + nonUTCDateStrings.forEach((nonUTCDateString) => { + it('should return false given an invalid UTC date string: ' + JSON.stringify(nonUTCDateString), () => { + expect(isUTCDateString(nonUTCDateString as any)).to.be.false; + }); + }); + + it('should return true given a valid UTC date string', () => { + expect(isUTCDateString(validUTCDateString)).to.be.true; + }); +}); + +describe('isISODateString()', () => { + const validISODateString = '2019-10-25T04:07:34.036Z'; + it('should return false given no argument', () => { + expect(isISODateString(undefined as any)).to.be.false; + }); + + const nonISODateStrings = [ + null, NaN, 0, 1, true, false, [], ['a'], {}, { a: 1 }, _.noop, + new Date().getTime(), + new Date().getTime().toString(), + new Date().toUTCString(), + 'Fri, 25 Oct 2019 04:01:21', + '25 Oct 2019', 'Fri, 25 Oct 2019', + '2019-10-25', '2019-10-25T04:07:34.036', + new Date().toDateString(), + ]; + nonISODateStrings.forEach((nonISODateString) => { + it('should return false given an invalid ISO date string: ' + JSON.stringify(nonISODateString), () => { + expect(isISODateString(nonISODateString as any)).to.be.false; + }); + }); + + it('should return true given a valid ISO date string', () => { + expect(isISODateString(validISODateString)).to.be.true; + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 76c69140fc..1b21a0e365 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,12 +1,21 @@ { "compilerOptions": { "module": "commonjs", - "target": "es5", - "noImplicitAny": false, - "lib": ["es5", "es2015.promise", "es2015"], + "target": "es2020", + "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. + "noImplicitThis": true, + "alwaysStrict": true, + "strictBindCallApply": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + //"strictPropertyInitialization": true, + "lib": ["es2020"], "outDir": "lib", - // We manually craft typings in src/index.d.ts instead of auto-generating them. - // "declaration": true, + "stripInternal": true, "rootDir": "." }, "files": [ diff --git a/tslint-test.json b/tslint-test.json deleted file mode 100644 index 734982b50f..0000000000 --- a/tslint-test.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "extends": "tslint:latest", - "rules": { - "interface-name": false, - "member-ordering": [true, - "public-before-private", - "static-before-instance", - "variables-before-functions" - ], - "prefer-object-spread": false, - "no-implicit-dependencies": [true, "dev"], - "max-classes-per-file": false, - "max-line-length": [true, 120], - "no-consecutive-blank-lines": false, - "object-literal-sort-keys": false, - "ordered-imports": false, - "quotemark": [true, "single", "avoid-escape"], - "no-unused-expression": false, - "variable-name": [true, "ban-keywords", "check-format", "allow-trailing-underscore"] - } -} diff --git a/tslint.json b/tslint.json deleted file mode 100644 index 7d2dcb2b7c..0000000000 --- a/tslint.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "extends": "tslint:latest", - "rules": { - "interface-name": false, - "member-ordering": [true, - "public-before-private", - "static-before-instance", - "variables-before-functions" - ], - "prefer-object-spread": false, - "no-implicit-dependencies": [true, "dev"], - "max-classes-per-file": false, - "max-line-length": [true, 120], - "no-consecutive-blank-lines": false, - "object-literal-sort-keys": false, - "ordered-imports": false, - "quotemark": [true, "single", "avoid-escape"], - "variable-name": [true, "ban-keywords", "check-format", "allow-trailing-underscore"] - } -}